Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Your First MVP in Laravel

Chris Gmyr
February 18, 2016

Building Your First MVP in Laravel

Chris Gmyr

February 18, 2016
Tweet

More Decks by Chris Gmyr

Other Decks in Technology

Transcript

  1. Building Your First
    MVP in Laravel
    @cmgmyr

    View full-size slide

  2. Your takeaways
    • Better understanding of an MVP
    • See how Laravel can help you create an MVP faster

    View full-size slide

  3. What is an MVP?
    A minimum viable product has just those core features that allow
    the product to be deployed, and no more.
    — wikipedia

    View full-size slide

  4. What are we building?

    View full-size slide

  5. SlickVoice.io
    Invoicing and payment service built with Lavavel and Stripe.

    View full-size slide

  6. What do we NEED? (think MVP)
    • Authentication
    • Manage clients
    • Generate invoices
    • Process invoices on due dates, retry on errors

    View full-size slide

  7. How much time would this MVP
    take?
    ...from start to deploy

    View full-size slide

  8. Our Tools
    • Laravel
    • Elixir (Gulp)
    • Bootstrap & jQuery
    • Homestead (local development)
    • Forge (server provisioning)
    • Envoyer (zero downtime deployments)

    View full-size slide

  9. Why Laravel?
    • Modern PHP Framework
    • Utilizes Composer
    • Aids in RAD (Rapid Application Development)
    • Simple to read API
    • Community & Resources
    • Easy path from development to deployment

    View full-size slide

  10. Community & Resources
    • laravel.com (docs & API explorer)
    • laracasts.com (video tutorials, forums)
    • laravel.io (forums)
    • laravel-news.com (articles, packages)
    • larachat.co (Slack chat)
    • Laracon (Conference) US & UK
    • ...and so many other books, blogs, videos, sites

    View full-size slide

  11. Key Laravel Features
    • Authentication
    • Eloquent ORM & Model Relationships
    • Blade Templating Engine
    • Events, Listeners, Jobs, and Queues
    • Task Scheduling
    • ...just to name a few

    View full-size slide

  12. Development -> Deployment

    View full-size slide

  13. Develop! With Homestead

    View full-size slide

  14. Homestead
    Laravel Homestead is an official, pre-packaged Vagrant box that
    provides you a wonderful development environment
    -- https://laravel.com/docs/5.2/homestead

    View full-size slide

  15. Homestead Included Software
    • Ubuntu 14.04
    • Git
    • PHP 7.0
    • HHVM
    • Nginx
    • MySQL, Sqlite3, Postgres
    • Composer
    • Node (With PM2, Bower, Grunt, and Gulp)
    • Redis
    • Memcached
    • Beanstalkd

    View full-size slide

  16. Provision! With Forge

    View full-size slide

  17. Key Features:
    • Installs: Nginx, PHP 7.0, MySQL, Postgres, Redis,
    Memcached, etc
    • Push to deploy*
    • Load balancers
    • Queues & Crons
    • SSL Security

    View full-size slide

  18. Deploy! With Envoyer

    View full-size slide

  19. Homestead -> Forge -> Envoyer

    View full-size slide

  20. Install Laravel (in Homestead)
    composer create-project --prefer-dist laravel/laravel slickvoice_mvp

    View full-size slide

  21. Laravel's File Structure
    app/
    config/
    database/
    public/
    resources/
    .env
    artisan
    composer.json
    gulpfile.js

    View full-size slide

  22. Artisan CLI
    Artisan is the name of the command-line
    interface included with Laravel. It
    provides a number of helpful commands
    for your use while developing your
    application. It is driven by the powerful
    Symfony Console component. To view a
    list of all available Artisan commands,
    you may use the list command:
    php artisan list
    -- https://laravel.com/docs/5.2/artisan

    View full-size slide

  23. Artisan Highlights
    • make:[controller|event|job|migration|model|seeder|...]
    • migrate[:refresh]
    • queue:listen
    • schedule:run
    • ...and custom commands!
    • sv:process-invoices

    View full-size slide

  24. Easy win for Laravel/Artisan

    View full-size slide

  25. Authentication Scaffolding
    $ php artisan make:auth
    Created View: /slickvoice_mvp/resources/views/auth/login.blade.php
    Created View: /slickvoice_mvp/resources/views/auth/register.blade.php
    Created View: /slickvoice_mvp/resources/views/auth/passwords/email.blade.php
    Created View: /slickvoice_mvp/resources/views/auth/passwords/reset.blade.php
    Created View: /slickvoice_mvp/resources/views/auth/emails/password.blade.php
    Created View: /slickvoice_mvp/resources/views/layouts/app.blade.php
    Created View: /slickvoice_mvp/resources/views/home.blade.php
    Created View: /slickvoice_mvp/resources/views/welcome.blade.php
    Installed HomeController.
    Updated Routes File.
    Authentication scaffolding generated successfully!

    View full-size slide

  26. What do we have so far?

    View full-size slide

  27. Thanks Laravel!
    ...but let's make it better

    View full-size slide

  28. Laravel Elixir (Gulp)
    var elixir = require('laravel-elixir');
    var paths = {
    'bootstrap': 'vendor/bower_components/bootstrap/',
    'jquery': 'vendor/bower_components/jquery/dist/',
    'fontawesome': 'vendor/bower_components/fontawesome/'
    };
    elixir(function(mix) {
    mix.sass('app.scss')
    .scripts([
    paths.jquery + 'jquery.js',
    paths.bootstrap + 'dist/js/bootstrap.js',
    'resources/assets/js/app.js'
    ], 'public/js/app.js', './')
    .copy(paths.bootstrap + 'fonts/**', 'public/build/fonts')
    .copy(paths.fontawesome + 'fonts/**', 'public/build/fonts')
    .version(['css/app.css', 'js/app.js']);
    });

    View full-size slide

  29. Blade
    resources/views/layouts/app.blade.php


    SlickVoice






    @yield('content')





    View full-size slide

  30. A little design facelift...

    View full-size slide

  31. Migrations & Seeders

    View full-size slide

  32. Create Migrations
    $ php artisan make:migration CreateClientsTable
    Schema::create('clients', function (Blueprint $table) {
    $table->increments('id')->unsigned();
    $table->string('stripe_id')->unique();
    $table->string('email')->unique();
    $table->string('name')->nullable();
    $table->integer('card_last_four');
    $table->integer('card_exp_month');
    $table->integer('card_exp_year');
    $table->string('card_brand');
    // ... address & phone info
    $table->timestamps();
    });

    View full-size slide

  33. Populate tables with initial data
    class DatabaseSeeder extends Seeder
    {
    public function run()
    {
    $this->call(ClientsTableSeeder::class);
    // $this->call(InvoicesTableSeeder::class);
    // $this->call(InvoiceItemsTableSeeder::class);
    }
    }
    class ClientsTableSeeder extends Seeder
    {
    public function run()
    {
    Client::create([
    'stripe_id' => 'cust_123',
    'name' => 'Chris Gmyr',
    'email' => '[email protected]',
    // ...
    ]);
    }
    }

    View full-size slide

  34. Models & Eloquent

    View full-size slide

  35. Create a Model
    $ php artisan make:model Client

    View full-size slide

  36. app/Client.php
    use Illuminate\Database\Eloquent\Model;
    class Client extends Model
    {
    protected $table = 'clients'; // optional, but I like it
    protected $fillable = [
    'stripe_id', 'email', 'name', 'card_last_four', // list other columns here...
    ];
    public function invoices()
    {
    return $this->hasMany(Invoice::class);
    }
    }

    View full-size slide

  37. Eloquent is...Eloquent
    // get all clients
    $clients = Client::all();
    // Find client by id
    $client = Client::find(1);
    // Find client by stripe_id
    $client = Client::where('stripe_id', 'cus_123')->first();
    // Dynamic methods
    $client = Client::whereStripeId('cust_123')->first();
    // Create a client
    $client = Client::create([
    'stripe_id' => 'cus_123',
    'name' => 'Test Client',
    'email' => '[email protected]',
    // ...
    ]);
    // Update a client
    $client->email = '[email protected]';
    $client->save();

    View full-size slide

  38. Relationships are easier with Eloquent
    // All invoices for a client
    $clientInvoices = $client->invoices->all();
    // Get pending invoices for a client
    $pending = $client->invoices()->where('status', 'pending')->get();
    // Add invoice to a client
    $invoice = Invoice::create(['data' => 'here']);
    $client->invoices()->save($invoice);
    // Add invoice items to an invoice
    $items[] = InvoiceItem::create(['data' => 'here']);
    $items[] = InvoiceItem::create(['data' => 'here']);
    $invoice->items()->saveMany($items);

    View full-size slide

  39. Controllers & Routes

    View full-size slide

  40. Create a Controller
    $ php artisan make:controller ClientsController --resource

    View full-size slide

  41. namespace Sv\Http\Controllers;
    class ClientsController extends Controller
    {
    // Display a listing of the resource.
    public function index(){}
    // Show the form for creating a new resource.
    public function create(){}
    // Store a newly created resource in storage.
    public function store(Request $request){}
    // Display the specified resource.
    public function show($id){}
    // Show the form for editing the specified resource.
    public function edit($id){}
    // Update the specified resource in storage.
    public function update(Request $request, $id){}
    // Remove the specified resource from storage.
    public function destroy($id){}
    }

    View full-size slide

  42. Build Routes
    Route::group(['middleware' => 'auth'], function () {
    Route::group(['prefix' => 'clients'], function () {
    Route::get('/', ['as' => 'clients.index', 'uses' => 'ClientsController@index']);
    Route::get('create', ['as' => 'clients.create', 'uses' => 'ClientsController@create']);
    Route::post('/', ['as' => 'clients.store', 'uses' => 'ClientsController@store']);
    Route::get('{id}/edit', ['as' => 'clients.edit', 'uses' => 'ClientsController@edit']);
    Route::put('{id}', ['as' => 'clients.update', 'uses' => 'ClientsController@update']);
    Route::delete('{id}', ['as' => 'clients.destroy', 'uses' => 'ClientsController@destroy']);
    });
    });

    View full-size slide

  43. Putting it together

    View full-size slide

  44. Client Index
    public function index()
    {
    $clients = Client::orderBy('name', 'ASC')->simplePaginate(15);
    return view('clients.index', compact('clients'));
    }

    View full-size slide

  45. resources/views/clients/index.blade.php
    @section('content')



    Stripe ID
    Name
    Email
    Manage



    @foreach($clients as $client)

    {{ $client->stripe_id }}
    {{ $client->name }}
    {{ $client->email }}

    Edit


    @endforeach


    @stop
    @section('pagination')
    {!! $clients->render() !!}
    @stop

    View full-size slide

  46. Fast Forward!
    • Build out Clients CRUD
    • Build out Invoices & Items CRUD

    View full-size slide

  47. Processing Invoices
    Artisan Commands & Task Scheduling

    View full-size slide

  48. app/Console/Kernel.php
    class Kernel extends ConsoleKernel
    {
    protected $commands = [
    ProcessInvoices::class,
    ];
    protected function schedule(Schedule $schedule)
    {
    $schedule->command('sv:process-invoices')->hourly();
    }
    }

    View full-size slide

  49. app/Console/Commands/ProcessInvoices.php
    class ProcessInvoices extends Command
    {
    use DispatchesJobs;
    protected $signature = 'sv:process-invoices';
    protected $description = 'Processes pending invoices for the day';
    public function handle()
    {
    $invoices = Invoice::whereIn('status', ['pending', 'overdue'])
    ->where('num_tries', '<=', 3)
    ->whereDate('try_on_date', '<=', Carbon::today()->toDateString())
    ->get();
    if ($invoices->count() > 0) {
    foreach ($invoices as $invoice) {
    $this->dispatch(new PayInvoice($invoice));
    }
    $this->info('Invoices Processed!');
    }
    }
    }

    View full-size slide

  50. app/Jobs/PayInvoice.php
    class PayInvoice extends Job implements ShouldQueue
    {
    public function __construct(Invoice $invoice)
    {
    $this->invoice = $invoice;
    }
    public function handle()
    {
    $stripe_id = $this->invoice->client->stripe_id;
    $total = $this->invoice->items->sum('price') * 100; // stripe uses cents instead of dollars
    try {
    // ...
    } catch (Exception $e) {
    $this->tryAgain();
    }
    }
    }

    View full-size slide

  51. try {
    $charge = StripeCharge::create([
    'amount' => $total,
    'currency' => 'usd',
    'customer' => $stripe_id,
    ]);
    // change status, add charge date, charge id, etc...save
    event(new InvoiceWasPaid($this->invoice));
    if ($this->invoice->repeat != 'no') {
    $this->dispatch(new DuplicateInvoice($this->invoice, $this->invoice->repeat));
    }
    }

    View full-size slide

  52. protected function tryAgain()
    {
    $this->invoice->status = 'overdue';
    if ($this->invoice->num_tries >= 3) {
    $this->invoice->status = 'error';
    }
    $this->invoice->try_on_date = Carbon::tomorrow();
    $this->invoice->increment('num_tries');
    $this->invoice->save();
    event(new InvoiceWasNotPaid($this->invoice));
    }

    View full-size slide

  53. Events & Listeners

    View full-size slide

  54. app/Providers/EventServiceProvider.php
    class EventServiceProvider extends ServiceProvider
    {
    protected $listen = [
    'Sv\Events\InvoiceWasPaid' => [
    'Sv\Listeners\SendClientPaidInvoice',
    'Sv\Listeners\SendAdminsPaidInvoice',
    ],
    'Sv\Events\InvoiceWasNotPaid' => [
    'Sv\Listeners\SendClientNotPaidInvoice',
    'Sv\Listeners\SendAdminsNotPaidInvoice',
    ],
    ];
    }

    View full-size slide

  55. app/Events/InvoiceWasPaid
    class InvoiceWasPaid extends Event
    {
    public $invoice;
    public function __construct(Invoice $invoice)
    {
    $this->invoice = $invoice;
    }
    }

    View full-size slide

  56. app/Listeners/SendClientPaidInvoice.php
    class SendClientPaidInvoice implements ShouldQueue
    {
    public function handle(InvoiceWasPaid $event)
    {
    $invoice = $event->invoice;
    Mail::queue('emails.invoice', compact('invoice'), function ($m) use ($invoice) {
    $m->to($invoice->client->email, $invoice->client->name);
    $m->subject('Your Payment Receipt!');
    });
    }
    }

    View full-size slide

  57. Where are we now?
    • Authentication
    • CRUD Clients & Invoices
    • Invoice Processing
    • Email Notifications

    View full-size slide

  58. All of Our Needs
    With a Few Extras!

    View full-size slide

  59. How much time would this have taken in
    another language, framework, and/or
    platform?
    • Building similar components found in Laravel?
    • Local development setup?
    • Provisioning a server?
    • (Making sure these environments match?)
    • Deployments that don't take down your site?

    View full-size slide

  60. Time to Complete this MVP?

    View full-size slide

  61. LIVE DEMO!!!

    View full-size slide

  62. Thank you!
    Feedback:
    Joind.in: https://joind.in/talk/ab6ad (or search TrianglePHP)
    Download:
    Source: github.com/cmgmyr/mvp.slickvoice.io
    Slides: speakerdeck.com/cmgmyr/building-your-first-mvp-in-laravel
    Connect:
    cmgmyr [gmail | twitter | github | .com]

    View full-size slide