Search code examples
laravellaravel-5.3

Why won't this Laravel 5.4 service provider register?


I am trying to do a hello world service provider with the new Laravel 5.4.

I have created the following service provider file:

//File: app/TestProvider/TestServiceProvider.php

namespace App\TestProvider;

use Illuminate\Support\ServiceProvider;

class TestServiceProvider extends ServiceProvider
{
    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register()
    {       
        $this->app->bind('Test', function ($app) {
            return new Test();
        });
    }
}

I have created a simple class under the same namespace:

//File: app/TestProvider/Test.php

namespace App\TestProvider;

class Test 
{
    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function helloWorld()
    {       
        echo "hello world";
    }
}

The problem is, this is not registering. The register method is executing as when I put a breaker before the 'bind' method, it executes:

public function register()
{       
    dd("BREAKER");
    $this->app->bind('Test', function ($app) {
        return new Test();
    });
}

So this outputs "BREAKER" as expected. However if I put the breaker in the closure, nothing happens which suggests for some reason, that 'bind' method isn't being executed??

Any ideas?

EDIT:

Just some further info: I know that the Test class is registered and in the correct namespace as I can do:

dd(new Test());

in the registration method, and it outputs the resource id as expected.


Solution

  • Explanation

    The closure provided only runs when the binding is being resolved. That's why it's a closure, it can be saved in the service container and resolved at any time while the program runs.

    Solution

    To see the resolved binding, create a controller and resolve the class in that controller:

    // File: app/Http/Controllers/TestController.php
    
    namespace App\Http\Controllers;
    
    // This isn't the best way, but it works. See the best way below
    
    class TestController extends Controller {
        public function index()
        {
            return \App::make('Test')->helloWorld();
        }
    }
    

    Of course, don't forget to register the route:

    // File: routes/web.php
    
    Route::get('/', 'TestController@index');
    

    The binding will resolve when you hit the homepage.

    However, as I said, it's not the best way, so here I prepared a better way. Change the way you register the binding:

    // File: app/Providers/TestProvider.php
    
    namespace App\TestProvider;
    
    use Illuminate\Support\ServiceProvider;
    use App\TestProvider\Test;
    
    // Better way
    
    class TestServiceProvider extends ServiceProvider
    {
        /**
         * Register bindings in the container.
         *
         * @return void
         */
        public function register()
        {
            // Note: we bind the exact complete class name!
            $this->app->bind(Test::class, function ($app) {
                return new Test();
            });
        }
    }
    

    After this change the controller so that it looks like this:

    namespace App\Http\Controllers;
    
    use App\TestProvider\Test;
    
    class TestController extends Controller {
        /**
         * @var Test $test
         */
        private $test;
    
        // Let Laravel resolve the dependency on constructing the class
        public function __construct(Test $test)
        {
            $this->test = $test;
        }
    
        public function index()
        {
            return $this->test->helloWorld();
        }
    }
    

    You will see that the exact same thing happens, but it looks more elegant and avoids conflicts.

    Details

    Laravel gives only a high level overview of the service container, which doesn't help to learn how it works on the inside. The best way to see that is to go down the call stack.

    When you do that, you find that Laravel registers every class in the project in the service container. That means that whether you create a service provider or not, the class will be in the container. How exactly?

    When you run php artisan optimize, Laravel creates files that have array with all the classes of the project. When you run the app, after registering everything from the service providers, Laravel registers the rest of the classes from that file.

    That means that in your case, if you don't specifically register the Test class, it will still be resolvable. Basically, you only need to register classes that need some specific instructions to be resolved.

    So how does Laravel resolve the dependencies?

    1. When you run \App::make(Test::class) or inject dependency via type hinting in the constructor (the "better way" from my solution), Laravel looks for that dependency among the bindings.
    2. When it finds the dependency, it resolves either the closure associated to it or the constructor of the class directly.
    3. When it resolves the constructor directly, it looks for type hints among the constructor parameters and recursively resolves all of them until there's nothing else to resolve.
    4. After that it returns the resolved class.

    Of course, bear in mind that for Laravel to analyze the contructor of a class, it needs to be resolved via the service container in the first place. You can't just call $test = new Test(); and expect Laravel to do all the magic :)

    Conclusion

    This is a rather quick overview of Laravel's service container. The best way for you to learn it is, of course, studying the sources for yourself. It's truly elegant and it uses PHP's functionality to the fullest.

    I really hope this shed some light on the service container for you and can help you in the future :)