Building Your First MVP in Laravel

6834447bdbca70cd2390adc690c3b4f1?s=47 Chris Gmyr
February 18, 2016

Building Your First MVP in Laravel

6834447bdbca70cd2390adc690c3b4f1?s=128

Chris Gmyr

February 18, 2016
Tweet

Transcript

  1. Building Your First MVP in Laravel @cmgmyr

  2. None
  3. Your takeaways • Better understanding of an MVP • See

    how Laravel can help you create an MVP faster
  4. What is an MVP? A minimum viable product has just

    those core features that allow the product to be deployed, and no more. — wikipedia
  5. None
  6. What are we building?

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

  8. What do we NEED? (think MVP) • Authentication • Manage

    clients • Generate invoices • Process invoices on due dates, retry on errors
  9. How much time would this MVP take? ...from start to

    deploy
  10. Our Tools • Laravel • Elixir (Gulp) • Bootstrap &

    jQuery • Homestead (local development) • Forge (server provisioning) • Envoyer (zero downtime deployments)
  11. 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
  12. 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
  13. Key Laravel Features • Authentication • Eloquent ORM & Model

    Relationships • Blade Templating Engine • Events, Listeners, Jobs, and Queues • Task Scheduling • ...just to name a few
  14. Development -> Deployment

  15. Develop! With Homestead

  16. Homestead Laravel Homestead is an official, pre-packaged Vagrant box that

    provides you a wonderful development environment -- https://laravel.com/docs/5.2/homestead
  17. 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
  18. Provision! With Forge

  19. Key Features: • Installs: Nginx, PHP 7.0, MySQL, Postgres, Redis,

    Memcached, etc • Push to deploy* • Load balancers • Queues & Crons • SSL Security
  20. None
  21. Deploy! With Envoyer

  22. None
  23. Homestead -> Forge -> Envoyer

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

  25. Laravel's File Structure app/ config/ database/ public/ resources/ .env artisan

    composer.json gulpfile.js
  26. 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
  27. Artisan Highlights • make:[controller|event|job|migration|model|seeder|...] • migrate[:refresh] • queue:listen • schedule:run

    • ...and custom commands! • sv:process-invoices
  28. Easy win for Laravel/Artisan

  29. 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!
  30. What do we have so far?

  31. None
  32. None
  33. None
  34. None
  35. Thanks Laravel! ...but let's make it better

  36. 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']); });
  37. Blade resources/views/layouts/app.blade.php <!DOCTYPE html> <head> <title>SlickVoice</title> <link rel="stylesheet" href="{{ elixir("css/app.css")

    }}"> </head> <body> <div class="container"> <div class="row"> <div class="col-md-12"> @yield('content') </div> </div> </div> <script src="{{ elixir("js/app.js") }}"></script> </body> </html>
  38. A little design facelift...

  39. None
  40. None
  41. Migrations & Seeders

  42. 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(); });
  43. 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' => 'cmgmyr@gmail.com', // ... ]); } }
  44. Models & Eloquent

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

  46. 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); } }
  47. 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' => 'test@customer.com', // ... ]); // Update a client $client->email = 'newemail@customer.com'; $client->save();
  48. 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);
  49. Controllers & Routes

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

  51. 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){} }
  52. 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']); }); });
  53. Putting it together

  54. Client Index public function index() { $clients = Client::orderBy('name', 'ASC')->simplePaginate(15);

    return view('clients.index', compact('clients')); }
  55. resources/views/clients/index.blade.php @section('content') <table class="table table-condensed"> <thead> <tr> <td>Stripe ID</td> <td>Name</td>

    <td>Email</td> <td>Manage</td> </tr> </thead> <tbody> @foreach($clients as $client) <tr> <td>{{ $client->stripe_id }}</td> <td>{{ $client->name }}</td> <td>{{ $client->email }}</td> <td> <a href="{{ route('clients.edit', $client->id) }}">Edit</a> </td> </tr> @endforeach </tbody> </table> @stop @section('pagination') <div class="text-center">{!! $clients->render() !!}</div> @stop
  56. None
  57. Fast Forward! • Build out Clients CRUD • Build out

    Invoices & Items CRUD
  58. Processing Invoices Artisan Commands & Task Scheduling

  59. app/Console/Kernel.php class Kernel extends ConsoleKernel { protected $commands = [

    ProcessInvoices::class, ]; protected function schedule(Schedule $schedule) { $schedule->command('sv:process-invoices')->hourly(); } }
  60. 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!'); } } }
  61. 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(); } } }
  62. 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)); } }
  63. 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)); }
  64. Events & Listeners

  65. 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', ], ]; }
  66. app/Events/InvoiceWasPaid class InvoiceWasPaid extends Event { public $invoice; public function

    __construct(Invoice $invoice) { $this->invoice = $invoice; } }
  67. 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!'); }); } }
  68. Where are we now? • Authentication • CRUD Clients &

    Invoices • Invoice Processing • Email Notifications
  69. All of Our Needs With a Few Extras!

  70. 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?
  71. Time to Complete this MVP?

  72. ~18 Hours

  73. LIVE DEMO!!!

  74. 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]