本篇用于介绍 Laravel 5.6底层源码
最早加载的文件
一旦你打开某个网站,比如 http://example.com,你的 Web 服务器(nginx, Apache, ...)首先指向的是 public 目录下的 index.php 。 所以,你对网站的每一次请求都会先走到这个文件,让我们来看下 index.php文件的代码:
<?php define('LARAVEL_START', microtime(true)); require __DIR__.'/../vendor/autoload.php'; $app = require_once __DIR__.'/../bootstrap/app.php'; $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send(); $kernel->terminate($request, $response);
那么让 我们开始吧...
define('LARAVEL_START', microtime(true));
这行代码本质上启动了一个计时器,从而可以计算启动框架需要多长时间等等。一个有趣的事实是这个常量在框架的生命周期中从来没有被使用过。
require __DIR__.'/../vendor/autoload.php';
这行代码引入了 Composer 启动文件, 基本上每一个 PHP 文件在这里进行加载和使用, 因此没有必要在代码中进行 require MyClass.php。 注意这里并没有涉及到 Laravel 的任何内容
##从这里事情变得有意思了...
$app = require_once __DIR__.'/../bootstrap/app.php';
让我们查看 /bootstrap/app.php 文件, 毕竟这是 $app 变量所包含的全部内容。
<?php // 实例化 Application $app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); // Http Kernel 单例 $app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); // Console Kernel 单例 $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class ); // 异常处理 单例 $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); // 返回 $app return $app;
创建一个新的应用程序类实例
在这里,我们做了一些事情。基本上,我们启动框架概要,注册HTTP/控制台内核,并将应用程序的实例返回index.php。注意这里的app.php是一个应用程序工厂,这就是为什么它返回$app变量的原因,尽管$app由于需要而在全局上下文中。现在有代表LARAVER应用实例的Illuminate\Foundation\Application类。这个类包含从环境、Laravel版本、容器、路径等所有内容。一旦我们创建了这个类的新实例,我们就将根目录传递给构造函数中的项目,并调用两个方法。注意,应用程序类扩展了容器类。调用的方法是
$this->setBasePath($basePath); $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases();
我们将检查这些方法中的每一个,并在每次调用后查看应用程序类的内容。
在容器中注册路径
setBasePath($rootDirectory) 方法在容器中注册了所有相关的路径, 例如 app 根路径, 存储路径,资源路径等。 注意:你可以很方便的更改配置, 例如:
class MyCoolApplication extends Illuminate\Foundation\Application { public function langPath() { // /languages/* return $this->basePath.DIRECTORY_SEPARATOR.'language'; } public function configPath() { // resources/configuration/*.php return $this->resourcesPath().DIRECTORY_SEPARATOR.'configuration'; } } // 实例化自定义的 Application 来替代 Laravel 默认的 $app = new MyCoolApplication( realpath(__DIR__.'/../') );
在我们绑定完路径后,我们的程序结构看起来十分空旷:
Application {#7 ▼ #basePath: "/myCoolProject" #hasBeenBootstrapped: false #booted: false ... #instances: array:9 [▼ "path" => "/myCoolProject/app" "path.base" => "/myCoolProject" "path.lang" => "/myCoolProject/resources/lang" "path.config" => "/myCoolProject/config" "path.public" => "/myCoolProject/public" "path.storage" => "/myCoolProject/storage" "path.database" => "/myCoolProject/database" "path.resources" => "/myCoolProject/resources" "path.bootstrap" => "/myCoolProject/bootstrap" ] #aliases: [] #abstractAliases: [] ... }
你可以看到,还没有任何东西进行加载,容器目前只包含了路径。此时,任何路径都可以通过 $app->make('path.{what-you-want}') 方法进行解析。
在容器中注册自身
在 registerBaseBindings() 方法中,我们将自身(自身=应用类)设置为静态实例(所以我们可以用 单例模式 来解决这个问题)。这样做是因为我们只需要 一 个全局容器。接下来,我们在应用类上绑定一些别名,例如 app, Container::class (Illuminate\Container\Container) 并且我们也可以做一件特定的事儿--绑定由 Illuminate\Foundation\PackageManifest 类为代表的 package-loader。 请注意,在这些类中的构造函数中除了设置相关的基础路径和将 FileSystem 类存放在其自身中之外,不要做任何其他的处理。此时尚未加载任何包。 在设置完别名之后,我们可以通过从自身中调用 app(), app(Container::class) 或 app('Illuminate\Container\Container') 来解析容器。
static::setInstance($this); $this->instance('app', $this); $this->instance(Container::class, $this); $this->instance(PackageManifest::class, new PackageManifest( new Filesystem, $this->basePath(), $this->getCachedPackagesPath() ));
我们甚至可以猜测到我们的当前容器中的内容是什么样的... 它将包含路径,app,Container::class 和 PackageManifest::class。让我们来看看:
Application {#7 ▼ ... #instances: array:12 [▼ "path" => "/myCoolProject/app" "path.base" => "/Users/josip/Code/poslovi" "path.lang" => "/myCoolProject/resources/lang" "path.config" => "/myCoolProject/config" "path.public" => "/myCoolProject/public" "path.storage" => "/myCoolProject/storage" "path.database" => "/myCoolProject/database" "path.resources" => "/myCoolProject/resources" "path.bootstrap" => "/myCoolProject/bootstrap" "app" => Application {#7} "Illuminate\Container\Container" => Application {#7} "Illuminate\Foundation\PackageManifest" => PackageManifest {#8 ▶} ] #aliases: [] #abstractAliases: [] ... }
至此,我们已经完成了在容器中的注册,因此我们在需要的时候可以随时解析全局实例。接下来,是在生命周期中注册核心服务提供者。
注册核心服务提供者
$this->register(new EventServiceProvider($this)); $this->register(new LogServiceProvider($this)); $this->register(new RoutingServiceProvider($this));
这些是我们首先需要的3个核心服务。让我们看一下 register($provider) 方法中的具体内容。
// 因为这些提供者并没有被注册,所以没有什么意义 if (($registered = $this->getProvider($provider)) && ! $force) { return $registered; } // 因为这里我们传递的是类的实例,并不是类的名称,所以也没有什么意义 if (is_string($provider)) { $provider = $this->resolveProvider($provider); } // 在我们的每个提供者中调用 register() 方法 if (method_exists($provider, 'register')) { $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->booted) { $this->bootProvider($provider); } return $provider; // 对我们来讲没什么意义
事件和日志的服务提供者(事件调度器和日志记录器)在容器中只绑定其具体实现的单例实例。当路由提供者启动时会配置路由器和包含的一些必要服务,例如 URL 生成器,重定向和控制调度器。关于路由器的讲解将会是一个单独的课程,因为它是框架中的一个复杂部分。 现在来看看容器,我们可以看到核心服务提供者已经被加载了。请注意,此时你的路由文件尚未被加载,我们只配置了路由请求所需要的所有内容。你的请求还尚未被路由。
Application {#7 ▼ ... #serviceProviders: array:3 [▼ 0 => EventServiceProvider {#10 ▶} 1 => LogServiceProvider {#13 ▶} 2 => RoutingServiceProvider {#16 ▶} ] ... #bindings: array:9 [▼ "events" => array:2 [▶] "log" => array:2 [▶] "router" => array:2 [▶] "url" => array:2 [▶] "redirect" => array:2 [▶] "Psr\Http\Message\ServerRequestInterface" => array:2 [▶] "Psr\Http\Message\ResponseInterface" => array:2 [▶] "Illuminate\Contracts\Routing\ResponseFactory" => array:2 [▶] "Illuminate\Routing\Contracts\ControllerDispatcher" => array:2 [▶] ] #methodBindings: [] #instances: array:12 [▼ "path" => "/myCoolProject/app" "path.base" => "/myCoolProject" "path.lang" => "/myCoolProject/resources/lang" "path.config" => "/myCoolProject/config" "path.public" => "/myCoolProject/public" "path.storage" => "/myCoolProject/storage" "path.database" => "/myCoolProject/database" "path.resources" => "/myCoolProject/resources" "path.bootstrap" => "/myCoolProject/bootstrap" "app" => Application {#7} "Illuminate\Container\Container" => Application {#7} "Illuminate\Foundation\PackageManifest" => PackageManifest {#8 ▶} ] ... }
注册核心类
在我们加载好日志记录器,路由器和事件调度器之后,便可以开始注册其他所有内容。这就是 registerCoreContainerAlisases() 的作用。让我们看看它的内容,我们可以看到全部加载完成的服务,例如认证,管理器,邮件收发器,数据库。
注意,我们还没有加载 .env 文件、配置或实例化与数据库的连接。我们仅仅在容器中加载和储存类,一切正在建立中。 炫酷的部分(解析请求, 连接数据库等)将在之后进行,并且我会在这个系列的下一部分来解释其原因和方法。
谁想知道更多
由于 singleton() 和 instance()在整个框架中被调用的很多次,让我们来看看这些核心方法。 singleton() 方式中只是将 bind() 方法中的 $shared 参数设置为 true 之后执行。这个方法实现了绑定到容器,每当我们需要的时候可以解析它。
public function bind($abstract, $concrete = null, $shared = false) { // 在我们的例子中, $share = true // 如果没有设置 $concrete ,$concrete会被设置与 $abstract 相同,或者只向下解析闭包 // 获取实例 $this->dropStaleInstances($abstract); if (is_null($concrete)) { $concrete = $abstract; } if (! $concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); } // 将 $abstract 作为键 ,参数组成的数组作为值储存在 $bindings 属性中 $this->bindings[$abstract] = compact('concrete', 'shared'); // 如果已经在此容器中解析了抽象类型, // 我们将触发反弹监听器。 // 以便已经解析的任何对象都可以通过侦听器回调更新对象的副本。 if ($this->resolved($abstract)) { $this->rebound($abstract); } }
同时,instance() 方法将已经实例化的类绑定到容器。
public function instance($abstract, $instance) { $this->removeAbstractAlias($abstract); $isBound = $this->bound($abstract); unset($this->aliases[$abstract]); // 我们将检查以前是否绑定过此对象。 // 如果绑定过,则触发 rebound 方法,进行重新绑定。 // 下面会自动覆盖 $abstract 字符串标识对应的绑定对象。 $this->instances[$abstract] = $instance; if ($isBound) { $this->rebound($abstract); } return $instance; }
注意,这些方法有一个参数 $abstract,之所以有这个参数,是因为我们可以将任何具体的实例或实现替换为另一个,但仍然使用相同的键解析它;即 $abstract 相当于 PHP 关联数组中的 key,而数组中的 value 就是绑定到容器中的对象,当我们想使用这个对象的时候,就可以通过 key 进行解析获取。 例如,拥有 Cache 接口并绑定其实现可以是 RedisStore、MemcachedStore、FileStore 等的缓存接口。您可以轻松地交换实现并使用相同的 Cache::class 键解析它们。
如下 Laravel 文档中的一段:
$this->app->bind( 'App\Contracts\EventPusher', 'App\Services\RedisEventPusher' // 这可以用任何具体的实现来替换 ); // 通过调用 app(App\Contracts\EventPusher::class) 来解析 RedisEventPusher 对象
© 著作权归作者所有
发表评论