$30 off During Our Annual Pro Sale. View Details »

Custom Laravel

Custom Laravel

Given at Laracon 2017.

Matt Stauffer

July 26, 2017
Tweet

More Decks by Matt Stauffer

Other Decks in Technology

Transcript

  1. customlaravel.com by @stauffermatt

    View Slide

  2. Before we start... !
    customlaravel.com by @stauffermatt

    View Slide

  3. customlaravel.com by @stauffermatt

    View Slide

  4. One more thing...
    customlaravel.com by @stauffermatt

    View Slide

  5. A brief history of my talks.
    customlaravel.com by @stauffermatt

    View Slide

  6. Why Modern PHP Is Amazing And
    How You Can Use It Today
    PeersConf 2014
    customlaravel.com by @stauffermatt

    View Slide

  7. Sharing Laravel
    Laracon Eu 2014
    customlaravel.com by @stauffermatt

    View Slide

  8. Leveraging Laravel
    Laracon US 2015
    customlaravel.com by @stauffermatt

    View Slide

  9. Empathy Gives You Superpowers
    Laracon Eu 2015
    customlaravel.com by @stauffermatt

    View Slide

  10. Mastering the
    Illuminate Container
    Laracon Online 2017
    customlaravel.com by @stauffermatt

    View Slide

  11. Custom Laravel
    Laracon US 2017
    customlaravel.com by @stauffermatt

    View Slide

  12. Wherefore
    "Custom Laravel"?
    customlaravel.com by @stauffermatt

    View Slide

  13. PART I
    The soul of customization
    customlaravel.com by @stauffermatt

    View Slide

  14. Laravel:
    We've got the
    best defaults
    customlaravel.com by @stauffermatt

    View Slide

  15. Laravel:
    Convention over
    configuration
    customlaravel.com by @stauffermatt

    View Slide

  16. Laravel:
    Convention and over
    (when helpful) configuration
    customlaravel.com by @stauffermatt

    View Slide

  17. Customizing Laravel: the basics
    ▸ Built-to-be-modified base code (/app/*)
    ▸ The most flexible container you've ever seen, and Illuminate Contracts
    ▸ Macros, façades, re-binding, and more
    ▸ Environment variables and conditional config
    ▸ It's Just PHP™
    customlaravel.com by @stauffermatt

    View Slide

  18. What you choose to
    customize matters.
    customlaravel.com by @stauffermatt

    View Slide

  19. “Taking it too far is a rite of passage”
    - Jeffrey Way
    customlaravel.com by @stauffermatt

    View Slide

  20. TwentyPercent.fm
    !
    macros... ?
    (an anecdote)
    customlaravel.com by @stauffermatt

    View Slide

  21. One of the
    hardest things to discover
    in any new app is:
    "where/how is this happening?"
    customlaravel.com by @stauffermatt

    View Slide

  22. Prioritize
    Discoverability
    customlaravel.com by @stauffermatt

    View Slide

  23. Write your apps so
    anyone who has read the
    Laravel docs can jump
    into them easily
    customlaravel.com by @stauffermatt

    View Slide

  24. LARAVELUPANDRUNNING.COM
    buy
    customlaravel.com by @stauffermatt

    View Slide

  25. Write your apps so
    anyone who has read the
    Laravel docs can jump
    into them easily
    customlaravel.com by @stauffermatt

    View Slide

  26. The opposite of discoverability is...
    ! CUSTOMIZATION DEBT !
    customlaravel.com by @stauffermatt

    View Slide

  27. customlaravel.com by @stauffermatt

    View Slide

  28. Discoverability caveat
    Just because something is less discoverable
    doesn't mean it's bad.
    Sometimes the best tool isn't the most discoverable.
    Sometimes it's us that needs to change, not the tool.
    customlaravel.com by @stauffermatt

    View Slide

  29. Discoverability Disasters
    ▸ Overriding default methods like create()
    ▸ Modifying the request object
    ▸ Route::controller
    ▸ Custom façades that aren't façades
    ▸ Anything makes you go "that's so clever" instead of "that's so simple"
    customlaravel.com by @stauffermatt

    View Slide

  30. customlaravel.com by @stauffermatt

    View Slide

  31. Tag::create(request()->all());
    customlaravel.com by @stauffermatt

    View Slide

  32. How to
    customize Laravel,
    and how to do it
    responsibly
    customlaravel.com by @stauffermatt

    View Slide

  33. PART II
    Customization
    tips & tricks
    customlaravel.com by @stauffermatt

    View Slide

  34. customlaravel.com by @stauffermatt

    View Slide

  35. Preface:
    the container and service providers
    customlaravel.com by @stauffermatt

    View Slide

  36. The container in 5 minutes
    ▸ registering things into it
    ▸ resolving things out of it
    ▸ things Laravel resolves using it
    ▸ autowiring vs. explicit
    ▸ service providers
    ▸ aliases and Facades
    customlaravel.com by @stauffermatt

    View Slide

  37. Registering things into the container
    ▸ Key value store: put this thing at this address
    ▸ "Address": a string, usually a fully-qualified class name or a shortcut
    string like "logger"
    ▸ "Thing": usually either a class name, a Closure, or an object
    app()->bind('address', Thing::class);
    app()->bind('logger', Logger::class);
    customlaravel.com by @stauffermatt

    View Slide

  38. Resolving things out of the container
    ▸ Ask for the "thing" at the given "address"
    ▸ "give me the thing for address 'logger'"
    ▸ app()->make('logger') returns an instance of Logger
    ▸ Also can resolve by type hinting a container-resolved class like a
    Controller
    customlaravel.com by @stauffermatt

    View Slide

  39. Things Laravel resolves using the container
    ▸ Anything Laravel creates itself it creates using the container
    ▸ Controllers, middleware, etc.
    ▸ Why we can type hint dependencies in a controller
    ▸ Also some methods; controller methods, firing jobs, etc.
    class MyController {
    public function __construct(APIClient $client) {
    customlaravel.com by @stauffermatt

    View Slide

  40. Autowiring vs. explicit binding
    ▸ The container knows how by default; ask for "App\Thing" and it tries
    new App\Thing
    ▸ If App\Thing has dependencies it'll do the same for each
    ▸ If all dependencies are easy to resolve then you're good! If not..
    ▸ app()->bind(Thing::class, function () {
    return new App\Thing('my-api-code'); });
    customlaravel.com by @stauffermatt

    View Slide

  41. Aliases and Facades
    ▸ You can "alias" one address to another:
    app()->alias('logger', SomeOtherLogger::class)
    ▸ "When I ask for SomeOtherLogger, give me 'logger'"
    ▸ Façades say "make a new class; let me call statics on it and proxy them
    through to an instance of another class"
    customlaravel.com by @stauffermatt

    View Slide

  42. customlaravel.com by @stauffermatt

    View Slide

  43. Back to the show
    customlaravel.com by @stauffermatt

    View Slide

  44. Custom
    Laravel
    101
    customlaravel.com by @stauffermatt

    View Slide

  45. Config
    ▸ Make custom config file keys (services.mailchimp.key)
    ▸ Make custom config files (config/chat.php)
    ▸ Make up your own .env variables and import into your config
    ▸ Important: Don't directly reference env() inline!
    customlaravel.com by @stauffermatt

    View Slide

  46. Default controllers
    ▸ Default auth controllers provide customization hooks:
    ▸ Customize views; e.g. auth.passwords.email
    ▸ Customize return routes; e.g.
    LoginController@$redirectTo
    ▸ Override protected methods; e.g. validateEmail(),
    sendResetLinkResponse(), validateLogin()
    customlaravel.com by @stauffermatt

    View Slide

  47. Default controllers (cont)
    ▸ Override intended "hook" methods; e.g.
    LoginController@authenticated(),
    LoginController@username()
    ▸ Some are placed in your default controller
    (RegisterController@create) but all are adjustable
    customlaravel.com by @stauffermatt

    View Slide

  48. Default providers
    ▸ AppServiceProvider ! a.k.a. core hacking for newbs
    class AppServiceProvider extends ServiceProvider
    {
    public function boot() {
    //
    }
    public function register() {
    //
    }
    customlaravel.com by @stauffermatt

    View Slide

  49. Default providers (cont)
    ▸ AuthServiceProvider@boot and
    EventServiceProvider@boot
    class AuthServiceProvider extends ServiceProvider
    {
    protected $policies = [
    'App\Model' => 'App\Policies\ModelPolicy',
    ];
    public function boot() {
    $this->registerPolicies();
    //
    }
    customlaravel.com by @stauffermatt

    View Slide

  50. Default providers (cont)
    ▸ RouteServiceProvider@boot, @map !
    class RouteServiceProvider extends ServiceProvider
    {
    public function boot()
    {
    //
    parent::boot();
    }
    public function map()
    {
    $this->mapApiRoutes();
    $this->mapWebRoutes();
    //
    }
    protected function mapWebRoutes()
    {
    Route::middleware('web')
    ->namespace($this->namespace)
    ->group(base_path('routes/web.php'));
    }
    customlaravel.com by @stauffermatt

    View Slide

  51. Pro tip
    Any time the stock files have //,
    that's a sign you should consider customizing there.
    class AuthServiceProvider extends ServiceProvider
    {
    public function boot()
    {
    $this->registerPolicies();
    //
    }
    customlaravel.com by @stauffermatt

    View Slide

  52. Default middleware
    ▸ EncryptCookies@except, TrimStrings@except,
    VerifyCsrfTokens@except
    ▸ RedirectIfAuthenticated: redirect('/home')
    class EncryptCookies or TrimStrings or VerifyCsrfTokens
    {
    protected $except = [
    //
    ];
    customlaravel.com by @stauffermatt

    View Slide

  53. Custom middleware groups
    ▸ api and web out of the box
    ▸ Add more in App\Http\Kernel@$middlewareGroups
    customlaravel.com by @stauffermatt

    View Slide

  54. Custom middleware groups (cont)
    class Kernel extends HttpKernel
    {
    protected $middlewareGroups = [
    'web' => [
    \App\Http\Middleware\EncryptCookies::class,
    // ...
    ],
    'api' => [
    'throttle:60,1',
    'bindings',
    ],
    'admin' [
    'web',
    'some-other-awesome-middleware',
    ]
    ];
    customlaravel.com by @stauffermatt

    View Slide

  55. Custom middleware
    ▸ Make your own: watch for "guards", etc. in your controller methods
    ▸ php artisan make:middleware
    KeepOutBadPeople
    ▸ Apply globally, add to a custom middleware group, make route optional
    customlaravel.com by @stauffermatt

    View Slide

  56. Custom middleware (cont)
    class KeepOutBadPeople
    {
    public function handle($request, Closure $next)
    {
    if ($this->isBad($request->person)) {
    abort(404);
    }
    return $next($request);
    }
    customlaravel.com by @stauffermatt

    View Slide

  57. Exceptions
    ▸ Custom exceptions (e.g.
    TryingToHackUsWhatTheHeckException)
    ▸ Custom global exception handling rules
    customlaravel.com by @stauffermatt

    View Slide

  58. Exceptions (cont)
    Customize exception handling: Pre-5.5
    App\Exceptions\Handler@report;
    check for instance type and respond
    Customize exception handling: 5.5+
    Define how to report an exception on the exception; @report()
    customlaravel.com by @stauffermatt

    View Slide

  59. Includes
    ▸ Extract chunks of code to includes
    ▸ See: routes/web.php from before; also console.php
    ▸ What about schedule.php?
    customlaravel.com by @stauffermatt

    View Slide

  60. Includes (cont)
    // From this:
    protected function schedule(Schedule $schedule) {
    // $schedule->command('inspire')
    // ->hourly();
    }
    // To this:
    protected function schedule(Schedule $schedule) {
    require base_path('schedule.php');
    }
    customlaravel.com by @stauffermatt

    View Slide

  61. Frontend
    ▸ php artisan make:auth
    ▸ 5.5 Frontend presets
    ▸ Default files are just that; defaults!
    customlaravel.com by @stauffermatt

    View Slide

  62. GIF BREAK
    customlaravel.com by @stauffermatt

    View Slide

  63. customlaravel.com by @stauffermatt

    View Slide

  64. Custom
    Laravel
    201
    customlaravel.com by @stauffermatt

    View Slide

  65. Tests
    ▸ Add assertions to the base test class;
    e.g. assertUserIsAdmin
    ▸ Re-bind/mock classes, migrate/seed in setUp
    ▸ Custom variables in phpunit.xml
    ▸ Test-specific .env.testing
    ▸ RefreshDatabase migration trait, coming in 5.5
    customlaravel.com by @stauffermatt

    View Slide

  66. Helpers
    ▸ Bring back helpers.php!
    ▸ What works for your domain? sync(), etc.
    {
    "autoload": {
    "files": ["helpers.php"]
    }
    }
    customlaravel.com by @stauffermatt

    View Slide

  67. Advanced container binding/re-binding
    ▸ Get familiar with app()->bind() and its cousins
    ▸ Step 1. Bind your own class
    app()->bind(MyClass::class, Closure)
    ▸ Step 2. Bind your class to interfaces
    app()->bind(MyInterface::class,
    ClosureOrClassName)
    customlaravel.com by @stauffermatt

    View Slide

  68. Advanced container binding/re-binding (cont)
    ▸ Step 3. Bind your class to a global shortcut
    app()->bind('myalias', ClosureOrClassName)
    ▸ Step 4. Re-bind other classes/interfaces to that global shortcut
    app()->alias('myalias',
    SomeContractClassName)
    customlaravel.com by @stauffermatt

    View Slide

  69. An example
    class MyContainer extends Container
    {
    public function storagePath()
    {
    return $this->basePath . '/storageOMGyay';
    }
    }
    customlaravel.com by @stauffermatt

    View Slide

  70. Advanced container binding/re-binding (cont)
    Example from Bugsnag docs:
    $this->app->singleton('bugsnag.logger', function (Container $app) {
    return new LaravelLogger($app['bugsnag']);
    });
    $this->app->singleton('bugsnag.multi', function (Container $app) {
    return new MultiLogger([$app['log'], $app['bugsnag.logger']]);
    });
    Bind:
    $this->app->alias('bugsnag.logger', \Illuminate\Contracts\Logging\Log::class);
    $this->app->alias('bugsnag.logger', \Psr\Log\LoggerInterface::class);
    customlaravel.com by @stauffermatt

    View Slide

  71. Contracts
    Interfaces to all major Laravel components
    Illuminate\Contracts\*
    namespace Illuminate\Contracts\Logging;
    interface Log
    {
    public function alert($message, array $context = []);
    public function critical($message, array $context = []);
    // ...
    customlaravel.com by @stauffermatt

    View Slide

  72. Breathe
    customlaravel.com by @stauffermatt

    View Slide

  73. Packages & Publishing
    ▸ Packages often allow you to "publish" config and view files
    ▸ E.g. customize Spark views or Media-Library config by running:
    ▸ php artisan vendor:publish
    ▸ Note: 5.5 vendor:publish = !
    customlaravel.com by @stauffermatt

    View Slide

  74. Custom Façades
    Step 1. Create the Façade class extending
    Illuminate\Support\Facades\Facade
    Step 2. Implement getFacadeAccessor() method; return a string
    that would resolve out of the container
    Step 3. Register it in config/app.php@aliases
    customlaravel.com by @stauffermatt

    View Slide

  75. Façade example
    // FitbitServiceProvider.php
    public function register() {
    $this->app->singleton('fitbit', function () { ... });
    }
    // Somewhere
    class Fitbit extends Illuminate\Support\Facades\Facade {
    public function getFacadeAccessor() { return 'fitbit'; }
    }
    // In consuming code (controller, etc.)
    Fitbit::nonStaticMethodOnFitbitClass();
    customlaravel.com by @stauffermatt

    View Slide

  76. Swapping Façades and bindings in tests
    ▸ Swappable façades e.g.:
    Cache::shouldReceive('get')->once()->with('key')-
    >andReturn('value')
    ▸ Shout out to MailThief ❤
    ▸ Swap `em yourself!
    customlaravel.com by @stauffermatt

    View Slide

  77. Custom validation: pre-5.5
    // In a service provider:
    Validator::extend('repo', function ($attribute, $value, $parameters, $validator) {
    return app(Client::class)->validRepository($url);
    });
    // or
    Validator::extend('repo', 'RepoValidator@validate');
    // .. in your language file:
    [
    "repo" => "The given repository is invalid.",
    ]
    customlaravel.com by @stauffermatt

    View Slide

  78. Custom validation: 5.5+
    class ValidRepository implements Rule
    {
    public function __construct($source, $branch)
    {
    $this->source = $source;
    $this->branch = $branch;
    }
    public function passes($attribute, $value)
    {
    if (! $this->source instanceof Source) return false;
    return $this->source->client()->validRepository(
    $value, $this->branch
    );
    }
    public function message()
    {
    return 'The given repository is invalid.';
    }
    customlaravel.com by @stauffermatt

    View Slide

  79. Macros
    // In a service provider
    Response::macro('jsend', function ($body, $status = 'success') {
    if ($status == 'error') {
    return Response::json(['status' => $status, 'message' => $body]);
    }
    return Response::json(['status' => $status, 'data' => $body]);
    });
    // In a route
    return response()->jsend(Order::all());
    customlaravel.com by @stauffermatt

    View Slide

  80. Blade directives: traditional style
    // In a service provider
    Blade::directive('public', function () {
    return "isPublic()): ?>";
    });
    Blade::directive('endpublic', function () {
    return "";
    });
    // Use:
    @public
    Only show on the public web site
    @endpublic
    customlaravel.com by @stauffermatt

    View Slide

  81. Blade directives: using Blade::if in 5.5
    // In a service provider
    Blade::if('env', function ($env) {
    return app()->environment($env);
    });
    @env('production')

    @endenv
    customlaravel.com by @stauffermatt

    View Slide

  82. View Composers
    // In a service provider
    // Class-based composer
    View::composer(
    'minigraph', 'App\Http\ViewComposers\GraphsComposer'
    );
    // Closure-based composer
    View::composer('minigraph', function ($view) {
    return app('reports')->graph()->mini();
    });
    customlaravel.com by @stauffermatt

    View Slide

  83. Custom route model bindings
    // In a service provider
    Route::bind('user', function ($value) {
    return App\User::public()
    ->where('name', $value)
    ->first();
    });
    // In your binding
    Route::get('profile/{user}', function (App\User $user) {
    //
    });
    customlaravel.com by @stauffermatt

    View Slide

  84. Form requests
    // Traditional request injection
    public function index(Request $request) {}
    // Form Request Validation
    public function store(StoreCommentRequest $request) {}
    // Form Request
    class StoreCommentRequest extends FormRequest {
    public function authorize() { return (bool)rand(0,1); }
    public function rules () { return ['validation rules here']; }
    }
    customlaravel.com by @stauffermatt

    View Slide

  85. Form requests (cont)
    class StoreCommentRequest extends FormRequest
    {
    public function sanitized()
    {
    return array_merge($this->all(), [
    'body' => $this->sanitizeBody($this->input('body'))
    ]);
    }
    protected function sanitizeBody($body)
    {
    // Do stuff
    return $body;
    }
    }
    customlaravel.com by @stauffermatt

    View Slide

  86. GIF BREAK
    customlaravel.com by @stauffermatt

    View Slide

  87. customlaravel.com by @stauffermatt

    View Slide

  88. Custom
    Laravel
    301
    customlaravel.com by @stauffermatt

    View Slide

  89. Arbitrary route bindings
    // In a service provider
    Route::bind('topic', function ($value) {
    return array_get([
    'automotive' => 'I love cars!',
    'pets' => 'I love pets!',
    'fashion' => 'I love clothes!'
    ], $value, 'undefined');
    });
    // In your routes file
    Route::get('list/{topic}', function ($topic) {
    //
    });
    customlaravel.com by @stauffermatt

    View Slide

  90. Arbitrary route bindings (cont)
    // In a service provider
    Route::bind('repository', function ($value) {
    return app('github')->findRepository($value);
    });
    // In your routes file
    Route::get('repositories/{repository}', function ($repo) {
    //
    });
    customlaravel.com by @stauffermatt

    View Slide

  91. Custom request objects
    class MyMagicalRequest extends Request
    {
    // Add methods, properties, etc.
    }
    // Type-hint in a route
    public function index(MyMagicalRequest $request) {}
    // Or even re-bind globally in public/index.php
    $response = $kernel->handle(
    // $request = Illuminate\Http\Request::capture()
    $request = App\MyMagicalRequest::capture()
    );
    // (but, remember, macros!)
    customlaravel.com by @stauffermatt

    View Slide

  92. Custom response objects
    class MyMagicalResponse extends Response
    {
    // Add methods, properties, etc.
    }
    // Return from a route
    public function index()
    {
    return MyMagicalResponse::forUser(User::find(1337));
    }
    // Or macro it
    Response::macro('magical', function ($user) {
    return MyMagicalResponse::forUser($user);
    });
    return response()->magical($user);
    customlaravel.com by @stauffermatt

    View Slide

  93. Custom response objects: Responsable in 5.5
    class Thing implements Responsable
    {
    public function toResponse()
    {
    return 'HEY THIS IS A USER RESPONSE FOR THING NUMBER ' . $this->id;
    }
    }
    customlaravel.com by @stauffermatt

    View Slide

  94. Custom Eloquent collections
    class ItemCollection extends Collection
    {
    public function price()
    {
    return $this->sum('amount') + $this->sumTax();
    }
    public function sumTax()
    {
    return $this->reduce(function ($carry, $item) {
    return $carry + $item->tax();
    }, 0);
    }
    }
    class Item
    {
    public function newCollection(array $models = [])
    {
    return new ItemCollection($models);
    }
    customlaravel.com by @stauffermatt

    View Slide

  95. Hijacking index.php
    if ($response->getStatusCode() != 404) {
    $response->send();
    $kernel->terminate($request, $response);
    exit;
    }
    // CodeIgniter or whatever else
    customlaravel.com by @stauffermatt

    View Slide

  96. GIF BREAK
    customlaravel.com by @stauffermatt

    View Slide

  97. customlaravel.com by @stauffermatt

    View Slide

  98. Independent Study
    (other tools)
    customlaravel.com by @stauffermatt

    View Slide

  99. Custom Homestead config
    composer require laravel/homestead --dev
    php vendor/bin/homestead make
    # generates Homestead.yaml:
    ip: 192.168.10.10
    memory: 2048
    cpus: 1
    provider: virtualbox
    authorize: ~/.ssh/id_rsa.pub
    keys:
    - ~/.ssh/id_rsa
    folders:
    -
    map: /Users/mattstauffer/Sites/live-from-new-york-its-me
    to: /home/vagrant/Code/live-from-new-york-its-me
    sites:
    -
    map: live-from-new-york-its-me
    to: /home/vagrant/Code/live-from-new-york-its-me/public
    databases:
    - homestead
    name: live-from-new-york-its-me
    hostname: live-from-new-york-its-me
    customlaravel.com by @stauffermatt

    View Slide

  100. Lambo pro tip
    ▸ Lambo with config and after scripts ❤"
    customlaravel.com by @stauffermatt

    View Slide

  101. Commonly installed packages
    https://mattstauffer.co/blog/what-packages-do-you-install-on-every-
    laravel-application-you-create
    customlaravel.com by @stauffermatt

    View Slide

  102. Recap
    1. With great power... customize responsibly!
    2. Prioritize, but don't enshrine, discoverability
    3. 101: Modify the intended-to-be-modified; make your own, too
    4. 201: Use the container!
    5. 301: Override/custom all the classes!
    6. Custom Homestead, Lambo
    customlaravel.com by @stauffermatt

    View Slide

  103. customlaravel.com
    (for tips and slides)
    customlaravel.com by @stauffermatt

    View Slide