(Talk notes)
Intro
My old talks
- PeersConf 2014 Why Modern PHP is amazing and how you can use it today (Slides)
- Laracon Eu 2014 Sharing Laravel (Slides | Video)
- Laracon US 2015 Leveraging Laravel (Slides)
- Laracon Eu 2015 Empathy Gives You Superpowers (Slides | Video)
- Laracon Online 2017 Mastering the Illuminate Container (Slides)
Other links
- Convention over configuration (blog post)
- TwentyPercent on Macros | TwentyPercent.fm on keeping it simple (podcast episodes)
- Laravel: Up and Running (book)
Custom Laravel 101
Config
- Make custom config file keys
<?php // config/services.php return [ 'myfavoriteservice' => [ 'key' => env('abcdefg') ] ]; // use elsewhere: return config('services.myfavoriteservice.key');
- Make custom config files
<?php // config/chat.php return [ 'room' => [ 'endpoint' => env('CHAT_ROOM_ENDPOINT') ] ]; // use elsewhere: return config('chat.room.endpoint');
- Make up your own
.env
variables and import into your config (DotEnv)
# .env APP_NAME="My Awesome App" # ... CHAT_ROOM_ENDPOINT=http://chat.service/endpoints/12345
- Don't reference
env()
inline! (breaks configuration caching)
// replace this: app()->bind(Intercom::class, function () { return new IntercomService( env('INTERCOM_API_KEY') ); }); // with this: app()->bind(Intercom::class, function () { return new IntercomService( config('services.intercom.key') ); }); // (and this, in config/services.php) return [ 'intercom' => [ 'key' => env('INTERCOM_API_KEY') ] ];
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()
- Override intended "hook" methods; e.g.
LoginController@authenticated()
,LoginController@username()
- Some customizable methods and properties are placed in your default controller (e.g.
RegisterController@create
); but you can always adjust the others by just extending
- Customize views; e.g.
Default providers
AppServiceProvider
😍 a.k.a. core hacking for newbs
// Just waiting for you to try it out! class AppServiceProvider extends ServiceProvider { public function boot() { // } public function register() { // } }
AuthServiceProvider@boot
andEventServiceProvider@boot
// AuthServiceProvider public function boot() { $this->registerPolicies(); // } // EventServiceProvider public function boot() { parent::boot(); // }
RouteServiceProvider@boot
,RouteServiceProvider@map
// RouteServiceProvider public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); // Just waiting for you to add your own... }
Default middleware
EncryptCookies@except
,TrimStrings@except
a,VerifyCsrfToken@except
protected $except = [ // Exclude cookies from encryption // Exclude routes from CSRF protection // Exclude strings from trimming ];
RedirectIfAuthenticated: redirect('home')
public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->check()) { // Change this link to change where // auth'ed users are sent by default return redirect('/home'); } return $next($request); }
Custom middleware groups
api
andweb
out of the box- Add more in
App\Http\Kernel@$middlewareGroups
- Also globally add any in
$middleware
or make route-optional in$routeMiddleware
class Kernel extends HttpKernel
{
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
// ...
],
'api' => [
'throttle:60,1',
'bindings',
],
'admin' [
'web',
'some-other-awesome-middleware',
]
];
}
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
// In your middleware
class KeepOutBadPeople
{
public function handle($request, Closure $next)
{
if ($this->isBad($request->person)) {
abort(404);
}
return $next($request);
}
}
// In App/Http/Kernel.php
protected $middleware = [
// Put here to apply globally
];
protected $middlewareGroups = [
'web' => [
// Put here to apply to just certain groups
],
];
protected $routeMiddleware = [
// Put here to allow it to be conditionally applied
];
Exceptions
- Custom exceptions (e.g.
TryingToHackUsWhatTheHeckException
) - Custom global exception handling rules
- 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
Includes
- Extract chunks of code to includes
- See:
routes.php
,console.php
- What about, maybe,
schedule.php
?
// App\Console\Kernel // From this: protected function schedule(Schedule $schedule) { // $schedule->command('inspire') // ->hourly(); } // To this: protected function schedule(Schedule $schedule) { require base_path('schedule.php'); }
Frontend
php artisan make:auth
- 5.5 Frontend presets
- Default files are just that: defaults!
GIF BREAK
Custom Laravel 201
Tests
- Add assertions to the base class
abstract class TestCase extends BaseTestCase { use CreatesApplication; public function assertUserIsAdmin($user) { // Functionality here } }
- Customize/re-bind/seed in setUp
class TestSomethingHappens extends TestCase { public function setUp() { parent::setUp(); app()->bind(SomeClass::class, function () { // stuff }); } }
- Custom variables in
phpunit.xml
<php> <env name="APP_ENV" value="testing"/> <env name="CACHE_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/> <env name="QUEUE_DRIVER" value="sync"/> <env name="INTERCOM_API_KEY" value="12345"/> </php>
- Test-specific
.env.testing
- Custom database migration type
RefreshDatabase
—coming in 5.5
Helpers
- Bring back
helpers.php
! - What works for your domain?
sync()
? etc. - In your
composer.json
:
{ "autoload": { "files": ["helpers.php"] } }
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)
- 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)
- Step 1. Bind your own class
- An example: GitHub issue about customizing storage paath
class MyContainer extends Container { public function storagePath() { return $this->basePath . '/storageOMGyay'; } }
(solution explained in my blog post) - Another example: Bugsnag
Example from Bugsnag docs:
$this->app->alias('bugsnag.logger', \Illuminate\Contracts\Logging\Log::class); $this->app->alias('bugsnag.logger', \Psr\Log\LoggerInterface::class);
Previously in their service provider:
$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']]); });
- Contracts: interfaces to all major Laravel components; live in
Illuminate\Contracts\*
<?php namespace Illuminate\Contracts\Logging; interface Log { public function alert($message, array $context = []); public function critical($message, array $context = []); // ...
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
= 💥
Custom Facades
- 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
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::nonStaticClassOnFitbitClass();
Swapping Facades and bindings in tests
- Swappable façades e.g.:
Cache::shouldReceive('get') ->once() ->with('key') ->andReturn('value')
- Shout out to MailThief ❤️
- Swap 'em yourself!
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.", ]
-
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.'; } }
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());
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
-
Conditionals in 5.5+ using Blade::if
// In a service provider Blade::if('env', function ($env) { return app()->environment($env); }); @env('production') <script src="analytics.js"></script> @endenv
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();
});
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) {
//
});
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']; }
}
... but you can also create custom methods!
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;
}
}
GIF BREAK!
Custom Laravel 301
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) {
//
});
// In a service provider
Route::bind('repository', function ($value) {
return app('github')->findRepository($value);
});
// In your routes file
Route::get('repositories/{repository}', function ($repo) {
//
});
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!)
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);
Custom response objects: with Responsable in 5.5
class Thing implements Responsable
{
public function toResponse()
{
return 'USER RESPONSE FOR THING NUMBER ' . $this->id;
}
}
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);
}
}
Hijacking index.php
if ($response->getStatusCode() != 404) {
$response->send();
$kernel->terminate($request, $response);
exit;
}
// CodeIgniter or whatever else
GIF BREAK!
Independent study
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
Lambo pro tip
Lambo with config and after scripts ❤️🏎️https://mattstauffer.co/blog/lambo-config-and-after-scripts-for-even-better-laravel-app-creation