As the scale of applications becomes larger and larger, the dependency between objects becomes more and more complex, and the coupling degree becomes higher and higher. Multiple dependencies between objects often occur. For such a large and complex application, any modification is likely to affect the whole body, which causes a lot of trouble for the later maintenance of the application.

In order to solve the problem of high coupling degree between objects, the idea of inversion of control (IoC) was also born. Inversion of control is a design principle in object-oriented programming that aims to reduce coupling between code. In Laravel, inversion of control is implemented through dependency injection (DI).

The basic idea of inversion of control is to decouple dependencies between objects with the help of the IoC container. After the introduction of IoC container, the control of all objects is handed over to IoC container, IoC container becomes the core of the whole system, glue all objects together to play a role. The containers in Laravel do just that.

Same container

The container in Laravel is the \Illuminate\Foundation\Application object that the Laravel framework creates at startup.

# public/index.php
$app = require_once __DIR__.'/.. /bootstrap/app.php';
# bootstrap/app.php
$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH']???? dirname(__DIR__));Copy the code

Laravel also performs some basic binding and service registration for the container as it creates it. Laravel first binds the Container instance to app and Illuminate Container\Container; Laravel then registers the underlying service providers into the container instance, including event, logging, and routing service providers; Finally, Laravel registers the framework core class with its corresponding alias into the container instance.

// namespace Illuminate\Foundation\Application

public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }

    $this->registerBaseBindings();
    $this->registerBaseServiceProviders();
    $this->registerCoreContainerAliases();
}
    
protected function registerBaseBindings()
{
    static::setInstance($this);

    $this->instance('app'.$this);

    $this->instance(Container::class, $this);
    / *... . * /
}

protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));
    $this->register(new LogServiceProvider($this));
    $this->register(new RoutingServiceProvider($this));
}

public function registerCoreContainerAliases()
{
    foreach ([
        'app'= > [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
        /* ... ...*/
        'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class],
        'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
        / *... . * /
        'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
        'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
        / *... . * /
    ] as $key= >$aliases) {
        foreach ($aliases as $alias) {
            $this->alias($key.$alias); }}}// namespace Illuminate\Container\Container
public function alias($abstract.$alias)
{
    if ($alias= = =$abstract) {
        throw new LogicException("[{$abstract}] is aliased to itself.");
    }

    $this->aliases[$alias] = $abstract;

    $this->abstractAliases[$abstract=] []$alias;
}
Copy the code

After completing these three basic registration steps, we can easily access the object instances that have been registered in the container. For example, the Container itself can be accessed directly through $app[‘app’] or $app[‘Illuminate\Container\Container’], and the database connection can be accessed directly through $app[‘db’].

⒉ Service provider registration and service access

4. Belief Registered service provider

The underlying service provider is registered during container creation, which is done by calling the register() method.

// namespace Illuminate\Foundation\Application
public function register($provider.$force = false)
{
    if (($registered = $this->getProvider($provider)) &&!$force) {
        return $registered;
    }

    if (is_string($provider)) {
        $provider = $this->resolveProvider($provider);
    }

    $provider->register();

    if (property_exists($provider.'bindings')) {
        foreach ($provider->bindings as $key= >$value) {
            $this->bind($key.$value); }}if (property_exists($provider.'singletons')) {
        foreach ($provider->singletons as $key= >$value) {
            $this->singleton($key.$value); }}$this->markAsRegistered($provider);

    if ($this->isBooted()) {
        $this->bootProvider($provider);
    }

    return $provider;
}
Copy the code

Laravel first determines whether the specified service provider is already registered in the container (by calling the getProvider() method). If the specified service provider is already registered in the container and the registration is not mandatory, the registered service provider is returned.

If the above conditions are not met, Then Laravel starts registering the service provider. At this point, if the argument is a string, Laravel defaults to the service provider’s class name and instantiates it (via the resolveProvider() method). After that, the register() method defined by the service provider is invoked to register. In the case of a logging service provider, the body of its register() method is as follows

// namespace Illuminate\Log\LogServiceProvider
public function register()
{
    $this->app->singleton('log'.function ($app) {
        return new LogManager($app);
    });
}
Copy the code

The register() method registers the Illuminate\Log\LogManager object as a singleton to the container. Once registered, an entry is added to the $Bindings property of the container

$app->bindings['log'] = [
    'concrete'= >'Illuminate\Log\LogManager {#162}'.'shared'= >true,];Copy the code

If the service provider also defines the $Bindings attribute and the $Singletons attribute, Laravel also calls the corresponding bind() and Singleton () methods to register the service provider’s custom bindings.

Laravel then marks the service provider as registered, and then invoking the boot() method defined by the service provider to start the service provider (assuming the application has been started).

There are bind() and singleton() methods to register a binding with a container. The only difference is whether the registered binding is singleton, that is, whether the shared property is true.

// namespace Illuminate\Container\Container
public function singleton($abstract.$concrete = null)
{
    $this->bind($abstract.$concrete.true);
}

public function bind($abstract.$concrete = null.$shared = false)
{
    // Delete the old binding
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    if (! $concrete instanceof Closure) {
        if (! is_string($concrete)) {
            throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
        }

        $concrete = $this->getClosure($abstract.$concrete);
    }

    $this->bindings[$abstract] = compact('concrete'.'shared');

    if ($this->resolved($abstract)) {
        $this->rebound($abstract); }}protected function getClosure($abstract.$concrete)
{
    return function ($container.$parameters = []) use ($abstract.$concrete) {
        if ($abstract= =$concrete) {
            return $container->build($concrete);
        }

        return $container->resolve(
            $concrete.$parameters.$raiseEvents = false
        );
    };
}
Copy the code

Again using the logging service provider as an example, the logging service provider registers in singleton mode at registration time and the $Concrete parameter is a closure. Before the binding starts, Laravel first removes the old binding. Since $concrete is a closure, Laravel does nothing but store the binding information into the $Bindings attribute.

⓶ Access services

After the service provider registration is complete, we can access the service in the same way as accessing a database connection mentioned above. Using the logging service as an example, we can access the logging service via $app[‘log’]. Alternatively, in Laravel, we can use Facades to access services. For example, we can call Illuminate\Support\Facades\Log::info() to Log.

// namespace Illuminate\Support\Facades\Log
class Log extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'log'; }}// namespace Illuminate\Support\Facades\Facade
public static function __callStatic($method.$args)
{
    $instance = static::getFacadeRoot();
    / *... . * /
    return $instance->$method(...$args);
}

public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
}

protected static function resolveFacadeInstance($name)
{
    if (is_object($name)) {
        return $name;
    }

    if (isset(static: :$resolvedInstance[$name]) {return static: :$resolvedInstance[$name];
    }

    if (static: :$app) {
        return static: :$resolvedInstance[$name] = static: :$app[$name]; }}Copy the code

When logging through static invocation, the magic method __callStatic() on the Facade is first accessed, which first resolves the service instance corresponding to the Facade and then invokes the method under that service instance to perform the corresponding function. Each facade defines a getFacadeAccessor() method that returns a tag that, in the case of logging services, is the key of the logging service provider’s closure in the container’s $Bindings attribute. That is, the result of the facade approach is $app[‘log’].

So why is it possible to access objects/services registered in the container through associative arrays? Illuminate Container Container implements ArrayAccess and defines the OffsetGet() method, while Illuminate Foundation Application inherits Container, $app is the object instantiated by the Application, so the Container’s OffsetGet() method is accessed by associative arrays of objects registered in the Container. The make() method of the Container is called in the OffsetGet() method, and the resolve() method is called in the make() method. The resolve() method eventually resolves and returns the appropriate object.

// namespace Illuminate\Container
public function offsetGet($key)
{
    return $this->make($key);
}

public function make($abstract.array $parameters = [])
{
    return $this->resolve($abstract.$parameters);
}

protected function resolve($abstract.$parameters= [].$raiseEvents = true)
{
    / *... . * /

    $this->with[] = $parameters;

    if (is_null($concrete)) {
        $concrete = $this->getConcrete($abstract);
    }

    if ($this->isBuildable($concrete.$abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    / *... . * /

    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

protected function getConcrete($abstract)
{
    if (isset($this->bindings[$abstract]) {return $this->bindings[$abstract] ['concrete'];
    }

    return $abstract;
}

protected function isBuildable($concrete.$abstract)
{
    return $concrete= = =$abstract || $concrete instanceof Closure;
}

public function build($concrete)
{
    if ($concrete instanceof Closure) {
        return $concrete($this.$this->getLastParameterOverride());
    }
    / *... . * /
}

protected function getLastParameterOverride()
{
    return count($this->with) ? end($this->with) : [];
}
Copy the code

$concrete in resolve() returns a closure that isBuildable() returns true when calling the log server instance in $app[‘log’]. So Laravel calls the build() method directly. Since $Concrete is a closure at this point, the closure function is executed directly in the build() method, which returns the LogManager instance.

C. Request processing

After the underlying binding and service registration is complete, the container is created and $app is returned. Laravel then registers the kernel (both Http kernel and Console kernel) and exception handling into the container. Laravel then starts processing the request.

// namespace bootstrap/app.php
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

// public/index.php
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Request::capture()
)->send();

$kernel->terminate($request.$response);
Copy the code

Before starting to process the request, Laravel first parses the Http kernel object $kernel, which is instantiated by App\Http\ kernel. App\Http\Kernel inherits Illuminate\Foundation\Kernel, so $Kernel actually calls the handle() method in Illuminate\Foundation\Kernel.

namespace Illuminate\Foundation\Http
use Illuminate\Contracts\Debug\ExceptionHandler

public function handle($request)
{
    try{$request->enableHttpMethodParameterOverride(a);$response = $this->sendRequestThroughRouter($request);
    } catch (Throwable $e) {
        $this->reportException($e);

        $response = $this->renderException($request.$e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request.$response));return $response;
}
// Error reported
protected function reportException(Throwable $e)
{
    $this->app[ExceptionHandler::class]->report($e);
}
// Render error message
protected function renderException($request.Throwable $e)
{
    return $this->app[ExceptionHandler::class]->render($request.$e);
}
Copy the code

If the handle() method encounters any exceptions or errors during the processing of a request, Laravel calls the registered exception handler in the container to report the exception and render the information back.

After the container is created, Laravel registers the binding between Illuminate\Contracts\Debug\ExceptionHandler and App\Exceptions\Handler into the container, So Laravel actually calls the methods in App\Exceptions\Handler to handle Exceptions. During actual development, developers can customize the report() and Render () methods in App\Exceptions\Handler according to their needs.

In PHP 7, ‘Exception’ and ‘Error’ are two different types, but both inherit from ‘Throwable’, so the ‘handler()’ method catches’ Throwable ‘objects.

Laravel performs some boot starts, including loading environment variables, configuration information, and so on, that play a very important role in running Laravel before it actually starts processing the request.

// namespace Illuminate\Foundation\Http\Kernel

protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
    \Illuminate\Foundation\Bootstrap\BootProviders::class,
];

protected function sendRequestThroughRouter($request)
{
    / *... . * /
    $this->bootstrap();
    / *... . * /
}

public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers()); }}// namespace Illuminate\Foundation\Application

public function bootstrapWith(array $bootstrappers)
{
    $this->hasBeenBootstrapped = true;

    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->dispatch('bootstrapping: '.$bootstrapper[$this]);

        $this->make($bootstrapper)->bootstrap($this);

        $this['events']->dispatch('bootstrapped: '.$bootstrapper[$this]); }}Copy the code

As you can see from the code, the bootstrap() method is actually called in each class. Among them:

  • LoadEnvironmentVariablesUsed to load environment variables
  • LoadConfigurationUsed to loadconfigConfiguration file in the directory
  • HandleExceptionsUsed to set thePHPError reporting levels and the corresponding exception and error handlers, in addition to setting PHP program termination functions
// namespace Illuminate\Foundation\Bootstrap\HandleExceptions

public function bootstrap(Application $app)
{
    / *... . * /
    $this->app = $app;

    error_reporting(-1);

    set_error_handler([$this.'handleError']);

    set_exception_handler([$this.'handleException']);

    register_shutdown_function([$this.'handleShutdown']);
    / *... . * /
}

public function handleError($level.$message.$file = ' '.$line = 0.$context = [])
{
    if (error_reporting() & $level) {
        / *... . * /
        throw new ErrorException($message.0.$level.$file.$line); }}public function handleException(Throwable $e)
{
    / *... . * /
        $this->getExceptionHandler()->report($e);
    / *... . * /
}

public function handleShutdown()
{
    if (! is_null($error = error_get_last()) && $this->isFatal($error['type']) {$this->handleException($this->fatalErrorFromPhpError($error.0)); }}protected function getExceptionHandler()
{
    return $this->app->make(\Illuminate\Contracts\Debug\ExceptionHandler::class);
}
Copy the code

As you can see from the above code, although HandleExceptions defines a Handler for Exceptions, errors, and program termination, in either case the method in App\Exceptions\Handler is ultimately called to HandleExceptions or errors.

  • RegisterFacadesThe function of one is to register profiles as well as third-party packages in customaliasClass, another very important function is forIlluminate\Support\Facades\FacadeClass set$appProperties.
// namespace Illuminate\Foundation\Bootstrap\RegisterFAcades

public function bootstrap(Application $app)
{
    Facade::clearResolvedInstances();

    Facade::setFacadeApplication($app);

    AliasLoader::getInstance(array_merge(
        $app->make('config')->get('app.aliases', []),
        $app->make(PackageManifest::class)->aliases()
    ))->register();
}
Copy the code

&emsp The static::$app that the facade uses to resolve the service instance in the container is set when we ask the registered service in the container through a facade.

  • RegisterProvidersIs used to register service providers defined in configuration files and third-party packages
// namespace Illuminate\Foundation\Bootstrap\RegisterProviders

public function bootstrap(Application $app)
{
    $app->registerConfiguredProviders();
}

public function registerConfiguredProviders()
{
    $providers = Collection::make($this->make('config')->get('app.providers'))
                    ->partition(function ($provider) {
                        return strpos($provider.'Illuminate\\') = = =0;
                    });

    $providers->splice(1.0[$this->make(PackageManifest::class)->providers()]);

    (new ProviderRepository($this.new Filesystem, $this->getCachedServicesPath()))
                ->load($providers->collapse()->toArray());
}
Copy the code

During the actual registration process, Laravel registers in the order of service provider of the Laravel framework > service provider of the third-party package > developer’s custom service provider

  • BootProvidersThe service providers that have been registered in the container are called sequentiallyboot()Method (if defined by the service providerboot()Methods)

After the boot boot is complete, Laravel starts processing the request, first applying the global middleware to the request. Laravel then dispatches the request to the appropriate Route for processing, which requires that the request’s Illuminate\Routing\Route object be found. In Laravel, in addition to the global middleware, there are middleware that only work on specific routes or routing groupings, which are then used on request. Once this is done, the routing object executes the code to complete the request.

// namespace Illuminate\Foundation\Http\Kernel
protected function sendRequestThroughRouter($request)
{
    / *... . * /
    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request'.$request);
        
        return $this->router->dispatch($request);
    };
}

// namespace Illuminate\Routing\Router
public function dispatch(Request $request)
{
    $this->currentRequest = $request;

    return $this->dispatchToRoute($request);
}

public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request.$this->findRoute($request));
}

protected function runRoute(Request $request, Route $route)
{
    / *... . * /
    return $this->prepareResponse($request.$this->runRouteWithinStack($route.$request)); }protected function runRouteWithinStack(Route $route, Request $request)
{
    / *... . * /   
    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            $request.$route->run()
                        );
                    });
}
Copy the code

(a) Host dependency injection

The route in Laravel can be registered as either a controller method or a closure. But either way, parameters need to be passed, and passing parameters can lead to situations that require dependency injection.

The Route object makes controller method calls or closure calls when executing the run() method, depending on the type of action. But both methods ultimately require parsing the parameters, and dependency injection is required if class is used in the parameters.

// namespace Illuminate\Routing\Router
public function run()
{
    $this->container = $this->container ? :new Container;

    try {
        if ($this->isControllerAction()) {
            return $this->runController();
        }

        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse(); }}protected function runController()
{
    return $this->controllerDispatcher()->dispatch(
        $this.$this->getController(), $this->getControllerMethod()
    );
}

protected function runCallable()
{
    / *... . * /
    return $callable(... array_values($this->resolveMethodDependencies(
        $this->parametersWithoutNulls(), new ReflectionFunction($callable)))); }// namespace Illuminate\Routing\ControllerDispatcher
public function dispatch(Route $route.$controller.$method)
{
    $parameters = $this->resolveClassMethodDependencies(
        $route->parametersWithoutNulls(), $controller.$method
    );
    / *... . * /
}

// namespace Illuminate\Routing\RouteDependencyResolverTrait
protected function resolveClassMethodDependencies(array $parameters.$instance.$method)
{
    / *... . * /
    return $this->resolveMethodDependencies(
        $parameters.new ReflectionMethod($instance.$method)); }public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
    / *... . * /
    foreach ($reflector->getParameters() as $key= >$parameter) {
        $instance = $this->transformDependency($parameter.$parameters.$skippableValue);
        / *... . * /
    }

    return $parameters;
}

protected function transformDependency(ReflectionParameter $parameter.$parameters.$skippableValue)
{
    $className = Reflector::getParameterClassName($parameter);

    if ($className&&!$this->alreadyInParameters($className.$parameters)) {
        return $parameter->isDefaultValueAvailable() ? null : $this->container->make($className);
    }

    return $skippableValue;
}
Copy the code

During execution, Laravel first gets the argument list by reflection (for controller methods, ReflectionMethod, for closure functions, ReflectionFunction). After getting the argument list, Laravel still uses reflection to determine the type of argument one by one. If the parameter type is a built-in PHP type, no special processing is required; But if the parameter is not a built-in PHP type, you need to use reflection to resolve the specific type of the parameter. After the specific type of the parameter is resolved, the next step is to determine whether the object of that type already exists in the parameter list. If not, and the type is not set to a default value, then you need to create an instance of that type from the container.

To create an instance of the specified class from the container, you still need to use the resolve() method. We’ve already described using the resolve() method to resolve closure functions, so instantiating the class is worth describing here.

// namespace Illuminate\Container\Container

public function build($concrete)
{
    / *... . * /

    try {
        $reflector = new ReflectionClass($concrete);
    } catch (ReflectionException $e) {
        throw new BindingResolutionException("Target class [$concrete] does not exist.".0.$e);
    }

    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    try {
        $instances = $this->resolveDependencies($dependencies);
    } catch (BindingResolutionException $e) {
        array_pop($this->buildStack);

        throw $e;
    }

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);
}

protected function resolveDependencies(array $dependencies)
{
    $results = [];

    foreach ($dependencies as $dependency) {
        if ($this->hasParameterOverride($dependency)) {
            $results[] = $this->getParameterOverride($dependency);

            continue;
        }

        $result = is_null(Util::getParameterClassName($dependency))?$this->resolvePrimitive($dependency)
                        : $this->resolveClass($dependency);

        if ($dependency->isVariadic()) {
            $results = array_merge($results.$result);
        } else {
            $results[] = $result; }}return $results;
}
Copy the code

When a container instantiates a class, it still gets the basic class information through reflection. Laravel throws exceptions for classes that cannot be instantiated (such as interface, abstract class). Otherwise, Laravel will continue to fetch information about the class’s constructor. Classes that do not have constructors are instantiated with no additional dependencies and can be directly instantiated using new. Otherwise, reflection still resolves the constructor’s argument list information and instantiates the classes used in those argument lists one by one. Once the classes in these parameter lists are instantiated, the preparation for creating a class from the container is complete, and the container can create an instance of the specified class and inject it into a controller method or closure.