Categories


Archives


Recent Posts


Categories


Laravel Service Manager Indirection

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

Consider the Laravel caching facade. Laravel lets you store and retrieve values to/from the cache with the following syntax

Cache::put('key','value');
Cache::get('key');

If you’ve been following along, you know there’s no global class named Cache. Cache is an alias that points to the Facade class Illuminate\Support\Facades\Cache. This Facade class defines a service accessor/identifier: cache

#File: vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php
protected static function getFacadeAccessor() { 
    return 'cache'; 
}

In a default Laravel installation, the service accessor cache points to the class Illuminate/Cache/CacheManager, bound in the register method of the CacheServiceProvider

#File: vendor/laravel/framework/src/Illuminate/Cache/CacheServiceProvider.php
public function register()
{
    $this->app->bindShared('cache', function($app)
    {
        return new CacheManager($app);
    });

    //...
}

You’ll notice Laravel binds cache as a shared service, meaning there’s only ever one instance of the Illuminate\Cache\CacheManager object.

So, as a developer familiar with facades, you know if you wanted to look for the definitions of the put and get methods

Cache::put('key','value');
Cache::get('key');

that you’d want to look at Illuminate\Cache\CacheManager.

However, if we look at this class definition, of the 14 methods defined on the CacheManager class, there’s not a put or get among them.

#File: vendor/laravel/framework/src/Illuminate/Cache/CacheManager.php
class CacheManager extends Manager 
{
    protected function createApcDriver() //...
    protected function createArrayDriver() //...
    protected function createDatabaseDriver() //...
    protected function createFileDriver() //...
    protected function createMemcachedDriver() //...
    protected function createRedisDriver() //...
    protected function createWincacheDriver() //...
    protected function createXcacheDriver() //...
    protected function getDatabaseConnection() //...
    protected function repository(StoreInterface $store) //...
    public function getDefaultDriver() //...
    public function getPrefix() //...
    public function setDefaultDriver($name) //...
    public function setPrefix($name) //...
}

At this point, if you’re an inexperienced developer, or even an experienced developer new to Laravel, you may think you’ve followed the facade indirection chain incorrectly — that the Cache facade actually locates a different service, or maybe something’s extended Laravel to make the service identifier cache point at something else.

While an understandable conclusion, that’s not what’s going on here. The Illuminate\Cache\CacheManager class is the cache service, and the Cache facade does point at the cache service. What’s actually happening is another level of indirection.

Who you Gonna __call

The piece we’re missing is the cache manager’s parent class has a __call method. That is, expanded out, the call

Cache::get(...);

Looks like this

$manager = app()->make('cache');
/* @var Illuminate\Cache\CacheManager */
$manager->get();

When PHP can’t find a get method defined on the Illuminate\Cache\CacheManager class or any of its parents, PHP invokes the __call method, defined in the CacheManger‘s parent Illuminate\Support\Manager class

#File: login/vendor/laravel/framework/src/Illuminate/Support/Manager.php
public function __call($method, $parameters)
{
    return call_user_func_array(array($this->driver(), $method), $parameters);
}

The Manager‘s __call method fetches an object with a call to $this->driver(), and then passes the get call to this object. For the specific case of Cache::get('key'), that looks like this

#File: login/vendor/laravel/framework/src/Illuminate/Support/Manager.php
public function __call('get', $parameters)
{
    $driver = $this->getDriver();
    return call_user_func_array(
        [$driver, 'get'],
        $parameters);
}

While this may seem needlessly confusing, this indirection is what allows Laravel to support multiple cache engines. In other words, it’s why you can store cache values in Memcached, Redis, a database, the filesystem, etc.

Where things start to get interesting, and more confusing, is in the driver method. We’re going to follow the method invokation all the way down — if it gets a little confusing don’t worry, we’ll sum it up at the end for you.

#File: login/vendor/laravel/framework/src/Illuminate/Support/Manager.php
public function driver($driver = null)
{
    $driver = $driver ?: $this->getDefaultDriver();

    // If the given driver has not been created before, we will create the instances
    // here and cache it so we can return it next time very quickly. If there is
    // already a driver created by this name, we'll just return that instance.
    if ( ! isset($this->drivers[$driver]))
    {
        $this->drivers[$driver] = $this->createDriver($driver);
    }

    return $this->drivers[$driver];
}

The driver method

  1. Fetches a driver identifier with getDefaultDriver
  2. Passes this string to the createDriver method, which returns an object
  3. Returns that object (caching a copy in ->drivers[])

So, if we investigate the getDefaultDriver method, we’ll find it’s an abstract method

#File: login/vendor/laravel/framework/src/Illuminate/Support/Manager.php
abstract public function getDefaultDriver();

This means it’s the responsibility of each individual service manager class to return a driver. Let’s take a look at the cache manger’s implementation

#File: vendor/laravel/framework/src/Illuminate/Cache/CacheManager.php
public function getDefaultDriver()
{
    return $this->app['config']['cache.driver'];
}

We can see the cache manager references the Laravel configuration to find the cache driver. In a stock system this is the string file

#File: app/config/cache.php
return array(
    'driver' => 'file',

So, returning to the driver method, we can see the object instantiation call looks like this

#File: login/vendor/laravel/framework/src/Illuminate/Support/Manager.php
public function driver($driver = null)
{
    //...
        $this->drivers['file'] = $this->createDriver('file');
    //...
    return $this->drivers['file'];
}

That is, driver calls the createDriver method. If we take a look at an edited version of the createDriver method

#File: login/vendor/laravel/framework/src/Illuminate/Support/Manager.php
protected function createDriver($driver)
{
    //...
    $method = 'create'.ucfirst($driver).'Driver';
    //...    
        return $this->$method();
    //...

    throw new \InvalidArgumentException("Driver [$driver] not supported.");
}

We see the createDriver method uses the driver name (file) to create a method name

createFileDriver

and then calls that method. Similar to the earlier getDefaultDriver abstract method, it’s the responsibility of the parent class to implement a createFileDriver method. Unlike the previous method, there’s no abstract createFileDriver method.

If we take a look at the definition of of createFileDriver, we’ll see two additional method calls (createFileDriver, repository)

#File: vendor/laravel/framework/src/Illuminate/Cache/CacheManager.php
protected function createFileDriver()
{
    $path = $this->app['config']['cache.path'];

    return $this->repository(new FileStore($this->app['files'], $path));
}

protected function repository(StoreInterface $store)
{
    return new Repository($store);
}

That eventually results in the instantiation of a Illuminate\Support\Manager\Filestore object, and the return of an Illuminate\Support\Manager\Repository object. Getting into the full implementation of the Cache service is beyond the scope of this article, but your main takeaway should be realizing when you call Cache::get, the actual method you’re calling is Illuminate\Support\Manager\Repository::get(...)

#File: vendor/laravel/framework/src/Illuminate/Cache/Repository.php
public function get($key, $default = null)
{
    $value = $this->store->get($key);

    return ! is_null($value) ? $value : value($default);
}

As you can see above, the Repository::get(...) method passes the call onto the store property. The store property contains the instantiated Illuminate\Support\Manager\FileStore.

Indirection All the Way Down

So — that’s a pretty twisty path. Here’s a directed graph that outlines it more tersely

That’s five levels of indirection from a client programmer calling get, to the actual function that does the work of get executing. The Cache and Auth facades also point to services that are implemented with the Illuminate\Support\Manager pattern.

This sort of thing interests me for a lot of reasons. The one I’ll talk about today is that, Laravel, under the covers, is arguably more complicated than systems like Magento, Zend, and Symfony. Magento’s factory pattern

Mage::getModel('catalog/product');

is one or two levels of indirection. You could argue that Magento’s layout system is maybe two or three levels — but nowhere near Laravel’s five. So why is Laravel embraced by so many when a “less complicated” system like Magento receives scorn for its over-complications?

It’s because you don’t need to understand Laravel’s complicated nature to get things done. On the surface level, when most developers call something like

Cache::get(...)
Student::find($id);

they’re not thinking of the underlying software patterns, and Laravel doesn’t force them to. The basics in Laravel are made super simple. For a programmer to get started in Laravel all they need to do is drop some code in app/routes.php and they have a responding page. To get started in Magento, Zend, Symfony, any many other frameworks, you need to undertake a multiple step process that involves editing configuration files and/or naming controller files in specific ways.

Under the hood Laravel’s routing system is just as complicated as any MVC framework’s routing system. It’s just that end-user programmers don’t need to dive into that level of detail to get their job done.

One of Laravel’s core values, whether stated by its maintainers or not, is to put the power of modern framework driven programming in the hands of people who might not otherwise understand modern framework driven programming.

The Tradeoff

While this approach has led to massive success and buzz for Laravel, it does create some problems when the framework doesn’t do what a user expects. For example, it’s very common when debugging that you need to dive deeply into framework code to make sure something is doing what you think it is. Most software bugs come down to a mistaken assumption, and the quickest way to undo that assumption is watch the underlying framework code do its job.

Unfortunately, when you see

Cache::get(...)

you have no idea what the underlying class is without going through the above framework debugging. While the Auth and Session facades follow the same Illuminate\Support\Manager pattern, other stock facades implement one off __call logic, which means anytime you want to dig into core service code you need to stop, break flow, figure out where the final implementation class lives, and then return to work.

While not a mortal sin, or even a venial one, this is something you’ll need to deal with if you’ll be building long term operational systems with Laravel. While one could argues that the Laravel core is solid enough not to require regular debugging, the ecosystem of third party Laravel code contains many popular but less well programmed packages/modules.

This indirection can also lead to confusion with the other Laravel systems that use a static calling syntax (::), but are not considered facades. Next time we’ll explore this further when we look at the static call related features of the Eloquent ORM.

Originally published November 9, 2014
Series Navigation<< Laravel’s MacroableTraitEloquent ORM Static Meta-Programming >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 9th November 2014