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. Your takeaways • Better understanding of an MVP • See

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

    those core features that allow the product to be deployed, and no more. — wikipedia
  3. What do we NEED? (think MVP) • Authentication • Manage

    clients • Generate invoices • Process invoices on due dates, retry on errors
  4. Our Tools • Laravel • Elixir (Gulp) • Bootstrap &

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

    Relationships • Blade Templating Engine • Events, Listeners, Jobs, and Queues • Task Scheduling • ...just to name a few
  8. Homestead Laravel Homestead is an official, pre-packaged Vagrant box that

    provides you a wonderful development environment -- https://laravel.com/docs/5.2/homestead
  9. 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
  10. Key Features: • Installs: Nginx, PHP 7.0, MySQL, Postgres, Redis,

    Memcached, etc • Push to deploy* • Load balancers • Queues & Crons • SSL Security
  11. 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
  12. 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!
  13. 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']); });
  14. 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>
  15. 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(); });
  16. 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]', // ... ]); } }
  17. 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); } }
  18. 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();
  19. 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);
  20. 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){} }
  21. 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']); }); });
  22. 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
  23. app/Console/Kernel.php class Kernel extends ConsoleKernel { protected $commands = [

    ProcessInvoices::class, ]; protected function schedule(Schedule $schedule) { $schedule->command('sv:process-invoices')->hourly(); } }
  24. 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!'); } } }
  25. 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(); } } }
  26. 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)); } }
  27. 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)); }
  28. 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', ], ]; }
  29. app/Events/InvoiceWasPaid class InvoiceWasPaid extends Event { public $invoice; public function

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

    Invoices • Invoice Processing • Email Notifications
  32. 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?
  33. 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]