Laravel Behind the Scenes: Interfaces

Digging through the Laravel's source code, I came across this little piece of code that you've probably seen before:

Interfaces

That's a lot of interfaces and I thought this may be a good topic since some of these interfaces are implemented in various classes throughout the entire framework and for a good reason too.

Why?

You probably noticed that Laravel provides flexibility and customization everywhere. For example, you can create a JSON response out of your models, encode strings and arrays into the JSON, render emails in your browser. This is the point of these interfaces. Laravel can't know which classes you're using and what exactly is happening in your logic. That's why it's using interfaces to identify a class type of the object. Whether that object is the response from the controller, a view data or anything else, Laravel can dictate what it does. Example: let's say some URI is hit you return a simple object. How does Laravel know that this class should encode the object and output the JSON? How does Laravel know when you return a view, that it should render? The thing Laravel knows is that some object implemented for example Jsonable interface and knows that this object has the toJson method. Same thing for rendering, creating a response, etc.

Search

Interfaces

ArrayAccess

We'll kick things of with this guy. Note that this is native PHP interface and has nothing to do with Laravel, but Laravel uses this a lot. As the documentation states, this is:

Interface to provide accessing objects as arrays.

There's not much to talk about, if the $config variable holds an object, like here, you can extend it's functionality with array-like access: $config['key'] by implementing the ArrayAccess methods, like Config/Repository.php has. Laravel uses this in a lot of places: Eloquent, Collections, Container, Config repository, Cache repository, ...

Example:

class User implements ArrayAccess
{
    protected $firstName = 'John';
    protected $lastName = 'Doe';

    public function offsetExists($key)
    {
        return property_exists($this, $key);
    }

    public function offsetGet($key)
    {
        return $this->{$key};
    }

    public function offsetSet($key, $value)
    {
        $this->{$key} = $value;
    }

    public function offsetUnset($key)
    {
        $this->{$key} = null;
    }
}

Route::get('/', function () {
    $user = new User;

    dd($user['firstName']);
});

JsonSerializable

This is another interface that PHP provides and as the documentation says:

Objects implementing JsonSerializable can customize their JSON representation when encoded with json_encode().

Essentially you can modify how your object looks like when it's being encoded to JSON. If we take a simple User class for example:

class User
{
    public $firstName = 'John';
    public $lastName = 'Doe';
}

Once you try to encode this class with json_encode(new User) and dump the contents you'll get this: "{"firstName":"John","lastName":"Doe"}". If you want to modify how the JSON representation looks like, implement the JsonSerializable interface located in the global namespace and implement the jsonSerialize method, for example:

class User implements JsonSerializable
{
    public $firstName = 'John';
    public $lastName = 'Doe';

    public function jsonSerialize()
    {
        return ['name' => sprintf('%s %s', $this->firstName, $this->lastName)];
    }
}

If we now dump the encoded object, we see exactly what we expected: "{"name":"John Doe"}". This is also used throughout the framework. For example, Eloquent model serializes the array representation of the model. This is used with Jsonable interface to provide flexibility when encoding objects to JSON, for Responses, etc.

Jsonable

This is Laravel's interface and it serves a very similiar purpose as JsonSerializable, however with one small modification. Let's say we want to hook into jsonSerialize method and do some checking before encoding to the JSON. This is the purpose of Jsonable interface -- it dictates how the object should be encoded, while JsonSerializable dictates how the encoded object looks like. This interface implements toJson($options = 0) method. Example:

class User implements Jsonable
{
    public $firstName = 'John';
    public $lastName = 'Doe';

    public function toJson($options = 0)
    {
        if ($this->firstName === 'John') {
            throw new Exception('Name must not be John.');
        }

        return json_encode(['name' => sprintf('%s %s', $this->firstName, $this->lastName)], $options);
    }
}

Route::get('/', function () {
    return new User;
});

If the name is John, it throws an exception, however if the name is not John, sure enough it encodes the JSON. Note that you need to encode it to JSON yourself. You can use this combined with JsonSerializable to not repeat yourself:

class User implements Jsonable, JsonSerializable
{
    public $firstName = 'Jane';
    public $lastName = 'Doe';

    public function toJson($options = 0)
    {
        if ($this->firstName === 'John') {
            throw new Exception('Name must not be John.');
        }
        return json_encode($this->jsonSerialize(), $options);
    }

    public function jsonSerialize()
    {
        return ['name' => sprintf('%s %s', $this->firstName, $this->lastName)];
    }
}

Arrayable

This is essentially the same concept as Jsonable, but it dictates how the object looks when represented as an array. And this is where the infamous toArray method comes from. Some of the examples are Collections and view data. Collections operate on a collection of data, but Collection class shouldn't care if your object is of type User, or Car, or Pet, or whatever. As long as your classes implement this interface, we know that this object can be represented as an array. In views, once you pass an object as view data, and that object implements the Arrayable interface, it calls the toArray method on the object as can be seen here.

Example:

class User implements Arrayable
{
    protected $firstName = 'John';
    protected $lastName = 'Doe';

    public function toArray()
    {
        return ['name' => sprintf('%s %s', $this->firstName, $this->lastName)];
    }
}

Route::get('/', function () {
    return view('welcome', new User);
});
// view:
{{ $name }}

Responsable

This interface requires the implementation of toResponse method and I addressed it in my Lifecycle: Response article. It's not so widely used, but it can help you out a lot because you can format the response however you want. Any class is responsable as long as it implements this interface. As you can see here, Laravel checks to see if the response is the instance of Responsable and invokes the toResponse method. Full power of this interface was briefly talked about at Laracon EU 2017, but Taylor mentioned it probably won't make it into the 5.5... and it did not. How you can use this interface is up to you, but combined with API resources, and it's really powerful.

Let me give you a simple example: So the core life cycle is request-response, right? You can structure your controllers in such a way. Where Request is an object, Response is an object and perform some business logic in between. So any component is reusable again.

class Responses\AvatarUploaded implements Responsable
{
    protected $success;

    public function __construct($success)
    {
        $this->success = $success;
    }

    public function toResponse($request)
    {
        if (!$this->success) {
            return new JsonResponse('Something went wrong!', 400);
        }

        return new JsonResponse('Your avatar has been uploaded.');
    }
}

// UploadAvatar performs a validation
Route::post('/avatar', function (Requests\UploadAvatar $request) {
    $avatar = new Services\UserAvatar($request->user());

    // generate thumbnails, remove old avatar, etc...
    $success = $avatar->upload($request->avatar);

    return new Responses\AvatarUploaded($success);
});

Your controllers will look really clean!

Htmlable

This less known interface (that looks like somebody smashes the keyboard) is also not so widely used, but it's really powerful. It only requires the implementation of one method: toHtml. As you may know, Blade tries to escape everything between the curly brackets before rendering in the browser, as can be seen here. However, the escape helper checks if the content between double curly brackets is an instance of Htmlable then invokes the implemented toHtml method.

function e($value, $doubleEncode = true)
{
    if ($value instanceof Htmlable) {
        return $value->toHtml();
    }

    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', $doubleEncode);
}

So in essence, any object can be rendered in Blade with {{ }} as long as it implements this interface. You can use it in conjunction with HtmlString class to get the most out of it. The best example I can think of for Htmlable interface is rendering pagination links with {{ $collection->links() }}. The paginator's links method delegates the render() method which creates a new instance of HtmlString, which is in fact Htmlable!

Renderable

Surprise, surprise -- anything that is renderable should implement this. Not much to talk about, currently only View and Mail objects implement this, but anything else can implement this interface. As you can see here, Laravel checks to see if your object is Renderable and calls the render() method on your object to render the data.

Example:

class User implements Renderable
{
    protected $firstName = 'John';
    protected $lastName = 'Doe';

    public function render()
    {
        return "<h1>{$this->firstName} {$this->lastName}</h1>";
    }
}

Route::get('/', function () {
    return new User;
});

And sure enough, we get the HTML: <h1>John Doe</h1>. There's not much there, but essentially, this is it!

UrlRoutable

This one is a bit tricky to explain. It's a contract that makes any object routable. Meaning what? It's best to see it in action:

class Users
{
    public function find($key, $value)
    {
        $users = [
            new User(1, 'John', 'Doe'),
            new User(2, 'Jane', 'Doe'),
            new User(3, 'Jeff', 'Doe')
        ];

        return collect($users)
            ->where($key, $value)
            ->first();
    }
}

class User implements UrlRoutable, JsonSerializable
{
    public $id;
    public $firstName;
    public $lastName;

    public function __construct($id = null, $firstName = null, $lastName = null)
    {
        $this->id = $id;
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function getRouteKey()
    {
        return $this->{$this->getRouteKeyName()};
    }

    public function getRouteKeyName()
    {
        return 'id';
    }

    public function resolveRouteBinding($value)
    {
        return (new Users)->find($this->getRouteKeyName(), $value);
    }

    public function jsonSerialize()
    {
        return [
            'name' => sprintf('%s %s', $this->firstName, $this->lastName)
        ];
    }
}

Route::get('{user}', function (User $user) {
    return $user;
})->name('home');

Let's say we have a list of users in a class called Users. Then we implemented the UrlRoutable interface in the User class that allows us to typehint User object in routes and use route model binding. When we hit the URI, Laravel goes into the resolveRouteBinding() method and passes the parameter from the URI. getRouteKeyName is a method that indicates on which property should be check for a resource. And finally, if we want to generate a URL for our object without explicitly having to specify the key: route('home', ['id' => 1]), we can leverage what will be passed on to the route parameter with getRouteKey method. In this case, we can see that we're referencing 'id' property on $this.

Example:

$user = (new Users)->find('id', 2);

dd(route('home', $user));

This generates the URI for our route: domain.com/2. Even though I'm not really sure about use-case for this interface, but it's nice to know how it works.

This will be it for some core Interfaces and subscribe for more!

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