Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel deferred service provider `provides` not being called

I have the following definition:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\SomeClass;

class SomeProvider extends ServiceProvider
{
    protected $defer = true;

    public function register()
    {
        $this->app->bind(SomeClass::class, function ($app)
        {
            return new SomeClass();
        });
    }

    public function provides()
    {
        die("This never gets called");
        return [SomeClass::class];
    }
}

And it returns an instance of SomeClass as expected, except that according to the documentation, if $defer is true then the provides() method should be called. No matter what I set $defer to, and no matter if I actually ask for an instance of SomeClass or not, provides() is never called.

The way I'm asking for an instance of the class is as follows:

App::make('SomeClass');
like image 537
Julian Avatar asked Sep 23 '16 14:09

Julian


2 Answers

Short answer:
Your compiled manifest file is already compiled by framework.

On the first time when Laravel build the application (and resolves all of services providers in IoC container) it writes to cached file named services.php (that is, the manifest file, placed in: bootstrap/cache/services.php).
So, if you clear the compiled via php artisan clear-compiled command it should force framework to rebuild the manifest file and you could to note that provides method is called. On the next calls/requests provides method is not called anymore.

The sequence of framework boot is nearly like this:

//public/index.php
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
    \Illuminate\Foundation\Http\Kernel::__construct();
    \Illuminate\Foundation\Http\Kernel::handle();
        \Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter();
            \Illuminate\Foundation\Http\Kernel::bootstrap();
                \Illuminate\Foundation\Application::bootstrapWith();
                    # where $bootstrapper is a item from \Illuminate\Foundation\Http\Kernel::$bootstrappers 
                    # and $this is instance of \Illuminate\Foundation\Application
                    \Illuminate\Foundation\Application::make($bootstrapper)->bootstrap($this);

One of bootstrappers is Illuminate\Foundation\Bootstrap\RegisterProviders which invokes \Illuminate\Foundation\Application::registerConfiguredProviders() and then invokes \Illuminate\Foundation\ProviderRepository::__construct() and finally:

\Illuminate\Foundation\ProviderRepository::load()

When \Illuminate\Foundation\ProviderRepository::load() is called all services providers is registered and \Illuminate\Support\ServiceProvider::provides() are called also well.

And here is the snippet you should know (from \Illuminate\Foundation\ProviderRepository::load):

    /**
     * Register the application service providers.
     *
     * @param  array  $providers
     * @return void
     */
    public function load(array $providers)
    {
        $manifest = $this->loadManifest();

        // First we will load the service manifest, which contains information on all
        // service providers registered with the application and which services it
        // provides. This is used to know which services are "deferred" loaders.
        if ($this->shouldRecompile($manifest, $providers)) {
            $manifest = $this->compileManifest($providers);
        }

        // Next, we will register events to load the providers for each of the events
        // that it has requested. This allows the service provider to defer itself
        // while still getting automatically loaded when a certain event occurs.
        foreach ($manifest['when'] as $provider => $events) {
            $this->registerLoadEvents($provider, $events);
        }

        // We will go ahead and register all of the eagerly loaded providers with the
        // application so their services can be registered with the application as
        // a provided service. Then we will set the deferred service list on it.
        foreach ($manifest['eager'] as $provider) {
            $this->app->register($this->createProvider($provider));
        }

        $this->app->addDeferredServices($manifest['deferred']);
    }

\Illuminate\Foundation\ProviderRepository::compileManifest() is the place where your provides() method is performed.

like image 82
felipsmartins Avatar answered Nov 15 '22 16:11

felipsmartins


After doing my own testing, it would seem this is an issue (maybe "issue" is a strong word in this case).

If you register something in a service provider which has the name of a class, Laravel is just going to return that class and disregard whatever is in the service provider. I started doing the same thing as you did....

protected $defer = true;

public function register()
{
    $this->app->bind(SomeClass::class, function ($app)
    {
        return new SomeClass();
    });
}

public function provides()
{
    dd('testerino');
}

$test = \App::make('App\SomeClass');

And $test is an instance of SomeClass. However if I make the following change...

$this->app->bind('test', function ($app) { ... }

And use

$test = \App::make('test');

Then it hits the deffered function and outputs the text testerino.

I think the issue here is that Laravel knows you are just trying to grab a class. In this instance, there is no reason to register what you are trying to register with the container, you aren't doing anything except telling Laravel to make an instance of App\SomeClass when it should make an instance of App\SomeClass.

However, if you tell Laravel you want an instance of App\SomeClass when you call App::make('test'), then it actually needs to bind that class to test so then I think it starts to pay attention to the service provider.

like image 27
user1669496 Avatar answered Nov 15 '22 16:11

user1669496