Custom Laravel

Custom Laravel

Given at Laracon 2017.

60187fe0ab07ea5a46572a3ab05f61dd?s=128

Matt Stauffer

July 26, 2017
Tweet

Transcript

  1. customlaravel.com by @stauffermatt

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

  3. customlaravel.com by @stauffermatt

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

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

  6. Why Modern PHP Is Amazing And How You Can Use

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

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

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

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

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

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

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

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

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

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

  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
  18. What you choose to customize matters. customlaravel.com by @stauffermatt

  19. “Taking it too far is a rite of passage” -

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

  21. One of the hardest things to discover in any new

    app is: "where/how is this happening?" customlaravel.com by @stauffermatt
  22. Prioritize Discoverability customlaravel.com by @stauffermatt

  23. Write your apps so anyone who has read the Laravel

    docs can jump into them easily customlaravel.com by @stauffermatt
  24. LARAVELUPANDRUNNING.COM <blink>buy</blink> customlaravel.com by @stauffermatt

  25. Write your apps so anyone who has read the Laravel

    docs can jump into them easily customlaravel.com by @stauffermatt
  26. The opposite of discoverability is... ! CUSTOMIZATION DEBT ! customlaravel.com

    by @stauffermatt
  27. customlaravel.com by @stauffermatt

  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
  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
  30. customlaravel.com by @stauffermatt

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

  32. How to customize Laravel, and how to do it responsibly

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

  34. customlaravel.com by @stauffermatt

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

  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
  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
  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
  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
  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
  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
  42. customlaravel.com by @stauffermatt

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

  44. Custom Laravel 101 customlaravel.com by @stauffermatt

  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
  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
  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
  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
  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
  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
  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
  52. Default middleware ▸ EncryptCookies@except, TrimStrings@except, VerifyCsrfTokens@except ▸ RedirectIfAuthenticated: redirect('/home') class

    EncryptCookies or TrimStrings or VerifyCsrfTokens { protected $except = [ // ]; customlaravel.com by @stauffermatt
  53. Custom middleware groups ▸ api and web out of the

    box ▸ Add more in App\Http\Kernel@$middlewareGroups customlaravel.com by @stauffermatt
  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
  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
  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
  57. Exceptions ▸ Custom exceptions (e.g. TryingToHackUsWhatTheHeckException) ▸ Custom global exception

    handling rules customlaravel.com by @stauffermatt
  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
  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
  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
  61. Frontend ▸ php artisan make:auth ▸ 5.5 Frontend presets ▸

    Default files are just that; defaults! customlaravel.com by @stauffermatt
  62. GIF BREAK customlaravel.com by @stauffermatt

  63. customlaravel.com by @stauffermatt

  64. Custom Laravel 201 customlaravel.com by @stauffermatt

  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
  66. Helpers ▸ Bring back helpers.php! ▸ What works for your

    domain? sync(), etc. { "autoload": { "files": ["helpers.php"] } } customlaravel.com by @stauffermatt
  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
  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
  69. An example class MyContainer extends Container { public function storagePath()

    { return $this->basePath . '/storageOMGyay'; } } customlaravel.com by @stauffermatt
  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
  71. Contracts Interfaces to all major Laravel components Illuminate\Contracts\* <?php namespace

    Illuminate\Contracts\Logging; interface Log { public function alert($message, array $context = []); public function critical($message, array $context = []); // ... customlaravel.com by @stauffermatt
  72. Breathe customlaravel.com by @stauffermatt

  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
  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
  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
  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
  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
  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
  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
  80. Blade directives: traditional style // In a service provider Blade::directive('public',

    function () { return "<?php if (app('context')->isPublic()): ?>"; }); Blade::directive('endpublic', function () { return "<?php endif; ?>"; }); // Use: @public Only show on the public web site @endpublic customlaravel.com by @stauffermatt
  81. Blade directives: using Blade::if in 5.5 // In a service

    provider Blade::if('env', function ($env) { return app()->environment($env); }); @env('production') <script src="analytics.js"></script> @endenv customlaravel.com by @stauffermatt
  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
  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
  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
  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
  86. GIF BREAK customlaravel.com by @stauffermatt

  87. customlaravel.com by @stauffermatt

  88. Custom Laravel 301 customlaravel.com by @stauffermatt

  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
  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
  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
  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
  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
  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
  95. Hijacking index.php if ($response->getStatusCode() != 404) { $response->send(); $kernel->terminate($request, $response);

    exit; } // CodeIgniter or whatever else customlaravel.com by @stauffermatt
  96. GIF BREAK customlaravel.com by @stauffermatt

  97. customlaravel.com by @stauffermatt

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

  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
  100. Lambo pro tip ▸ Lambo with config and after scripts

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

  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
  103. customlaravel.com (for tips and slides) customlaravel.com by @stauffermatt