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. 2.
  2. 3.

    Your takeaways • Better understanding of an MVP • See

    how Laravel can help you create an MVP faster
  3. 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
  4. 5.
  5. 8.

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

    clients • Generate invoices • Process invoices on due dates, retry on errors
  6. 10.

    Our Tools • Laravel • Elixir (Gulp) • Bootstrap &

    jQuery • Homestead (local development) • Forge (server provisioning) • Envoyer (zero downtime deployments)
  7. 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
  8. 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
  9. 13.

    Key Laravel Features • Authentication • Eloquent ORM & Model

    Relationships • Blade Templating Engine • Events, Listeners, Jobs, and Queues • Task Scheduling • ...just to name a few
  10. 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
  11. 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
  12. 19.

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

    Memcached, etc • Push to deploy* • Load balancers • Queues & Crons • SSL Security
  13. 20.
  14. 22.
  15. 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
  16. 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!
  17. 31.
  18. 32.
  19. 33.
  20. 34.
  21. 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']); });
  22. 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>
  23. 39.
  24. 40.
  25. 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(); });
  26. 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', // ... ]); } }
  27. 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); } }
  28. 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();
  29. 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);
  30. 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){} }
  31. 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']); }); });
  32. 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
  33. 56.
  34. 59.

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

    ProcessInvoices::class, ]; protected function schedule(Schedule $schedule) { $schedule->command('sv:process-invoices')->hourly(); } }
  35. 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!'); } } }
  36. 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(); } } }
  37. 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)); } }
  38. 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)); }
  39. 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', ], ]; }
  40. 66.

    app/Events/InvoiceWasPaid class InvoiceWasPaid extends Event { public $invoice; public function

    __construct(Invoice $invoice) { $this->invoice = $invoice; } }
  41. 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!'); }); } }
  42. 68.

    Where are we now? • Authentication • CRUD Clients &

    Invoices • Invoice Processing • Email Notifications
  43. 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?
  44. 72.
  45. 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]