Laravel Behind the Scenes: Supports

Usually, your project will contain some code that needs to be reused throughout the codebase. So does the Laravel framework itself. And it's a good practice to extract that code into reusable classes. Think of arrays and "dot" notation. Laravel allows you to get certain value from a nested array using "dot" notation in various places: configs, request params, translations, etc. So Laravel provides us with an Arr class that holds common logic for arrays, such as data retrieval using "dot" notation, etc. Enter: support components. Support components are various classes or traits whose logic is being reused throughout the framework. There are various support classes, and we'll talk about few: Fluent class, Manager, something we call macros, string and array classes, Collection and helpers. We'll go over each of those and explain what they're purpose is, where they're used and we'll show a use-case for each of those. All of those are placed within Illuminate\Support namespace. Note that all of those classes/traits can be used in your app and is not limited to the framework itself.

Macros

If you're familiar with Swift's Extensions, the Macros are essentially the same thing. Macros are a way to hook into the framework and extend with some additional functionality without explicitly inheriting the class. A lot of Illuminate components use the Macroable trait which allows the class to be extended. If we dig into the code and take a look at that trait, you can see a static property $macros. The effect of that static property is global access. If you assign a macro to Collection class, all Collection instances will be able to access the same macro.

Usage

Registering the macro

Binding the macro into the class is done by calling the macro($name, $macro) static method. Method literally binds the $macro (Closure that performs your functionality) into $macros array by the key $name. Using this, you can extend, for example, the Collection class like it's described in the docs. Note that this is usually done in a service provider.

Accessing the macro

We can also see the __call and __callStatic magic methods. Those methods are accessed whenever there is a call on inaccessible method on a class. We want to override that to see if the macro exists (static::hasMacro($macro)) and if it does, resolve functionality from the array and invoke it. A functionality is usually a Closure, but can be anything callable. Note that you can macro in another object if you'd like by calling the mixin method instead. In case you haven't noticed, calling $this inside a Closure for a macro will access the current instance of the class itself. How can this be possible? How can I use $this to access an object's properties and methods inside a Closure? Well, Closures in PHP have this method called bindTo and Laravel takes advantage of that. It binds the current object being macro'ed to the Closure, so $this references the class itself, not the Closure.

Conflict

If the class that used the Macroable trait has a __call method, there is going to be a conflict. Both methods can't be invoked, so when we use the trait, we should rename the Macroable's __call method into something else, but then make sure to run that method inside objects __call method. The example can be seen in the Cache\Repository class. We import the trait, but we also rename its __call into macroCall. Then, in the Repository's __call, we make sure to call the macroCall if the class has a macro, otherwise default to local method call — there is no conflict.

Note: don't over-use it. If you have a lot of macros, or some really complex logic, it's much better to extend the class and do your logic than to fill up your service providers with macros.

One macro I often like to use is checked for the Request class to determine whether or not a checkbox in a form is checked or not:

Request::macro('checked', function ($attribute) {
    return in_array($this->input($attribute, false), [true, 1, '1', 'on']);
});

Fluent

I just recently discovered this and it blew me away. Okay, so what is it? You know when you store some array of items in your app and have to build all this boilerplate. Let's say you have website settings that you store as a key-value pairs in your database. You probably know that you had to build a class that has a getter and a setter method, where getter probably needs to return some default value in case the requested item doesn't exist and your app shouldn't blow up by trying to access an array value that doesn't exist but rather return null, for example. Then, you need to probably implement various interfaces since you want to access those items with array notation, such as $settings['timezone'], or in object notation, $settings->timezone. Then, you need to specify how it renders when displaying as JSON, etc. Yeah, this is some common boilerplate you would need to implement. Not anymore. Illuminate\Support\Fluent provides that boilerplate and the code can be seen here. If you have something like Settings class, you can inherit the Fluent class and add some additional methods such as save, or persist or something similar that updates the database, and override the constructor to load the items from the database and store into the object and very quickly you built the primitive key-value Settings store.

Manager

A manager is a class that dictates which driver is currently being used for certain functionality of your app. It's best to show in an example. Think of caching: there are multiple cache stores you can use, such as memcached, redis, file, database, ... Cache manager says to Laravel: okay, you'll use the redis driver now, but if user says to use file driver, use that one. There are a lot of Illuminate components that work on drivers: sessions, authentication, queues, etc. And you can use the same pattern in your app. Let me provide you with an example, of the top of my head: let's say you are ranking some user content. You would create a class called Ranker that extends Illuminate\Support\Manager. You have multiple ranking algorithms implemented in your app, and each of those algorithm implementations are so-called drivers and you can swap them as you wish. All of them have the same methods dictated by some RankingAlgorithm interface. One day you want to use one algorithm, then if this one stops working for you for some reason, swap for another, etc. There are probably better examples, but oh well. There's a Illuminate\Support\Manager class that allows for this driver-based implementation. As you can see, if you're extending this class, you need to implement getDefaultDriver method to determine which driver is going to be used by default. In Cache case, this is read from the config file to determine which cache store the app will use. Then, once you call a method on your Ranker class, like rankContent, if the default driver is AlgorithmA, the Ranker will delegate to the rankContent method from the currently active driver. Now when you want to change for another driver, change the getDefaultDriver method to use AlgorithmB driver and you're done. Ranker class does not care which driver is being used and nothing else needs to be changed.

Registering drivers

Before the manager calls methods on drivers, it needs to know about those drivers. We need to register them. When calling a method on Ranker class, it calls that method on whatever is returned from driver(). Now we noticed that driver() method should return an instance of current driver being used. The driver() method can be seen here and it's easy to see it's calling the createDriver method. The createDriver method performs logic that creates a new instance of the driver. It uses PHPs dynamic method name magic and requires this naming convention:

$method = 'create'.Str::studly($driver).'Driver';

if (method_exists($this, $method)) {
    return $this->$method();
}

So, if you have drivers called AlgorithmA, AlgorithmB, etc. the methods that will be called and create them are: createAlgorithmADriver, etc. This method should return a new instance of the driver. Once it creates a new instance of drivers, the Ranker (Manager) will just call methods on that driver class.

Extending with custom drivers

Manager class provides you with extend method that works similarly as Macros, which allows a developer to register custom driver on the fly. Before a driver is created on the class, it's going to check if the custom driver exists and invoke it if it does.

Arr & Str

Laravel provides Arr and Str classes with a handful of methods to work on strings and arrays. Note that Str class is a big list of static methods, because strings are immutable, so it receives a string and returns a new modified version leaving the input one intact. Arr class is a helper class that performs operations on arrays, making it much easier for end user. Random element, last element, first element, nth element, cross join two arrays, convert to "dot" notation — all really easy with this class. Don't be scared to use them.

Collection

I believe this requires no introduction. But note that there are 2 Collection classes. Illuminate\Support\Collection and Illuminate\Database\Eloquent\Collection. The latter is inherited from the former and returned when you retrieve items from the database. It contains some additional functionality, such as load (eager loading models' relationships), loadMissing (same thing but loading only non-loaded relationships), etc. It also holds the find method (note that this is not the same thing as User::find($id)). First one operates on a Collection instance (meaning all your data has already been retrieved from the database and second one executes the query that only selects the item with specific ID. By browsing through Eloquent Collection methods, you can find some cool stuff there, such as modelKeys() method that returns (plucks) only primary key (usually IDs) of the models, fresh() method that reloads the query. You'll find yourself using this mostly in your unit tests when a model changes but you need to get a fresh instance to see if the data has persisted in the database. There's also a lot of very useful methods such as makeHidden and makeVisible which hides certain attributes from the model for current instance of Collection (not for every instance). You can find more at the documentation.

Custom collection

If you didn't know, you can create a custom Collection class by extending the base one. This is useful if you need your models to be an instance of your own collection for some hacking. Jeffrey Way used an example of custom collections for threaded comments, there he would have a root method and nested method on his own collection. Then, if you need your models (e.g. User) to return instance of your own collection (e.g. UserCollection), override Laravel's newCollection(array $models = []) method in User model by simply returning new UserCollection($models).

Helpers

There's a ton of helper functions that can be very useful from time to time. And many you probably didn't know exist. For example: have you heard about blank($value) helper function? Or value($value)? Yeah, there's a lot of great helper functions, and as they are very well documented, I won't go too deep into it, but make sure to check them out.

This will be it for today, I hope you learned something new about the framework and can find a good use for these classes and functions I showed you!

👋 I'm available for software development contract work and consulting. You can find me at [email protected]