< Back

Laravel Service Container

先看一段基础的laravel用法

创建一个服务如下:

<?php namespace App\Services; class GreetingService { public function greet($name) { return "Hello, $name!"; } }

控制器如下:

<?php namespace App\Http\Controllers; use App\Services\GreetingService; class GreetingController extends Controller { protected $greetingService; public function __construct(GreetingService $greetingService) { $this->greetingService = $greetingService; } public function greet($name) { $greeting = $this->greetingService->greet($name); return $greeting; } }

路由如下:

Route::get('/greet/{name}', 'GreetingController@greet');

访问比如http://127.0.0.1:8000/greet/tim,就能在页面上看到Hello, tim!

看完我产生了一个疑问:

🙋当路由被访问的时候,GreetingController是如何完成实例化的?

要回答这个问题,主要有以下几个部分需要了解:autoload服务容器路由的加载Facade

autoload

public/index.php首先就会去加载autoload.php

require __DIR__.'/../vendor/autoload.php';

创建loader并通过autoload_static.php里面的一些变量对loader初始化,这些变量像是一些路径,最后自动加载文件的时候会用到。比如那目前的这个问题来说,先关注prefixLengthsPsr4。

// vendor/composer/autoload_real.php self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::getInitializer($loader)); // vendor/composer/autoload_static.php public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$prefixDirsPsr4; $loader->fallbackDirsPsr4 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$fallbackDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$prefixesPsr0; $loader->classMap = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$classMap; }, null, ClassLoader::class); } public static $prefixLengthsPsr4 = array ( 'A' => array ( 'App\\' => 4, ), )

loader创建完成后,开始调用register,register主要就是用spl_autoload_register注册了loadClass,当引用的类没有定义的时候,会调用到loadClass来尝试加载。

// vendor/composer/autoload_real.php $loader->register(true); // vendor/composer/ClassLoader.php public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); // ... } public function loadClass($class) { if ($file = $this->findFile($class)) { $includeFile = self::$includeFile; $includeFile($file); return true; } return null; }

服务容器

composer的autoload加载完后,下面马上加载bootstrap/app.php,里面创建了一个Application对象,这个类继承自Container。

经常会看到的make实际上是直接调用的resolve方法,为了构建对象而做一些准备工作,比如检查是不是有设置别名等等。而真正创建对象是在build方法中,看到用反射来构建对象,并且,当构造函数需要参数的时候,通过resolveClass,可以看到也是直接使用make来创建的。这就是为什么他可以自动解决依赖。

$app = require_once __DIR__.'/../bootstrap/app.php'; // bootstrap/app.php $app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); // Illuminate\Foundation\Application class Application extends Container implements ApplicationContract, HttpKernelInterface { public function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); } protected function resolve($abstract, $parameters = []) { $abstract = $this->getAlias($abstract); $needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract) ); // If an instance of the type is currently being managed as a singleton we'll // just return an existing instance instead of instantiating new instances // so the developer can keep using the same objects instance every time. if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; } $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); // We're ready to instantiate an instance of the concrete type registered for // the binding. This will instantiate the types, as well as resolve any of // its "nested" dependencies recursively until all have gotten resolved. if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); } // If we defined any extenders for this type, we'll need to spin through them // and apply them to the object being built. This allows for the extension // of services, such as changing configuration or decorating the object. foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } // If the requested type is registered as a singleton we'll want to cache off // the instances in "memory" so we can return it later without creating an // entirely new instance of an object on each subsequent request for it. if ($this->isShared($abstract) && ! $needsContextualBuild) { $this->instances[$abstract] = $object; } $this->fireResolvingCallbacks($abstract, $object); // Before returning, we will also set the resolved flag to "true" and pop off // the parameter overrides for this build. After those two things are done // we will be ready to return back the fully constructed class instance. $this->resolved[$abstract] = true; array_pop($this->with); return $object; } public function build($concrete) { // If the concrete type is actually a Closure, we will just execute it and // hand back the results of the functions, which allows functions to be // used as resolvers for more fine-tuned resolution of these objects. if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); } $reflector = new ReflectionClass($concrete); // If the type is not instantiable, the developer is attempting to resolve // an abstract type such as an Interface of Abstract Class and there is // no binding registered for the abstractions so we need to bail out. if (! $reflector->isInstantiable()) { return $this->notInstantiable($concrete); } $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); // If there are no constructors, that means there are no dependencies then // we can just resolve the instances of the objects right away, without // resolving any other types or dependencies out of these containers. if (is_null($constructor)) { array_pop($this->buildStack); return new $concrete; } $dependencies = $constructor->getParameters(); // Once we have all the constructor's parameters we can create each of the // dependency instances and then use the reflection instances to make a // new instance of this class, injecting the created dependencies in. $instances = $this->resolveDependencies( $dependencies ); array_pop($this->buildStack); return $reflector->newInstanceArgs($instances); } protected function resolveDependencies(array $dependencies) { $results = []; foreach ($dependencies as $dependency) { // If this dependency has a override for this particular build we will use // that instead as the value. Otherwise, we will continue with this run // of resolutions and let reflection attempt to determine the result. if ($this->hasParameterOverride($dependency)) { $results[] = $this->getParameterOverride($dependency); continue; } // If the class is null, it means the dependency is a string or some other // primitive type which we can not resolve since it is not a class and // we will just bomb out with an error since we have no-where to go. $results[] = is_null($dependency->getClass()) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency); } return $results; } protected function resolveClass(ReflectionParameter $parameter) { try { return $this->make($parameter->getClass()->name); } // If we can not resolve the class instance, we will check to see if the value // is optional, and if it is we will return the optional parameter value as // the value of the dependency, similarly to how we do this with scalars. catch (BindingResolutionException $e) { if ($parameter->isOptional()) { return $parameter->getDefaultValue(); } throw $e; } } }

Facade

Route是指的那个类?这个答案比较容易找到,是指的vendor/laravel/framework/src/Illuminate/Routing/Route.php,但是里面没有get静态方法,父类里面也没有,但是父类里面有一个__callStatic,当静态调用的方法不存在的时候这个方法会触发。可以看到他最终是转去调用了另外一个$instance。getFacadeAccessor()返回字符串router,然后resolveFacadeInstance去找static::$app['router']返回。也就是说最终Route::get()实际上是调用的static::$app['router']->get()

public static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } 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]; } return static::$resolvedInstance[$name] = static::$app[$name]; }

那么$app是什么?只能看到可能是通过下面这个setFacadeApplication方法设置进去的,但是新问题:

🙋setFacadeApplication是在哪里调用的呢?

public static function setFacadeApplication($app) { static::$app = $app; }

一开始猜测肯定是在Application初始化的过程中,然后看了下Application的构造函数,没有找到相关的。所以我只能跳过app.php的加载,继续在之后的处理中找。

创建完app对象后,马上就创建了一个Http\Kernel的单例,singleton方法实际调用的是bind,只是参数$shared=true来保证单例。

// bootstrap/app.php $app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); $app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); // vendor/laravel/framework/src/Illuminate/Container/Container.php public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); } public function bind($abstract, $concrete = null, $shared = false) { // If no concrete type was given, we will simply set the concrete type to the // abstract type. After that, the concrete type to be registered as shared // without being forced to state their classes in both of the parameters. $this->dropStaleInstances($abstract); if (is_null($concrete)) { $concrete = $abstract; } // If the factory is not a Closure, it means it is just a class name which is // bound into this container to the abstract type and we will just wrap it // up inside its own Closure to give us more convenience when extending. if (! $concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); } $this->bindings[$abstract] = compact('concrete', 'shared'); // If the abstract type was already resolved in this container we'll fire the // rebound listener so that any objects which have already gotten resolved // can have their copy of the object updated via the listener callbacks. 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->make($concrete, $parameters); }; }

bind方法首先将$concrete转化成一个函数,这个函数接收一个容器对象,并通过make方法来创建对象。需要注意的是bind执行之后并没有真正开始创建对象,而只是保存了$this->bindings[$abstract],这就是为什么下面的代码中Illuminate\Contracts\Http\Kernel明明是一个interface却可以创建对象的原因,getConcrete会从bindings中拿到这个Closure,然后build的时候会直接调用他,不过最终还是调用的make方法来创建App\Http\Kernel,因此这里的$kernel实际上就是App\Http\Kernel对象。

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

然后继续往下看,这个handle会直接调到父类中的handle,当我看到bootstrap的时候我仿佛看到了一丝曙光,bootstrap方法会遍历$bootstrappers中设置的类,通过服务容器的make方法来创建对象,然后调用每个bootstrapper的bootstrap方法。(laravel的代码真是綺麗なぁ☺️,清晰易读

// public/index.php $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); // vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php public function handle($request) { try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } // .... return $response; } protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); } 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, ]; public function bootstrap() { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } // vendor/laravel/framework/src/Illuminate/Foundation/Application.php public function bootstrapWith(array $bootstrappers) { $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]); $this->make($bootstrapper)->bootstrap($this); $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]); } }

然后就发现RegisterFacades里面的bootstrap正是我要找到,这里不光解释了Facade里面的容器实例是在哪里设置的,还加载了alias,下面看看AliasLoader是如何工作的。

<?php namespace Illuminate\Foundation\Bootstrap; class 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(); } }

🙋这里为什么能够直接$app->make('config')来创建对象?

再梳理一遍make流程,发现是不能创建的,所以对象的创建应该不是发生在这里,然后我发现在Kernel执行bootstrap的时候,有个LoadConfiguration在RegisterFacades之前被执行了,然后发现他里面直接给instances数组塞了一个叫config的对象,因此当在这之后make('config')的时候,实际上只是直接返回这里创建的对象。

然后通过loadConfigurationFiles加载到config目录下的所有文件,require他们,set到config对象中,也就是Illuminate\Config\Repository里面,这样他就能够方便的通过config('aliases.Route')这种形式来获取数据了。

// vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php use Illuminate\Config\Repository; class LoadConfiguration { public function bootstrap(Application $app) { $items = []; // First we will see if we have a cache configuration file. If we do, we'll load // the configuration items from that file so that it is very quick. Otherwise // we will need to spin through every configuration file and load them all. if (file_exists($cached = $app->getCachedConfigPath())) { $items = require $cached; $loadedFromCache = true; } // Next we will spin through all of the configuration files in the configuration // directory and load each one into the repository. This will make all of the // options available to the developer for use in various parts of this app. $app->instance('config', $config = new Repository($items)); if (! isset($loadedFromCache)) { $this->loadConfigurationFiles($app, $config); } // Finally, we will set the application's environment based on the configuration // values that were loaded. We will pass a callback which will be used to get // the environment in a web context where an "--env" switch is not present. $app->detectEnvironment(function () use ($config) { return $config->get('app.env', 'production'); }); date_default_timezone_set($config->get('app.timezone', 'UTC')); mb_internal_encoding('UTF-8'); } protected function loadConfigurationFiles(Application $app, RepositoryContract $repository) { $files = $this->getConfigurationFiles($app); if (! isset($files['app'])) { throw new Exception('Unable to load the "app" configuration file.'); } foreach ($files as $key => $path) { $repository->set($key, require $path); } } protected function getConfigurationFiles(Application $app) { $files = []; $configPath = realpath($app->configPath()); foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) { $directory = $this->getNestedDirectory($file, $configPath); $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath(); } ksort($files, SORT_NATURAL); return $files; } } // vendor/laravel/framework/src/Illuminate/Foundation/Application.php public function configPath($path = '') { return $this->basePath.DIRECTORY_SEPARATOR.'config'.($path ? DIRECTORY_SEPARATOR.$path : $path); }

然后回到AliasLoad来,AliasLoader::getInstance(...)->register(),看下register干了什么,所谓的注册原来是注册了个autoload,且spl_autoload_register的prepend参数为true,这就意味着这里的load函数要比最开始composer里面设置的autoload函数优先级高。

// vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php public function register() { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } protected function prependToLoaderStack() { spl_autoload_register([$this, 'load'], true, true); }

看到这里的load函数,就可以回答一个很关键的问题,调用Route是怎么调用到vendor/laravel/framework/src/Illuminate/Routing/Route.php的。

函数的最后面会去检查$this->aliases,而这个aliases就是在之前getInstance调用的时候塞进来的app.aliases

也就是说当需要autoload的时候会优先检查config/app.php下的aliases配置,因此调用Route::get()最终会调用到这里的load函数,isset($this->aliases['Route'])==true,执行class_alias(Illuminate\Support\Facades\Route::class, $alias)

// vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php public function load($alias) { if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) { $this->loadFacade($alias); return true; } if (isset($this->aliases[$alias])) { return class_alias($this->aliases[$alias], $alias); } } public static function getInstance(array $aliases = []) { if (is_null(static::$instance)) { return static::$instance = new static($aliases); } $aliases = array_merge(static::$instance->getAliases(), $aliases); static::$instance->setAliases($aliases); return static::$instance; }

到这其实还没完,带来了一个新问题:

🙋class_alias是如何加载Illuminate\Support\Facades\Route::class的,因为这并不是一个完整的绝对路径,光这一句代码并不足以完成加载

没有具体去验证了,但是答案应该是在composer最开始设置的autoload中,下面的代码给classloader设置了classMap,而这个classMap中设置了Illuminate\Support\Facades\Route的绝对路径。

// vendor/composer/autoload_real.php require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::getInitializer($loader)); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$prefixDirsPsr4; $loader->fallbackDirsPsr4 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$fallbackDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$prefixesPsr0; $loader->classMap = ComposerStaticInitb7904d07d1e1765a0a199aa11d6301a3::$classMap; }, null, ClassLoader::class); } public static $classMap = array ( //... 'Illuminate\\Support\\Facades\\Route' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Support/Facades/Route.php', //... )

然后当去加载Illuminate\Support\Facades\Route的时候,会调用到之前ClassLoader中注册的autoload函数loadClass(上面有贴,这里不贴了),该函数会通过findFile去找这个文件,然后include进来。

findFile首先就会去检查classMap里是否有配置,有就直接返回,这样就拿到了Illuminate\Support\Facades\Route的绝对路径,并加载了进来。

// vendor/composer/ClassLoader.php public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } // ... return $file; }

目前为止知道了以下几点:

  • Route::get()实际上是调用的static::$app['router']->get()
  • $app就是服务容器实例,并且知道是在哪里设置进来的
  • Route是一个别名,且在加载类的时候,config/app下的aliases会优先进行autoload

然后关于Route::get('/greet/{name}', 'GreetingController@greet')剩下就只需要知道以下几点即可:

  • static::$app['router']里的router是什么
  • router在哪里创建的
  • GreetingController是如何加载的

在Application创建的时候,也就是构造函数里调用了registerBaseServiceProviders,里面注册了RoutingServiceProvider,此时还会执行RoutingServiceProvider->register()方法,这个时候就会看到注册了一个叫做router的单例对象,对应的类是Router。

// vendor/laravel/framework/src/Illuminate/Foundation/Application.php protected function registerBaseServiceProviders() { // ... $this->register(new RoutingServiceProvider($this)); } // vendor/laravel/framework/src/Illuminate/Routing/RoutingServiceProvider.php public function register() { $this->registerRouter(); // .... } protected function registerRouter() { $this->app->singleton('router', function ($app) { return new Router($app['events'], $app); }); }

这里又带来几个新的问题

🙋这里的Router又是什么?

🙋众所周知singleton只是绑定,并没有真正创建对象,那么是在哪里创建呢?

🙋**$app是一个对象,为何可以像访问数组元素一样通过$app['router']这种形式来获取router?**

在找寻这些问题的答案的过程中,又发现了一些“黑魔法”一样的东西😌。

首先一个关键点就是Container类实现了ArrayAccess接口,而这个接口其实是php本身的接口,通过实现里面的四个方法来使得对象也能数组那像使用。

比如$app['router']被调用的时候,就会去访问offsetGet方法,看来这里恍然大悟,原来是在这里make的,因此执行$app['router']的时候,router对应的对象就会被自动创建了。

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

然后关于Router是什么,我后来意识到这个问题有点傻😄,因为Illuminate/Routing/RoutingServiceProvider.php中有设置namespace Illuminate\Routing;,所以这里的Router其实就是Illuminate\Routing\Router,所以这里就不涉及到autoload这些的了。这样,这三个问题就搞清楚了:

千辛万苦知道了一个众所周知的知识点😂:Route::get() = Router->get()

回过头来,目前是在Kernel的bootstrap里面,遍历$bootstrappers数组,执行每个的boostrap方法,最后两个为:

\Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class

其中RegisterProviders会直接调用$app->registerConfiguredProviders(),将config/app.providers中所有provider按照eager,when,deferred的分类进行register。如果是eager就直接执行其中的register方法。

BootProviders在最后,会遍历config/app.providers,执行其中的boot方法。其中值得关注的是RouteServiceProvider。

// vendor/laravel/framework/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php public function boot() { $this->setRootControllerNamespace(); if ($this->app->routesAreCached()) { $this->loadCachedRoutes(); } else { $this->loadRoutes(); $this->app->booted(function () { $this->app['router']->getRoutes()->refreshNameLookups(); $this->app['router']->getRoutes()->refreshActionLookups(); }); } } protected function loadRoutes() { if (method_exists($this, 'map')) { $this->app->call([$this, 'map']); } } public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); } protected function mapWebRoutes() { Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); }

上面这个mapWebRoutes里面,调用了Route也就是Router(这个好容易搞混)并没有middleware,namespace这些方法但是有一个__call方法,所以当middleware被调用的时候,实际上是返回了一个RouteRegistrar对象,并设置了里面的attributes数组,所以attributes应该差多是这种感觉:

attributes = [ 'middleware' => 'web', 'namespace' => 'App\Http\Controllers' ] // vendor/laravel/framework/src/Illuminate/Routing/Router.php public function __call($method, $parameters) { if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); } if ($method == 'middleware') { return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters); } return (new RouteRegistrar($this))->attribute($method, $parameters[0]); } // vendor/laravel/framework/src/Illuminate/Routing/RouteRegistrar.php public function attribute($key, $value) { if (! in_array($key, $this->allowedAttributes)) { throw new InvalidArgumentException("Attribute [{$key}] does not exist."); } $this->attributes[Arr::get($this->aliases, $key, $key)] = $value; return $this; }

然后还调用group传入routes/web.php文件路径,RouteRegistrard->group会直接调用Router->group。这里就看到一个很关键的地方,这里完成了routes/web.php文件的加载,且有一个有意思的地方是$router = $this这一条,这样会使得web.php中除了可以使用Route这个Facade外,也可以直接使用$router这个变量进行路由的定义

// vendor/laravel/framework/src/Illuminate/Routing/Router.php public function group(array $attributes, $routes) { $this->updateGroupStack($attributes); // Once we have updated the group stack, we'll load the provided routes and // merge in the group's attributes when the routes are created. After we // have created the routes, we will pop the attributes off the stack. $this->loadRoutes($routes); array_pop($this->groupStack); } protected function loadRoutes($routes) { if ($routes instanceof Closure) { $routes($this); } else { $router = $this; require $routes; } }

这里得出结论:

在bootstrap的最后,也就是boot过程中加载了路由文件

到这里终于走到具体的路由定义部分了😮‍💨,从Router的get方法开始。

public function get($uri, $action = null) { return $this->addRoute(['GET', 'HEAD'], $uri, $action); } protected function addRoute($methods, $uri, $action) { return $this->routes->add($this->createRoute($methods, $uri, $action)); } protected function createRoute($methods, $uri, $action) { // If the route is routing to a controller we will parse the route action into // an acceptable array format before registering it and creating this route // instance itself. We need to build the Closure that will call this out. if ($this->actionReferencesController($action)) { // 这里将Controller添加上namespace $action = $this->convertToControllerAction($action); } $route = $this->newRoute( $methods, $this->prefix($uri), $action ); // If we have groups that need to be merged, we will merge them now after this // route has already been created and is ready to go. After we're done with // the merge we will be ready to return the route back out to the caller. if ($this->hasGroupStack()) { $this->mergeGroupAttributesIntoRoute($route); } $this->addWhereClausesToRoute($route); return $route; } protected function convertToControllerAction($action) { if (is_string($action)) { $action = ['uses' => $action]; } // Here we'll merge any group "uses" statement if necessary so that the action // has the proper clause for this property. Then we can simply set the name // of the controller on the action and return the action array for usage. if (! empty($this->groupStack)) { $action['uses'] = $this->prependGroupNamespace($action['uses']); } // Here we will set this controller name on the action array just so we always // have a copy of it for reference if we need it. This can be used while we // search for a controller name or do some other type of fetch operation. $action['controller'] = $action['uses']; return $action; } protected function prependGroupNamespace($class) { $group = end($this->groupStack); return isset($group['namespace']) && strpos($class, '\\') !== 0 ? $group['namespace'].'\\'.$class : $class; } protected function newRoute($methods, $uri, $action) { return (new Route($methods, $uri, $action)) ->setRouter($this) ->setContainer($this->container); }

get方法会往routes中添加一个路由对象(new Route),这里的routes是一个RouteCollection对象,在构造函数中定义的。

$this->routes = new RouteCollection;

在createRoute的过程中会给action部分(即GreetingController@greet这个字符串)加上namespace。

具体来说是在prependGroupNamespace中,会将之前设置的namespace拿出来添加到action前面,大概会长这样吧

App\Http\Controllers\GreetingController@greet

之前设置的namespace也就是mapWebRoutes中设置的

Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php'))

因此get方法只是创建路由对象,保存到routes集合中,途中给action加上了namespace。并没有真正的创建对象,也没有必要。

到这里为止,Kernel->bootstrap()完成了✅。

protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); }

dispatchToRouter将request交给router->dispatch()处理,router->dispatch会根据request找到匹配到的路由(Route对象),然后执行路由的run方法。

这时候终于找到了最开始的问题的答案,Controller是通过getController创建的,创建方式也毫不意外,使用make方法。

// vendor/laravel/framework/src/Illuminate/Routing/Route.php 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() ); } public function getController() { if (! $this->controller) { $class = $this->parseControllerCallback()[0]; $this->controller = $this->container->make(ltrim($class, '\\')); } return $this->controller; }

parseControllerCallback解析action字符串,方式是使用的Str::parseCallback,根据@符号分割,前面的就是Controller类名,后面的就是方法名。

// vendor/laravel/framework/src/Illuminate/Support/Str.php public static function parseCallback($callback, $default = null) { return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; }

有必要看一下$this->controllerDispatcher()->dispatch(),因为实际执行控制器是在这里,且在执行前他还先解决了控制器函数的依赖(通过transformDependency),方式依然是容器的make方法,所以怪不得laravel文档的一开始就先介绍了服务容器,因为实在是太重要了。

public function dispatch(Route $route, $controller, $method) { $parameters = $this->resolveClassMethodDependencies( $route->parametersWithoutNulls(), $controller, $method ); if (method_exists($controller, 'callAction')) { return $controller->callAction($method, $parameters); } return $controller->{$method}(...array_values($parameters)); } public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector) { $instanceCount = 0; $values = array_values($parameters); foreach ($reflector->getParameters() as $key => $parameter) { $instance = $this->transformDependency( $parameter, $parameters ); if (! is_null($instance)) { $instanceCount++; $this->spliceIntoParameters($parameters, $key, $instance); } elseif (! isset($values[$key - $instanceCount]) && $parameter->isDefaultValueAvailable()) { $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue()); } } return $parameters; } protected function transformDependency(ReflectionParameter $parameter, $parameters) { $class = $parameter->getClass(); // If the parameter has a type-hinted class, we will check to see if it is already in // the list of parameters. If it is we will just skip it as it is probably a model // binding and we do not want to mess with those; otherwise, we resolve it here. if ($class && ! $this->alreadyInParameters($class->name, $parameters)) { return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : $this->container->make($class->name); } }

至此就算是大致看完了整个过程了。