Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. Why Modern PHP Is Amazing And How You Can Use

    It Today PeersConf 2014 customlaravel.com by @stauffermatt
  2. 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
  3. “Taking it too far is a rite of passage” -

    Jeffrey Way customlaravel.com by @stauffermatt
  4. One of the hardest things to discover in any new

    app is: "where/how is this happening?" customlaravel.com by @stauffermatt
  5. Write your apps so anyone who has read the Laravel

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

    docs can jump into them easily customlaravel.com by @stauffermatt
  7. 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
  8. 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
  9. How to customize Laravel, and how to do it responsibly

    customlaravel.com by @stauffermatt
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. Default providers ▸ AppServiceProvider ! a.k.a. core hacking for newbs

    class AppServiceProvider extends ServiceProvider { public function boot() { // } public function register() { // } customlaravel.com by @stauffermatt
  20. 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
  21. 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
  22. 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
  23. Default middleware ▸ EncryptCookies@except, TrimStrings@except, VerifyCsrfTokens@except ▸ RedirectIfAuthenticated: redirect('/home') class

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

    box ▸ Add more in App\Http\Kernel@$middlewareGroups customlaravel.com by @stauffermatt
  25. 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
  26. 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
  27. 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
  28. 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
  29. Includes ▸ Extract chunks of code to includes ▸ See:

    routes/web.php from before; also console.php ▸ What about schedule.php? customlaravel.com by @stauffermatt
  30. 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
  31. Frontend ▸ php artisan make:auth ▸ 5.5 Frontend presets ▸

    Default files are just that; defaults! customlaravel.com by @stauffermatt
  32. 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
  33. Helpers ▸ Bring back helpers.php! ▸ What works for your

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

    { return $this->basePath . '/storageOMGyay'; } } customlaravel.com by @stauffermatt
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. Lambo pro tip ▸ Lambo with config and after scripts

    ❤" customlaravel.com by @stauffermatt
  60. 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