├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── articles ├── Auth1.md ├── Auth2.md ├── Auth3.md ├── ConsoleKernel.md ├── Contracts.md ├── Controller.md ├── Cookie.md ├── Database1.md ├── Database2.md ├── Database3.md ├── Database4.md ├── DecoratorPattern.md ├── ENV.md ├── Event.md ├── Exception.md ├── FacadePattern.md ├── Facades.md ├── Farewell.md ├── HttpKernel.md ├── IocContainer.md ├── Middleware.md ├── Observer.md ├── Request.md ├── Response.md ├── Route.md ├── ServiceProvider.md ├── Session.md └── reflection.md ├── codes └── reflection_dependency_injection_demo.php └── images ├── .gitkeep ├── WX20200119-143845@2x.png ├── WechatDonation.jpeg └── tWbHIMFsM3.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kevin Yan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning_Laravel_Kernel 2 | 3 | Laravel核心代码学习 4 | 5 | ## 前言 6 | 7 | 如果你对`Laravel`里面的依赖注入、服务绑定、服务解析等等这些东西很好奇,并且觉得只有理解了一个框架的核心代码才能真正把一个框架用好才能写出最佳实践,那么这个教程对你会很有帮助。教程完整覆盖了`Laravel`核心的所有内容,并且根据开发者使用`Laravel`的通用场景开始逐步深入内核讲解整个框架核心流程中涉及到的方方面面,整个教程的目录顺序也是根据用`Laravel`进行开发时常涉及的部分编排的。相信你认真学完这个教程自己融汇贯通后就能完全掌握`Laravel`并胜任用它设计和架构生产系统的职责。 8 | ## 面向的人群 9 | 10 | 要想很好地理解文章的内容你需要具备一定的`PHP`基础和`Laravel`的知识,我并不会解释核心里的每一行代码,更多的是通过梳理代码流程来解释`Laravel`核心模块里最典型功能的设计思路和具体实现。所以我希望读者可以将文章内容看作是源代码的导读,跟随文章自己逐步地去看一遍`Laravel`每个核心组件的代码,如果遇到理解起来比较困难的地方就去补齐那里用到的知识再来继续阅读,我也希望读者在理解了文章里说的那些典型功能后能够自己再去举一三地看看模块里其他功能的源代码。相信看完`Laravel`核心的代码后你不仅能更熟练地使用`Laravel`也能在其它基础知识方面有所提高。 11 | 12 | ## 涉及的内容 13 | 14 | 文章主要专注于`Laravel`核心的学习,包括:服务容器、服务提供器、中间件、路由、`Facades`、事件驱动系统、Auth用户认证系统以及作为核心服务的`Database`、`Request`、`Response`、`Cookie`和`Session`。`Laravel`里其它的部分也都是作为服务注册到服务容器里提供给应用使用的,当你理解了上面那些东西后再去看其它的服务也就会很容易理解了。在学习源码的过程中我会向读者解释关于这些核心模块的常见问题比如:使用`DB`或者`Model`操作数据库时`Laravel`是什么时候连接上数据库的? 注册到容器的服务是怎么被解析出来的等等。 15 | 16 | ## 关于框架版本 17 | 在通过这个项目学习`Laravel`核心代码时请使用`Laravel5.5`版本,由于服务容器和中间件两篇文章成稿比较早那会还在使用5.2版本的Laravel做项目所以引用的代码也来自5.2版本,其余章节的代码均引用自`Laravel5.5`的核心,两个版本的核心代码差异很小我已经在这两篇文章中标注出差异的地方所以不影响读者使用这个项目来学习`Laravel5.5`版本的核心代码。 18 | 19 | ## Contact 20 | - [Open an issue](https://github.com/kevinyan815/Learning_Laravel_Kernel/issues) 21 | - Gmail: kevinyan815@gmail.com 22 | - 公众号: "网管叨bi叨" 23 | 24 | ![](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/images/WX20200119-143845%402x.png) 25 | 26 | ## 文章目录 27 | 28 | - [类的反射和依赖注入](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/reflection.md) 29 | - [服务容器(IocContainer)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/IocContainer.md) 30 | - [服务提供器(ServiceProvider)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ServiceProvider.md) 31 | - [外观模式(Facade Pattern)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/FacadePattern.md) 32 | - [Facades](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Facades.md) 33 | - [路由](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Route.md) 34 | - [装饰模式(Decorator Pattern)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/DecoratorPattern.md) 35 | - [中间件](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Middleware.md) 36 | - [控制器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Controller.md) 37 | - [Request](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Request.md) 38 | - [Response](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Response.md) 39 | - [Database 基础介绍](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database1.md) 40 | - [Database 查询构建器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database2.md) 41 | - [Database 模型CRUD](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database3.md) 42 | - [Database 模型关联](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database4.md) 43 | - [观察者模式](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Observer.md) 44 | - [事件系统](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Event.md) 45 | - [用户认证系统(基础介绍)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Auth1.md) 46 | - [用户认证系统(实现细节)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Auth2.md) 47 | - [扩展用户认证系统](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Auth3.md) 48 | - [Session源码解析](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Session.md) 49 | - [Cookie源码解析](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Cookie.md) 50 | - [Contracts契约](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Contracts.md) 51 | - [加载和读取ENV配置](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ENV.md) 52 | - [HTTP内核](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/HttpKernel.md) 53 | - [Console内核](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ConsoleKernel.md) 54 | - [异常处理](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Exception.md) 55 | - [结束语](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Farewell.md) 56 | 57 | 58 | 59 | ## 其他推荐 60 | - [Go Web编程入门指南](https://github.com/go-study-lab/go-http-server) 61 | - [Laravel最佳实践](https://github.com/kevinyan815/laravel_best_practices_cn) 62 | - [我的Golang开发手记](https://github.com/kevinyan815/gocookbook) 63 | 64 | **另外最近我推出了自己的Go实战专栏课程,专栏配套一个专属的私有项目,通过tag版本追踪记录每个章节代码的变更,让大家能轻松跟上学习**。 65 | 66 | **专栏分为五大部分**: 67 | 68 | image 69 | 70 | **访问:https://xiaobot.net/p/golang 或者扫码下方海报二维码可查看课程详情** 71 | 72 | image 73 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /articles/Auth1.md: -------------------------------------------------------------------------------- 1 | # 用户认证系统(基础介绍) 2 | 3 | 使用过Laravel的开发者都知道,Laravel自带了一个认证系统来提供基本的用户注册、登录、认证、找回密码,如果Auth系统里提供的基础功能不满足需求还可以很方便的在这些基础功能上进行扩展。这篇文章我们先来了解一下Laravel Auth系统的核心组件。 4 | 5 | Auth系统的核心是由 Laravel 的认证组件的「看守器」和「提供器」组成。看守器定义了该如何认证每个请求中用户。例如,Laravel 自带的 session 看守器会使用 session 存储和 cookies 来维护状态。 6 | 7 | 下表列出了Laravel Auth系统的核心部件 8 | 9 | | 名称 | 作用 | 10 | | ------------- | ------------------------------------------------------------ | 11 | | Auth | AuthManager的Facade | 12 | | AuthManager | Auth认证系统面向外部的接口,认证系统通过它向应用提供所有与用户认证相关的功能| 13 | | Guard | 看守器,定义了该如何认证每个请求中用户 | 14 | | User Provider | 用户提供器,定义了如何从持久化的存储数据中检索用户 | 15 | 16 | 在本文中我们会详细介绍这些核心部件,然后在文章的最后更新每个部件的作用细节到上面给出的这个表中。 17 | 18 | 19 | 20 | ### 开始使用Auth系统 21 | 22 | 只需在新的 Laravel 应用上运行 `php artisan make:auth` 和 `php artisan migrate` 命令就能够在项目里生成Auth系统需要的路由和视图以及数据表。 23 | 24 | `php artisan make:auth`执行后会生成Auth认证系统需要的视图文件,此外还会在路由文件`web.php`中增加响应的路由: 25 | 26 | ``` 27 | Auth::routes(); 28 | ``` 29 | 30 | `Auth` Facade文件中单独定义了`routes`这个静态方法 31 | 32 | ``` 33 | public static function routes() 34 | { 35 | static::$app->make('router')->auth(); 36 | } 37 | ``` 38 | 39 | 所以Auth具体的路由方法都定义在`Illuminate\Routing\Router`的`auth`方法中,关于如何找到Facade类代理的实际类可以翻看之前Facade源码分析的章节。 40 | 41 | 42 | 43 | ``` 44 | namespace Illuminate\Routing; 45 | class Router implements RegistrarContract, BindingRegistrar 46 | { 47 | /** 48 | * Register the typical authentication routes for an application. 49 | * 50 | * @return void 51 | */ 52 | public function auth() 53 | { 54 | // Authentication Routes... 55 | $this->get('login', 'Auth\LoginController@showLoginForm')->name('login'); 56 | $this->post('login', 'Auth\LoginController@login'); 57 | $this->post('logout', 'Auth\LoginController@logout')->name('logout'); 58 | 59 | // Registration Routes... 60 | $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register'); 61 | $this->post('register', 'Auth\RegisterController@register'); 62 | 63 | // Password Reset Routes... 64 | $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request'); 65 | $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email'); 66 | $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset'); 67 | $this->post('password/reset', 'Auth\ResetPasswordController@reset'); 68 | } 69 | } 70 | ``` 71 | 72 | 在`auth`方法里可以清晰的看到认证系统里提供的所有功能的路由URI以及对应的控制器和方法。 73 | 74 | 75 | 76 | 使用Laravel的认证系统,几乎所有东西都已经为你配置好了。其配置文件位于 `config/auth.php`,其中包含了用于调整认证服务行为的注释清晰的选项配置。 77 | 78 | ``` 79 | [ 93 | 'guard' => 'web', 94 | 'passwords' => 'users', 95 | ], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Authentication Guards 100 | |-------------------------------------------------------------------------- 101 | | 102 | | 定义项目使用的认证看守器,默认的看守器使用session驱动和Eloquent User 用户数据提供者 103 | | 104 | | 所有的驱动都有一个用户提供者,它定义了如何从数据库或者应用使用的持久化用户数据的存储中取出用户信息 105 | | 106 | | Supported: "session", "token" 107 | | 108 | */ 109 | 110 | 'guards' => [ 111 | 'web' => [ 112 | 'driver' => 'session', 113 | 'provider' => 'users', 114 | ], 115 | 116 | 'api' => [ 117 | 'driver' => 'token', 118 | 'provider' => 'users', 119 | ], 120 | ], 121 | 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | User Providers 125 | |-------------------------------------------------------------------------- 126 | | 127 | | 所有的驱动都有一个用户提供者,它定义了如何从数据库或者应用使用的持久化用户数据的存储中取出用户信息 128 | | 129 | | Laravel支持通过不同的Guard来认证用户,这里可以定义Guard的用户数据提供者的细节: 130 | | 使用什么driver以及对应的Model或者table是什么 131 | | 132 | | Supported: "database", "eloquent" 133 | | 134 | */ 135 | 136 | 'providers' => [ 137 | 'users' => [ 138 | 'driver' => 'eloquent', 139 | 'model' => App\Models\User::class, 140 | ], 141 | 142 | // 'users' => [ 143 | // 'driver' => 'database', 144 | // 'table' => 'users', 145 | // ], 146 | ], 147 | 148 | /* 149 | |-------------------------------------------------------------------------- 150 | | 重置密码相关的配置 151 | |-------------------------------------------------------------------------- 152 | | 153 | */ 154 | 155 | 'passwords' => [ 156 | 'users' => [ 157 | 'provider' => 'users', 158 | 'table' => 'password_resets', 159 | 'expire' => 60, 160 | ], 161 | ], 162 | 163 | ]; 164 | ``` 165 | 166 | Auth系统的核心是由 Laravel 的认证组件的「看守器」和「提供器」组成。看守器定义了该如何认证每个请求中用户。例如,Laravel 自带的 `session` 看守器会使用 session 存储和 cookies 来维护状态。 167 | 168 | 提供器中定义了该如何从持久化的存储数据中检索用户。Laravel 自带支持使用 Eloquent 和数据库查询构造器来检索用户。当然,你可以根据需要自定义其他提供器。 169 | 170 | 所以上面的配置文件的意思是Laravel认证系统默认使用了web guard配置项, 配置项里使用的是看守器是SessionGuard,使用的用户提供器是`EloquentProvider` 提供器使用的model是`App\User`。 171 | 172 | ### Guard 173 | 174 | 看守器定义了该如何认证每个请求中的用户。Laravel自带的认证系统默认使用自带的 `SessionGuard` ,`SessionGuard`除了实现`\Illuminate\Contracts\Auth\Guard`契约里的方法还实现`Illuminate\Contracts\Auth\StatefulGuard` 和`Illuminate\Contracts\Auth\SupportsBasicAuth`契约里的方法,这些Guard Contracts里定义的方法都是Laravel Auth系统默认认证方式依赖的基础方法。 175 | 176 | 我们先来看一下这一些基础方法都意欲完成什么操作,等到分析Laravel是如何通过SessionGuard认证用户时在去关系这些方法的具体实现。 177 | 178 | #### Illuminate\Contracts\Auth\Guard 179 | 180 | 这个文件定义了基础的认证方法 181 | 182 | ``` 183 | namespace Illuminate\Contracts\Auth; 184 | 185 | interface Guard 186 | { 187 | /** 188 | * 返回当前用户是否时已通过认证,是返回true,否者返回false 189 | * 190 | * @return bool 191 | */ 192 | public function check(); 193 | 194 | /** 195 | * 验证是否时访客用户(非登录认证通过的用户) 196 | * 197 | * @return bool 198 | */ 199 | public function guest(); 200 | 201 | /** 202 | * 获取当前用户的用户信息数据,获取成功返回用户User模型实例(\App\User实现了Authenticatable接口) 203 | * 失败返回null 204 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 205 | */ 206 | public function user(); 207 | 208 | /** 209 | * 获取当前认证用户的用户ID,成功返回ID值,失败返回null 210 | * 211 | * @return int|null 212 | */ 213 | public function id(); 214 | 215 | /** 216 | * 通过credentials(一般是邮箱和密码)验证用户 217 | * 218 | * @param array $credentials 219 | * @return bool 220 | */ 221 | public function validate(array $credentials = []); 222 | 223 | /** 224 | * 将一个\App\User实例设置成当前的认证用户 225 | * 226 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 227 | * @return void 228 | */ 229 | public function setUser(Authenticatable $user); 230 | } 231 | 232 | ``` 233 | 234 | #### Illuminate\Contracts\Auth\StatefulGuard 235 | 236 | 这个Contracts定义了Laravel auth系统里认证用户时使用的方法,除了认证用户外还会涉及用户认证成功后如何持久化用户的认证状态。 237 | 238 | ``` 239 | getDefaultDriver(); 24 | 25 | return isset($this->guards[$name]) 26 | ? $this->guards[$name] 27 | : $this->guards[$name] = $this->resolve($name); 28 | } 29 | 30 | /** 31 | * 解析出给定name的Guard 32 | * 33 | * @param string $name 34 | * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard 35 | * 36 | * @throws \InvalidArgumentException 37 | */ 38 | protected function resolve($name) 39 | { 40 | //获取Guard的配置 41 | //$config = ['driver' => 'session', 'provider' => 'users'] 42 | $config = $this->getConfig($name); 43 | 44 | if (is_null($config)) { 45 | throw new InvalidArgumentException("Auth guard [{$name}] is not defined."); 46 | } 47 | //如果通过extend方法为guard定义了驱动器,这里去调用自定义的Guard驱动器 48 | if (isset($this->customCreators[$config['driver']])) { 49 | return $this->callCustomCreator($name, $config); 50 | } 51 | //Laravel auth默认的配置这里是执行createSessionDriver 52 | $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; 53 | 54 | if (method_exists($this, $driverMethod)) { 55 | return $this->{$driverMethod}($name, $config); 56 | } 57 | 58 | throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined."); 59 | } 60 | 61 | /** 62 | * 从config/auth.php中获取给定名称的Guard的配置 63 | * 64 | * @param string $name 65 | * @return array 66 | */ 67 | protected function getConfig($name) 68 | { 69 | //'guards' => [ 70 | // 'web' => [ 71 | // 'driver' => 'session', 72 | // 'provider' => 'users', 73 | // ], 74 | 75 | // 'api' => [ 76 | // 'driver' => 'token', 77 | // 'provider' => 'users', 78 | // ], 79 | //], 80 | // 根据Laravel默认的auth配置, 这个方法会获取key "web"对应的数组 81 | return $this->app['config']["auth.guards.{$name}"]; 82 | } 83 | 84 | /** 85 | * 调用自定义的Guard驱动器 86 | * 87 | * @param string $name 88 | * @param array $config 89 | * @return mixed 90 | */ 91 | protected function callCustomCreator($name, array $config) 92 | { 93 | return $this->customCreators[$config['driver']]($this->app, $name, $config); 94 | } 95 | 96 | /** 97 | * 注册一个自定义的闭包Guard 驱动器 到customCreators属性中 98 | * 99 | * @param string $driver 100 | * @param \Closure $callback 101 | * @return $this 102 | */ 103 | public function extend($driver, Closure $callback) 104 | { 105 | $this->customCreators[$driver] = $callback; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * 注册一个自定义的用户提供器创建器到 customProviderCreators属性中 112 | * 113 | * @param string $name 114 | * @param \Closure $callback 115 | * @return $this 116 | */ 117 | public function provider($name, Closure $callback) 118 | { 119 | $this->customProviderCreators[$name] = $callback; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * 创建基于session的认证看守器 SessionGuard 126 | * 127 | * @param string $name 128 | * @param array $config 129 | * @return \Illuminate\Auth\SessionGuard 130 | */ 131 | public function createSessionDriver($name, $config) 132 | { 133 | //$config['provider'] == 'users' 134 | $provider = $this->createUserProvider($config['provider'] ?? null); 135 | 136 | $guard = new SessionGuard($name, $provider, $this->app['session.store']); 137 | 138 | if (method_exists($guard, 'setCookieJar')) { 139 | $guard->setCookieJar($this->app['cookie']); 140 | } 141 | 142 | if (method_exists($guard, 'setDispatcher')) { 143 | $guard->setDispatcher($this->app['events']); 144 | } 145 | 146 | if (method_exists($guard, 'setRequest')) { 147 | $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); 148 | } 149 | 150 | return $guard; 151 | } 152 | 153 | //创建Guard驱动依赖的用户提供器对象 154 | public function createUserProvider($provider = null) 155 | { 156 | if (is_null($config = $this->getProviderConfiguration($provider))) { 157 | return; 158 | } 159 | //如果通过Auth::provider方法注册了自定义的用户提供器creator闭包则去调用闭包获取用户提供器对象 160 | if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) { 161 | return call_user_func( 162 | $this->customProviderCreators[$driver], $this->app, $config 163 | ); 164 | } 165 | 166 | switch ($driver) { 167 | case 'database': 168 | return $this->createDatabaseProvider($config); 169 | case 'eloquent': 170 | //通过默认的auth配置这里会返回EloquentUserProvider对象,它实现了Illuminate\Contracts\Auth 接口 171 | return $this->createEloquentProvider($config); 172 | default: 173 | throw new InvalidArgumentException( 174 | "Authentication user provider [{$driver}] is not defined." 175 | ); 176 | } 177 | } 178 | 179 | /** 180 | * 会通过__call去动态地调用AuthManager代理的Guard的用户认证相关方法 181 | * 根据默认配置,这里__call会去调用SessionGuard里的方法 182 | * @param string $method 183 | * @param array $parameters 184 | * @return mixed 185 | */ 186 | public function __call($method, $parameters) 187 | { 188 | return $this->guard()->{$method}(...$parameters); 189 | } 190 | } 191 | 192 | 193 | ``` 194 | 195 | ### 用户注册 196 | 197 | Laravel Auth系统中默认的注册路由如下: 198 | 199 | ``` 200 | $this->post('register', 'Auth\RegisterController@register'); 201 | ``` 202 | 203 | 所以用户注册的逻辑是由RegisterController的register方法来完成的 204 | 205 | ``` 206 | class RegisterController extends Controller 207 | { 208 | //方法定义在Illuminate\Foundation\Auth\RegisterUsers中 209 | public function register(Request $request) 210 | { 211 | $this->validator($request->all())->validate(); 212 | 213 | event(new Registered($user = $this->create($request->all()))); 214 | 215 | $this->guard()->login($user); 216 | 217 | return $this->registered($request, $user) 218 | 219 | } 220 | 221 | protected function validator(array $data) 222 | { 223 | return Validator::make($data, [ 224 | 'name' => 'required|string|max:255', 225 | 'email' => 'required|string|email|max:255|unique:users', 226 | 'password' => 'required|string|min:6|confirmed', 227 | ]); 228 | } 229 | 230 | protected function create(array $data) 231 | { 232 | return User::create([ 233 | 'name' => $data['name'], 234 | 'email' => $data['email'], 235 | 'password' => bcrypt($data['password']), 236 | ]); 237 | } 238 | 239 | } 240 | ``` 241 | 242 | register的流程很简单,就是验证用户输入的数据没问题后将这些数据写入数据库生成用户,其中密码加密采用的是bcrypt算法,如果你需要改成常用的salt加密码明文做哈希的密码加密方法可以在create方法中对这部分逻辑进行更改,注册完用户后会调用SessionGuard的login方法把用户数据装载到应用中,注意这个login方法没有登录认证,只是把认证后的用户装载到应用中这样在应用里任何地方我们都能够通过`Auth::user()`来获取用户数据啦。 243 | 244 | 245 | 246 | ### 用户登录认证 247 | 248 | Laravel Auth系统的登录路由如下 249 | 250 | ``` 251 | $this->post('login', 'Auth\LoginController@login'); 252 | ``` 253 | 254 | 我们看一下LoginController里的登录逻辑 255 | 256 | ``` 257 | class LoginController extends Controller 258 | { 259 | /** 260 | * 处理登录请求 261 | */ 262 | public function login(Request $request) 263 | { 264 | //验证登录字段 265 | $this->validateLogin($request); 266 | //防止恶意的多次登录尝试 267 | if ($this->hasTooManyLoginAttempts($request)) { 268 | $this->fireLockoutEvent($request); 269 | 270 | return $this->sendLockoutResponse($request); 271 | } 272 | //进行登录认证 273 | if ($this->attemptLogin($request)) { 274 | return $this->sendLoginResponse($request); 275 | } 276 | 277 | $this->incrementLoginAttempts($request); 278 | 279 | return $this->sendFailedLoginResponse($request); 280 | } 281 | 282 | //尝试进行登录认证 283 | protected function attemptLogin(Request $request) 284 | { 285 | return $this->guard()->attempt( 286 | $this->credentials($request), $request->filled('remember') 287 | ); 288 | } 289 | 290 | //获取登录用的字段值 291 | protected function credentials(Request $request) 292 | { 293 | return $request->only($this->username(), 'password'); 294 | } 295 | } 296 | ``` 297 | 298 | 可以看到,登录认证的逻辑是通过`SessionGuard`的`attempt`方法来实现的,其实就是`Auth::attempt()`, 下面我们来看看`attempt`方法里的逻辑: 299 | 300 | ``` 301 | class SessionGuard implements StatefulGuard, SupportsBasicAuth 302 | { 303 | public function attempt(array $credentials = [], $remember = false) 304 | { 305 | $this->fireAttemptEvent($credentials, $remember); 306 | 307 | $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); 308 | //如果登录认证通过,通过login方法将用户对象装载到应用里去 309 | if ($this->hasValidCredentials($user, $credentials)) { 310 | $this->login($user, $remember); 311 | 312 | return true; 313 | } 314 | //登录失败的话,可以触发事件通知用户有可疑的登录尝试(需要自己定义listener来实现) 315 | $this->fireFailedEvent($user, $credentials); 316 | 317 | return false; 318 | } 319 | 320 | protected function hasValidCredentials($user, $credentials) 321 | { 322 | return ! is_null($user) && $this->provider->validateCredentials($user, $credentials); 323 | } 324 | } 325 | ``` 326 | 327 | `SessionGuard`的`attempt`方法首先通过用户提供器的`retriveBycredentials`方法通过用户名从用户表中查询出用户数据,认证用户信息是通过用户提供器的`validateCredentials`来实现的,所有用户提供器的实现类都会实现UserProvider契约(interface)中定义的方法,通过上面的分析我们知道默认的用户提供器是`EloquentUserProvider` 328 | 329 | 330 | 331 | ``` 332 | class EloquentUserProvider implements UserProvider 333 | { 334 | 从数据库中取出用户实例 335 | public function retrieveByCredentials(array $credentials) 336 | { 337 | if (empty($credentials) || 338 | (count($credentials) === 1 && 339 | array_key_exists('password', $credentials))) { 340 | return; 341 | } 342 | 343 | $query = $this->createModel()->newQuery(); 344 | 345 | foreach ($credentials as $key => $value) { 346 | if (! Str::contains($key, 'password')) { 347 | $query->where($key, $value); 348 | } 349 | } 350 | 351 | return $query->first(); 352 | } 353 | 354 | //通过给定用户认证数据来验证用户 355 | public function validateCredentials(UserContract $user, array $credentials) 356 | { 357 | $plain = $credentials['password']; 358 | 359 | return $this->hasher->check($plain, $user->getAuthPassword()); 360 | } 361 | } 362 | 363 | class BcryptHasher implements HasherContract 364 | { 365 | //通过bcrypt算法计算给定value的散列值 366 | public function make($value, array $options = []) 367 | { 368 | $hash = password_hash($value, PASSWORD_BCRYPT, [ 369 | 'cost' => $this->cost($options), 370 | ]); 371 | 372 | if ($hash === false) { 373 | throw new RuntimeException('Bcrypt hashing not supported.'); 374 | } 375 | 376 | return $hash; 377 | } 378 | 379 | //验证散列值是否给定明文值通过bcrypt算法计算得到的 380 | public function check($value, $hashedValue, array $options = []) 381 | { 382 | if (strlen($hashedValue) === 0) { 383 | return false; 384 | } 385 | 386 | return password_verify($value, $hashedValue); 387 | } 388 | } 389 | ``` 390 | 391 | 用户密码的验证是通过`EloquentUserProvider`依赖的`hasher`哈希器来完成的,Laravel认证系统默认采用bcrypt算法来加密用户提供的明文密码然后存储到用户表里的,验证时`haser`哈希器的`check`方法会通过PHP内建方法`password_verify`来验证明文密码是否是存储的密文密码的原值。 392 | 393 | 394 | 395 | 用户认证系统的主要细节梳理完后我们就知道如何定义我们自己的看守器(Guard)或用户提供器(UserProvider)了,首先他们必须实现各自遵守的契约里的方法才能够无缝接入到Laravel的Auth系统中,然后还需要将自己定义的Guard或Provider通过`Auth::extend`、`Auth::provider`方法注册返回Guard或者Provider实例的闭包到Laravel中去,Guard和UserProvider的自定义不是必须成套的,我们可以单独自定义Guard仍使用默认的EloquentUserProvider,或者让默认的SessionGuard使用自定义的UserProvider。 396 | 397 | 下一节我会给出一个我们以前项目开发中用到的一个案例来更好地讲解应该如何对Laravel Auth系统进行扩展。 398 | 399 | 400 | 401 | 上一篇: [用户认证系统(基础介绍)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Auth1.md) 402 | 403 | 下一篇: [扩展用户认证系统](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Auth3.md) 404 | 405 | -------------------------------------------------------------------------------- /articles/Auth3.md: -------------------------------------------------------------------------------- 1 | # 扩展用户认证系统 2 | 3 | 4 | 5 | 上一节我们介绍了Laravel Auth系统实现的一些细节知道了Laravel是如何应用看守器和用户提供器来进行用户认证的,但是针对我们自己开发的项目或多或少地我们都会需要在自带的看守器和用户提供器基础之上做一些定制化来适应项目,本节我会列举一个在做项目时遇到的具体案例,在这个案例中用自定义的看守器和用户提供器来扩展了Laravel的用户认证系统让它能更适用于我们自己开发的项目。 6 | 7 | 8 | 9 | 在介绍用户认证系统基础的时候提到过Laravel自带的注册和登录验证用户密码时都是去验证采用`bcypt`加密存储的密码,但是很多已经存在的老系统中用户密码都是用盐值加明文密码做哈希后存储的,如果想要在这种老系统中应用Laravel开发项目的话那么我们就不能够再使用Laravel自带的登录和注册方法了,下面我们就通过实例看看应该如何扩展Laravel的用户认证系统让它能够满足我们项目的认证需求。 10 | 11 | 12 | 13 | ### 修改用户注册 14 | 15 | 首先我们将用户注册时,用户密码的加密存储的方式由`bcypt`加密后存储改为由盐值与明文密码做哈希后再存储的方式。这个非常简单,上一节已经说过Laravel自带的用户注册方法是怎么实现了,这里我们直接将`\App\Http\Controllers\Auth\RegisterController`中的`create`方法修改为如下: 16 | 17 | ``` 18 | /** 19 | * Create a new user instance after a valid registration. 20 | * 21 | * @param array $data 22 | * @return User 23 | */ 24 | protected function create(array $data) 25 | { 26 | $salt = Str::random(6); 27 | return User::create([ 28 | 'email' => $data['email'], 29 | 'password' => sha1($salt . $data['password']), 30 | 'register_time' => time(), 31 | 'register_ip' => ip2long(request()->ip()), 32 | 'salt' => $salt 33 | ]); 34 | } 35 | ``` 36 | 37 | 上面改完后注册用户后就能按照我们指定的方式来存储用户数据了,还有其他一些需要的与用户信息相关的字段也需要存储到用户表中去这里就不再赘述了。 38 | 39 | ### 修改用户登录 40 | 41 | 上节分析Laravel默认登录的实现细节时有说登录认证的逻辑是通过`SessionGuard`的`attempt`方法来实现的,在`attempt`方法中`SessionGuard`通过`EloquentUserProvider`的`retriveBycredentials`方法从用户表中查询出用户数据,通过 `validateCredentials`方法来验证给定的用户认证数据与从用户表中查询出来的用户数据是否吻合。 42 | 43 | 下面直接给出之前讲这块时引用的代码块: 44 | 45 | ``` 46 | class SessionGuard implements StatefulGuard, SupportsBasicAuth 47 | { 48 | public function attempt(array $credentials = [], $remember = false) 49 | { 50 | $this->fireAttemptEvent($credentials, $remember); 51 | 52 | $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); 53 | //如果登录认证通过,通过login方法将用户对象装载到应用里去 54 | if ($this->hasValidCredentials($user, $credentials)) { 55 | $this->login($user, $remember); 56 | 57 | return true; 58 | } 59 | //登录失败的话,可以触发事件通知用户有可疑的登录尝试(需要自己定义listener来实现) 60 | $this->fireFailedEvent($user, $credentials); 61 | 62 | return false; 63 | } 64 | 65 | protected function hasValidCredentials($user, $credentials) 66 | { 67 | return ! is_null($user) && $this->provider->validateCredentials($user, $credentials); 68 | } 69 | } 70 | 71 | class EloquentUserProvider implements UserProvider 72 | { 73 | 从数据库中取出用户实例 74 | public function retrieveByCredentials(array $credentials) 75 | { 76 | if (empty($credentials) || 77 | (count($credentials) === 1 && 78 | array_key_exists('password', $credentials))) { 79 | return; 80 | } 81 | 82 | $query = $this->createModel()->newQuery(); 83 | 84 | foreach ($credentials as $key => $value) { 85 | if (! Str::contains($key, 'password')) { 86 | $query->where($key, $value); 87 | } 88 | } 89 | 90 | return $query->first(); 91 | } 92 | 93 | //通过给定用户认证数据来验证用户 94 | /** 95 | * Validate a user against the given credentials. 96 | * 97 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 98 | * @param array $credentials 99 | * @return bool 100 | */ 101 | public function validateCredentials(UserContract $user, array $credentials) 102 | { 103 | $plain = $credentials['password']; 104 | 105 | return $this->hasher->check($plain, $user->getAuthPassword()); 106 | } 107 | } 108 | ``` 109 | 110 | ### 自定义用户提供器 111 | 112 | 好了, 看到这里就很明显了, 我们需要改成自己的密码验证就是自己实现一下`validateCredentials`就可以了, 修改`$this->hasher->check`为我们自己的密码验证规则。 113 | 114 | 首先我们来重写`$user->getAuthPassword();` 在User模型中覆盖其从父类中继承来的这个方法,把数据库中用户表的`salt`和`password`传递到`validateCredentials`中来: 115 | 116 | ``` 117 | class user extends Authenticatable 118 | { 119 | /** 120 | * 覆盖Laravel中默认的getAuthPassword方法, 返回用户的password和salt字段 121 | * @return array 122 | */ 123 | public function getAuthPassword() 124 | { 125 | return ['password' => $this->attributes['password'], 'salt' => $this->attributes['salt']]; 126 | } 127 | } 128 | ``` 129 | 130 | 然后我们用一个自定义的用户提供器,通过它的`validateCredentials`来实现我们自己系统的密码验证规则,由于用户提供器的其它方法不用改变沿用`EloquentUserProvider`里的实现就可以,所以我们让自定义的用户提供器继承自`EloquentUserProvider`: 131 | 132 | ``` 133 | namespace App\Foundation\Auth; 134 | 135 | use Illuminate\Auth\EloquentUserProvider; 136 | use Illuminate\Contracts\Auth\Authenticatable; 137 | use Illuminate\Support\Str; 138 | 139 | class CustomEloquentUserProvider extends EloquentUserProvider 140 | { 141 | 142 | /** 143 | * Validate a user against the given credentials. 144 | * 145 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 146 | * @param array $credentials 147 | */ 148 | public function validateCredentials(Authenticatable $user, array $credentials) 149 | { 150 | $plain = $credentials['password']; 151 | $authPassword = $user->getAuthPassword(); 152 | 153 | return sha1($authPassword['salt'] . $plain) == $authPassword['password']; 154 | } 155 | } 156 | ``` 157 | 158 | 接下来通过`Auth::provider()`将`CustomEloquentUserProvider`注册到Laravel系统中,`Auth::provider`方法将一个返回用户提供器对象的闭包作为用户提供器创建器以给定名称注册到Laravel中,代码如下: 159 | 160 | ``` 161 | class AppServiceProvider extends ServiceProvider 162 | { 163 | /** 164 | * Bootstrap any application services. 165 | * 166 | * @return void 167 | */ 168 | public function boot() 169 | { 170 | \Auth::provider('custom-eloquent', function ($app, $config) { 171 | return New \App\Foundation\Auth\CustomEloquentUserProvider($app['hash'], $config['model']); 172 | }); 173 | } 174 | ...... 175 | } 176 | ``` 177 | 178 | 注册完用户提供器后我们就可以在`config/auth.php`里配置让看守器使用新注册的`custom-eloquent`作为用户提供器了: 179 | 180 | ``` 181 | //config/auth.php 182 | 'providers' => [ 183 | 'users' => [ 184 | 'driver' => 'coustom-eloquent', 185 | 'model' => \App\User::class, 186 | ] 187 | ] 188 | ``` 189 | 190 | ### 自定义认证看守器 191 | 192 | 好了,现在密码认证已经修改过来了,现在用户认证使用的看守器还是`SessionGuard`, 在系统中会有对外提供API的模块,在这种情形下我们一般希望用户登录认证后会返回给客户端一个JSON WEB TOKEN,每次调用接口时候通过这个token来认证请求接口的是否是有效用户,这个需求需要我们通过自定义的Guard扩展功能来完成,有个`composer`包`"tymon/jwt-auth": "dev-develop"`, 他的1.0beta版本带的`JwtGuard`是一个实现了`Illuminate\Contracts\Auth\Guard`的看守器完全符合我上面说的要求,所以我们就通过`Auth::extend()`方法将`JwtGuard`注册到系统中去: 193 | 194 | ``` 195 | class AppServiceProvider extends ServiceProvider 196 | { 197 | /** 198 | * Bootstrap any application services. 199 | * 200 | * @return void 201 | */ 202 | public function boot() 203 | { 204 | \Auth::provider('custom-eloquent', function ($app, $config) { 205 | return New \App\Foundation\Auth\CustomEloquentUserProvider($app['hash'], $config['model']); 206 | }); 207 | 208 | \Auth::extend('jwt', function ($app, $name, array $config) { 209 | // 返回一个 Illuminate\Contracts\Auth\Guard 实例... 210 | return new \Tymon\JWTAuth\JwtGuard(\Auth::createUserProvider($config['provider'])); 211 | }); 212 | } 213 | ...... 214 | } 215 | ``` 216 | 217 | 218 | 219 | 定义完之后,将 `auth.php` 配置文件的`guards`配置修改如下: 220 | 221 | ``` 222 | 'guards' => [ 223 | 'web' => [ 224 | 'driver' => 'session', 225 | 'provider' => 'users', 226 | ], 227 | 228 | 'api' => [ 229 | 'driver' => 'jwt', // token ==> jwt 230 | 'provider' => 'users', 231 | ], 232 | ], 233 | ``` 234 | 235 | 接下来我们定义一个API使用的登录认证方法, 在认证中会使用上面注册的`jwt`看守器来完成认证,认证完成后会返回一个JSON WEB TOKEN给客户端 236 | 237 | ``` 238 | Route::post('apilogin', 'Auth\LoginController@apiLogin'); 239 | ``` 240 | 241 | ``` 242 | class LoginController extends Controller 243 | { 244 | public function apiLogin(Request $request) 245 | { 246 | ... 247 | 248 | if ($token = $this->guard('api')->attempt($credentials)) { 249 | $return['status_code'] = 200; 250 | $return['message'] = '登录成功'; 251 | $response = \Response::json($return); 252 | $response->headers->set('Authorization', 'Bearer '. $token); 253 | return $response; 254 | } 255 | 256 | ... 257 | } 258 | } 259 | ``` 260 | 261 | 262 | 263 | ### 总结 264 | 265 | 通过上面的例子我们讲解了如何通过自定义认证看守器和用户提供器扩展Laravel的用户认证系统,目的是让大家对Laravel的用户认证系统有一个更好的理解知道在Laravel系统默认自带的用户认证方式无法满足我们的需求时如何通过自定义这两个组件来扩展功能完成我们项目自己的认证需求。 266 | 267 | 上一篇: [用户认证系统(实现细节)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Auth2.md) 268 | 269 | 下一篇: [Session](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Session.md) 270 | -------------------------------------------------------------------------------- /articles/ConsoleKernel.md: -------------------------------------------------------------------------------- 1 | ### Console内核 2 | 3 | 上一篇文章我们介绍了Laravel的HTTP内核,详细概述了网络请求从进入应用到应用处理完请求返回HTTP响应整个生命周期中HTTP内核是如何调动Laravel各个核心组件来完成任务的。除了处理HTTP请求一个健壮的应用经常还会需要执行计划任务、异步队列这些。Laravel为了能让应用满足这些场景设计了`artisan`工具,通过`artisan`工具定义各种命令来满足非HTTP请求的各种场景,`artisan`命令通过Laravel的Console内核来完成对应用核心组件的调度来完成任务。 今天我们就来学习一下Laravel Console内核的核心代码。 4 | 5 | 6 | 7 | ### 内核绑定 8 | 9 | 跟HTTP内核一样,在应用初始化阶有一个内核绑定的过程,将Console内核注册到应用的服务容器里去,还是引用上一篇文章引用过的`bootstrap/app.php`里的代码 10 | 11 | ``` 12 | singleton( 20 | Illuminate\Contracts\Http\Kernel::class, 21 | App\Http\Kernel::class 22 | ); 23 | // console内核绑定 24 | $app->singleton( 25 | Illuminate\Contracts\Console\Kernel::class, 26 | App\Console\Kernel::class 27 | ); 28 | 29 | $app->singleton( 30 | Illuminate\Contracts\Debug\ExceptionHandler::class, 31 | App\Exceptions\Handler::class 32 | ); 33 | 34 | return $app; 35 | ``` 36 | 37 | 38 | 39 | Console内核 `\App\Console\Kernel`继承自`Illuminate\Foundation\Console`, 在Console内核中我们可以注册`artisan`命令和定义应用里要执行的计划任务。 40 | 41 | ``` 42 | /** 43 | * Define the application's command schedule. 44 | * 45 | * @param \Illuminate\Console\Scheduling\Schedule $schedule 46 | * @return void 47 | */ 48 | protected function schedule(Schedule $schedule) 49 | { 50 | // $schedule->command('inspire') 51 | // ->hourly(); 52 | } 53 | /** 54 | * Register the commands for the application. 55 | * 56 | * @return void 57 | */ 58 | protected function commands() 59 | { 60 | $this->load(__DIR__.'/Commands'); 61 | require base_path('routes/console.php'); 62 | } 63 | ``` 64 | 65 | 在实例化Console内核的时候,内核会定义应用的命令计划任务(shedule方法中定义的计划任务) 66 | 67 | ``` 68 | public function __construct(Application $app, Dispatcher $events) 69 | { 70 | if (! defined('ARTISAN_BINARY')) { 71 | define('ARTISAN_BINARY', 'artisan'); 72 | } 73 | 74 | $this->app = $app; 75 | $this->events = $events; 76 | 77 | $this->app->booted(function () { 78 | $this->defineConsoleSchedule(); 79 | }); 80 | } 81 | ``` 82 | 83 | 84 | 85 | ### 应用解析Console内核 86 | 87 | 查看`aritisan`文件的源码我们可以看到, 完成Console内核绑定的绑定后,接下来就会通过服务容器解析出console内核对象 88 | 89 | ``` 90 | $kernel = $app->make(Illuminate\Contracts\Console\Kernel::class); 91 | 92 | $status = $kernel->handle( 93 | $input = new Symfony\Component\Console\Input\ArgvInput, 94 | new Symfony\Component\Console\Output\ConsoleOutput 95 | ); 96 | ``` 97 | 98 | 99 | 100 | ### 执行命令任务 101 | 102 | 解析出Console内核对象后,接下来就要处理来自命令行的命令请求了, 我们都知道PHP是通过全局变量`$_SERVER['argv']`来接收所有的命令行输入的, 和命令行里执行shell脚本一样(在shell脚本里可以通过`$0`获取脚本文件名,`$1` `$2`这些依次获取后面传递给shell脚本的参数选项)索引0对应的是脚本文件名,接下来依次是命令行里传递给脚本的所有参数选项,所以在命令行里通过`artisan`脚本执行的命令,在`artisan`脚本中`$_SERVER['argv']`数组里索引0对应的永远是`artisan`这个字符串,命令行里后面的参数会依次对应到`$_SERVER['argv']`数组后续的元素里。 103 | 104 | 因为`artisan`命令的语法中可以指定命令参数选项、有的选项还可以指定实参,为了减少命令行输入参数解析的复杂度,Laravel使用了`Symfony\Component\Console\Input`对象来解析命令行里这些参数选项(shell脚本里其实也是一样,会通过shell函数getopts来解析各种格式的命令行参数输入),同样地Laravel使用了`Symfony\Component\Console\Output`对象来抽象化命令行的标准输出。 105 | 106 | #### 引导应用 107 | 108 | 在Console内核的`handle`方法里我们可以看到和HTTP内核处理请求前使用`bootstrapper`程序引用应用一样在开始处理命令任务之前也会有引导应用这一步操作 109 | 110 | 其父类 「Illuminate\Foundation\Console\Kernel」 内部定义了属性名为 「bootstrappers」 的 引导程序 数组: 111 | 112 | ``` 113 | protected $bootstrappers = [ 114 | \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, 115 | \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, 116 | \Illuminate\Foundation\Bootstrap\HandleExceptions::class, 117 | \Illuminate\Foundation\Bootstrap\RegisterFacades::class, 118 | \Illuminate\Foundation\Bootstrap\SetRequestForConsole::class, 119 | \Illuminate\Foundation\Bootstrap\RegisterProviders::class, 120 | \Illuminate\Foundation\Bootstrap\BootProviders::class, 121 | ]; 122 | ``` 123 | 124 | 数组中包括的引导程序基本上和HTTP内核中定义的引导程序一样, 都是应用在初始化阶段要进行的环境变量、配置文件加载、注册异常处理器、设置Console请求、注册应用中的服务容器、Facade和启动服务。其中设置Console请求是唯一区别于HTTP内核的一个引导程序。 125 | 126 | 127 | 128 | #### 执行命令 129 | 130 | 执行命令是通过Console Application来执行的,它继承自Symfony框架的`Symfony\Component\Console\Application`类, 通过对应的run方法来执行命令。 131 | 132 | ``` 133 | name Illuminate\Foundation\Console; 134 | class Kernel implements KernelContract 135 | { 136 | public function handle($input, $output = null) 137 | { 138 | try { 139 | $this->bootstrap(); 140 | 141 | return $this->getArtisan()->run($input, $output); 142 | } catch (Exception $e) { 143 | $this->reportException($e); 144 | 145 | $this->renderException($output, $e); 146 | 147 | return 1; 148 | } catch (Throwable $e) { 149 | $e = new FatalThrowableError($e); 150 | 151 | $this->reportException($e); 152 | 153 | $this->renderException($output, $e); 154 | 155 | return 1; 156 | } 157 | } 158 | } 159 | 160 | namespace Symfony\Component\Console; 161 | class Application 162 | { 163 | //执行命令 164 | public function run(InputInterface $input = null, OutputInterface $output = null) 165 | { 166 | ...... 167 | try { 168 | $exitCode = $this->doRun($input, $output); 169 | } catch { 170 | ...... 171 | } 172 | ...... 173 | return $exitCode; 174 | } 175 | 176 | public function doRun(InputInterface $input, OutputInterface $output) 177 | { 178 | //解析出命令名称 179 | $name = $this->getCommandName($input); 180 | 181 | //解析出入参 182 | if (!$name) { 183 | $name = $this->defaultCommand; 184 | $definition = $this->getDefinition(); 185 | $definition->setArguments(array_merge( 186 | $definition->getArguments(), 187 | array( 188 | 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name), 189 | ) 190 | )); 191 | } 192 | ...... 193 | try { 194 | //通过命令名称查找出命令类(命名空间、类名等) 195 | $command = $this->find($name); 196 | } 197 | ...... 198 | //运行命令类 199 | $exitCode = $this->doRunCommand($command, $input, $output); 200 | 201 | return $exitCode; 202 | } 203 | 204 | protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) 205 | { 206 | ...... 207 | //执行命令类的run方法来处理任务 208 | $exitCode = $command->run($input, $output); 209 | ...... 210 | 211 | return $exitcode; 212 | } 213 | } 214 | 215 | ``` 216 | 217 | 执行命令时主要有三步操作: 218 | 219 | - 通过命令行输入解析出命令名称和参数选项。 220 | 221 | - 通过命令名称查找命令类的命名空间和类名。 222 | - 执行命令类的`run`方法来完成任务处理并返回状态码。 223 | 224 | 和命令行脚本的规范一样,如果执行命令任务程序成功会返回0, 抛出异常退出则返回1。 225 | 226 | 还有就是打开命令类后我们可以看到并没有run方法,我们把处理逻辑都写在了`handle`方法中,仔细查看代码会发现`run`方法定义在父类中,在`run`方法会中会调用子类中定义的`handle`方法来完成任务处理。 严格遵循了面向对象程序设计的**SOLID **原则。 227 | 228 | ### 结束应用 229 | 230 | 执行完命令程序返回状态码后, 在`artisan`中会直接通过`exit($status)`函数输出状态码并结束PHP进程,接下来shell进程会根据返回的状态码是否为0来判断脚本命令是否执行成功。 231 | 232 | 233 | 234 | 到这里通过命令行开启的程序进程到这里就结束了,跟HTTP内核一样Console内核在整个生命周期中也是负责调度,只不过Http内核最终将请求落地到了`Controller`程序中而Console内核则是将命令行请求落地到了Laravel中定义的各种命令类程序中,然后在命令类里面我们就可以写其他程序一样自由地使用Laravel中的各个组件和注册到服务容器里的服务了。 235 | 236 | 上一篇: [HTTP内核](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/HttpKernel.md) 237 | 238 | 下一篇: [异常处理](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Exception.md) 239 | 240 | -------------------------------------------------------------------------------- /articles/Contracts.md: -------------------------------------------------------------------------------- 1 | # Contracts 2 | 3 | Laravel 的契约是一组定义框架提供的核心服务的接口, 例如我们在介绍用户认证的章节中到的用户看守器契约[Illumninate\Contracts\Auth\Guard](https://github.com/illuminate/contracts/blob/master/Auth/Guard.php) 和用户提供器契约[Illuminate\Contracts\Auth\UserProvider](https://github.com/illuminate/contracts/blob/master/Auth/UserProvider.php) 4 | 5 | 以及框架自带的`App\User`模型所实现的[Illuminate\Contracts\Auth\Authenticatable](https://github.com/illuminate/contracts/blob/master/Auth/Authenticatable.php)契约。 6 | 7 | ### 为什么使用契约 8 | 9 | 通过上面几个契约的源码文件我们可以看到,Laravel提供的契约是为核心模块定义的一组interface。Laravel为每个契约都提供了相应的实现类,下表列出了Laravel为上面提到的三个契约提供的实现类。 10 | 11 | | 契约 | Laravel内核提供的实现类 | 12 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 13 | | [Illumninate\Contracts\Auth\Guard](https://github.com/illuminate/contracts/blob/master/Auth/Guard.php) | [Illuminate\Auth\SessionGuard](https://github.com/illuminate/auth/blob/master/SessionGuard.php) | 14 | | [Illuminate\Contracts\Auth\UserProvider](https://github.com/illuminate/contracts/blob/master/Auth/UserProvider.php) | [Illuminate\Auth\EloquentUserProvider](https://github.com/illuminate/auth/blob/master/EloquentUserProvider.php) | 15 | | [Illuminate\Contracts\Auth\Authenticatable](https://github.com/illuminate/contracts/blob/master/Auth/Authenticatable.php) | Illuminate\Foundation\Auth\Authenticatable(User Model的父类) | 16 | 17 | 所以在自己开发的项目中,如果Laravel提供的用户认证系统无法满足需求,你可以根据需求定义看守器和用户提供器的实现类,比如我之前做的项目就是用户认证依赖于公司的员工管理系统的API,所以我就自己写了看守器和用户提供器契约的实现类,让Laravel通过自定义的Guard和UserProvider来完成用户认证。自定义用户认证的方法在介绍用户认证的章节中我们介绍过,读者可以去翻阅那块的文章。 18 | 19 | 20 | 21 | 所以Laravel为所有的核心功能都定义契约接口的目的就是为了让开发者能够根据自己项目的需要自己定义实现类,而对于这些接口的消费者(比如:Controller、或者内核提供的 AuthManager这些)他们不需要关心接口提供的方法具体是怎么实现的, 只关心接口的方法能提供什么功能然后去使用这些功能就可以了,我们可以根据需求在必要的时候为接口更换实现类,而消费端不用进行任何改动。 22 | 23 | ### 定义和使用契约 24 | 25 | 上面我们提到的都是Laravel内核提供的契约, 在开发大型项目的时候我们也可以自己在项目中定义契约和实现类,你有可能会觉得自带的Controller、Model两层就已经足够你编写代码了,凭空多出来契约和实现类会让开发变得繁琐。我们先从一个简单的例子出发,考虑下面的代码有什么问题: 26 | 27 | ``` 28 | class OrderController extends Controller 29 | { 30 | public function getUserOrders() 31 | { 32 | $orders= Order::where('user_id', '=', \Auth::user()->id)->get(); 33 | return View::make('order.index', compact('orders')); 34 | } 35 | } 36 | ``` 37 | 38 | 39 | 40 | 这段代码很简单,但我们要想测试这段代码的话就一定会和实际的数据库发生联系。也就是说, ORM和这个控制器有着紧耦合。如果不使用Eloquent ORM,不连接到实际数据库,我们就没办法运行或者测试这段代码。这段代码同时也违背了“关注分离”这个软件设计原则。简单讲:这个控制器知道的太多了。 控制器不需要去了解数据是从哪儿来的,只要知道如何访问就行。控制器也不需要知道这数据是从MySQL或哪儿来的,只需要知道这数据目前是可用的。 41 | 42 | >**Separation Of Concerns 关注分离** 43 | > 44 | >Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. 45 | > 46 | >每个类都应该只有单一的职责,并且职责里所有的东西都应该由这个类封装 47 | 48 | 接下来我们定义一个接口,然后实现该接口 49 | 50 | ``` 51 | interface OrderRepositoryInterface 52 | { 53 | public function userOrders(User $user); 54 | } 55 | 56 | class OrderRepository implements OrderRepositoryInterface 57 | { 58 | public function userOrders(User $user) 59 | { 60 | Order::where('user_id', '=', $user->id)->get(); 61 | } 62 | } 63 | ``` 64 | 65 | 将接口的实现绑定到Laravel的服务容器中 66 | 67 | ``` 68 | 69 | App::singleton('OrderRepositoryInterface', 'OrderRespository'); 70 | ``` 71 | 72 | 73 | 74 | 然后我们将该接口的实现注入我们的控制器 75 | 76 | ``` 77 | class UserController extends Controller 78 | { 79 | public function __construct(OrderRepositoryInterface $orderRepository) 80 | { 81 | $this->orders = $orderRespository; 82 | } 83 | 84 | public function getUserOrders(User $user) 85 | { 86 | $orders = $this->orders->userOrders($user); 87 | return View::make('order.index', compact('orders')); 88 | } 89 | } 90 | ``` 91 | 92 | 现在我们的控制器就完全和数据层面无关了。在这里我们的数据可能来自MySQL,MongoDB或者Redis。我们的控制器不知道也不需要知道他们的区别。这样我们就可以独立于数据层来测试Web层了,将来切换存储实现也会很容易。 93 | 94 | ### 接口与团队开发 95 | 96 | 当你的团队在开发大型应用时,不同的部分有着不同的开发速度。比如一个开发人员在开发数据层,另一个开发人员在做控制器层。写控制器的开发者想测试他的控制器,不过数据层开发较慢没法同步测试。那如果两个开发者能先以interface的方式达成协议,后台开发的各种类都遵循这种协议。一旦建立了约定,就算约定还没实现,开发者也可以为这接口写个“假”实现 97 | 98 | ``` 99 | class DummyOrderRepository implements OrderRepositoryInterface 100 | { 101 | public function userOrders(User $user) 102 | { 103 | return collect(['Order 1', 'Order 2', 'Order 3']); 104 | } 105 | } 106 | ``` 107 | 108 | 一旦假实现写好了,就可以被绑定到IoC容器里 109 | 110 | ``` 111 | App::singleton('OrderRepositoryInterface', 'DummyOrderRepository'); 112 | ``` 113 | 114 | 然后这个应用的视图就可以用假数据填充了。接下来一旦后台开发者写完了真正的实现代码,比如叫`RedisOrderRepository`。那么使用IoC容器切换接口实现,应用就可以轻易地切换到真正的实现上,整个应用就会使用从Redis读出来的数据了。 115 | 116 | ### 接口与测试 117 | 118 | 建立好接口约定后也更有利于我们在测试时进行Mock 119 | 120 | ``` 121 | public function testIndexActionBindsUsersFromRepository() 122 | { 123 | // Arrange... 124 | $repository = Mockery::mock('OrderRepositoryInterface'); 125 | $repository->shouldReceive('userOrders')->once()->andReturn(['order1', 'order2']); 126 | App::instance('OrderRepositoryInterface', $repository); 127 | // Act... 128 | $response = $this->action('GET', 'OrderController@getUserOrders'); 129 | 130 | // Assert... 131 | $this->assertResponseOk(); 132 | $this->assertViewHas('order', ['order1', 'order2']); 133 | } 134 | ``` 135 | 136 | 137 | 138 | ### 总结 139 | 140 | 接口在程序设计阶段非常有用,在设计阶段与团队讨论完成功能需要制定哪些接口,然后设计出每个接口具体要实现的方法,方法的入参和返回值这些,每个人就可以按照接口的约定来开发自己的模块,遇到还没实现的接口完全可以先定义接口的假实现等到真正的实现开发完成后再进行切换,这样既降低了软件程序结构中上层对下层的耦合也能保证各部分的开发进度不会过度依赖其他部分的完成情况。 141 | 142 | 上一篇: [Cookie源码解析](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Cookie.md) 143 | 144 | 下一篇: [ENV配置的加载和读取](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ENV.md) 145 | 146 | 147 | -------------------------------------------------------------------------------- /articles/Controller.md: -------------------------------------------------------------------------------- 1 | # 控制器 2 | 3 | 控制器能够将相关的请求处理逻辑组成一个单独的类, 通过前面的路由和中间件两个章节我们多次强调Laravel应用的请求在进入应用后首现会通过Http Kernel里定义的基本中间件 4 | 5 | ``` 6 | protected $middleware = [ 7 | \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, 8 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 9 | \App\Http\Middleware\TrimStrings::class, 10 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 11 | \App\Http\Middleware\TrustProxies::class, 12 | ]; 13 | ``` 14 | 然后Http Kernel会通过`dispatchToRoute`将请求对象移交给路由对象进行处理,路由对象会收集路由上绑定的中间件然后还是像上面Http Kernel里一样用一个Pipeline管道对象将请求传送通过这些路由上绑定的这些中间键,到达目的地后会执行路由绑定的控制器方法然后把执行结果封装成响应对象,响应对象依次通过后置中间件最后返回给客户端。 15 | 16 | 下面是刚才说的这些步骤对应的核心代码: 17 | 18 | ``` 19 | namespace Illuminate\Foundation\Http; 20 | class Kernel implements KernelContract 21 | { 22 | protected function dispatchToRouter() 23 | { 24 | return function ($request) { 25 | $this->app->instance('request', $request); 26 | 27 | return $this->router->dispatch($request); 28 | }; 29 | } 30 | } 31 | 32 | 33 | namespace Illuminate\Routing; 34 | class Router implements RegistrarContract, BindingRegistrar 35 | { 36 | public function dispatch(Request $request) 37 | { 38 | $this->currentRequest = $request; 39 | 40 | return $this->dispatchToRoute($request); 41 | } 42 | 43 | public function dispatchToRoute(Request $request) 44 | { 45 | return $this->runRoute($request, $this->findRoute($request)); 46 | } 47 | 48 | protected function runRoute(Request $request, Route $route) 49 | { 50 | $request->setRouteResolver(function () use ($route) { 51 | return $route; 52 | }); 53 | 54 | $this->events->dispatch(new Events\RouteMatched($route, $request)); 55 | 56 | return $this->prepareResponse($request, 57 | $this->runRouteWithinStack($route, $request) 58 | ); 59 | } 60 | 61 | protected function runRouteWithinStack(Route $route, Request $request) 62 | { 63 | $shouldSkipMiddleware = $this->container->bound('middleware.disable') && 64 | $this->container->make('middleware.disable') === true; 65 | //收集路由和控制器里应用的中间件 66 | $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); 67 | 68 | return (new Pipeline($this->container)) 69 | ->send($request) 70 | ->through($middleware) 71 | ->then(function ($request) use ($route) { 72 | return $this->prepareResponse( 73 | $request, $route->run() 74 | ); 75 | }); 76 | 77 | } 78 | } 79 | 80 | namespace Illuminate\Routing; 81 | class Route 82 | { 83 | public function run() 84 | { 85 | $this->container = $this->container ?: new Container; 86 | try { 87 | if ($this->isControllerAction()) { 88 | return $this->runController(); 89 | } 90 | return $this->runCallable(); 91 | } catch (HttpResponseException $e) { 92 | return $e->getResponse(); 93 | } 94 | } 95 | 96 | } 97 | ``` 98 | 99 | 我们在前面的文章里已经详细的解释过Pipeline、中间件和路由的原理了,接下来就看看当请求最终找到了路由对应的控制器方法后Laravel是如何为控制器方法注入正确的参数并调用控制器方法的。 100 | 101 | ### 解析控制器和方法名 102 | 路由运行控制器方法的操作`runController`首现会解析出路由中对应的控制器名称和方法名称。我们在讲路由那一章里说过路由对象的action属性都是类似下面这样的: 103 | 104 | ``` 105 | [ 106 | 'uses' => 'App\Http\Controllers\SomeController@someAction', 107 | 'controller' => 'App\Http\Controllers\SomeController@someAction', 108 | 'middleware' => ... 109 | ] 110 | ``` 111 | 112 | 113 | ``` 114 | class Route 115 | { 116 | protected function isControllerAction() 117 | { 118 | return is_string($this->action['uses']); 119 | } 120 | 121 | protected function runController() 122 | { 123 | return (new ControllerDispatcher($this->container))->dispatch( 124 | $this, $this->getController(), $this->getControllerMethod() 125 | ); 126 | } 127 | 128 | public function getController() 129 | { 130 | if (! $this->controller) { 131 | $class = $this->parseControllerCallback()[0]; 132 | 133 | $this->controller = $this->container->make(ltrim($class, '\\')); 134 | } 135 | 136 | return $this->controller; 137 | } 138 | 139 | protected function getControllerMethod() 140 | { 141 | return $this->parseControllerCallback()[1]; 142 | } 143 | 144 | protected function parseControllerCallback() 145 | { 146 | return Str::parseCallback($this->action['uses']); 147 | } 148 | } 149 | 150 | class Str 151 | { 152 | //解析路由里绑定的控制器方法字符串,返回控制器和方法名称字符串构成的数组 153 | public static function parseCallback($callback, $default = null) 154 | { 155 | return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; 156 | } 157 | } 158 | 159 | ``` 160 | 所以路由通过`parseCallback`方法将uses配置项里的控制器字符串解析成数组返回, 数组第一项为控制器名称、第二项为方法名称。在拿到控制器和方法的名称字符串后,路由对象将自身、控制器和方法名传递给了`Illuminate\Routing\ControllerDispatcher`类,由`ControllerDispatcher`来完成最终的控制器方法的调用。下面我们详细看看`ControllerDispatcher`是怎么来调用控制器方法的。 161 | 162 | ``` 163 | class ControllerDispatcher 164 | { 165 | use RouteDependencyResolverTrait; 166 | 167 | public function dispatch(Route $route, $controller, $method) 168 | { 169 | $parameters = $this->resolveClassMethodDependencies( 170 | $route->parametersWithoutNulls(), $controller, $method 171 | ); 172 | 173 | if (method_exists($controller, 'callAction')) { 174 | return $controller->callAction($method, $parameters); 175 | } 176 | 177 | return $controller->{$method}(...array_values($parameters)); 178 | } 179 | } 180 | ``` 181 | 上面可以很清晰地看出,ControllerDispatcher里控制器的运行分为两步:解决method的参数依赖`resolveClassMethodDependencies`、调用控制器方法。 182 | 183 | ### 解决method参数依赖 184 | 解决方法的参数依赖通过`RouteDependencyResolverTrait`这一`trait`负责: 185 | 186 | ``` 187 | trait RouteDependencyResolverTrait 188 | { 189 | protected function resolveClassMethodDependencies(array $parameters, $instance, $method) 190 | { 191 | if (! method_exists($instance, $method)) { 192 | return $parameters; 193 | } 194 | 195 | 196 | return $this->resolveMethodDependencies( 197 | $parameters, new ReflectionMethod($instance, $method) 198 | ); 199 | } 200 | 201 | //参数为路由参数数组$parameters(可为空array)和控制器方法的反射对象 202 | public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector) 203 | { 204 | $instanceCount = 0; 205 | 206 | $values = array_values($parameters); 207 | 208 | foreach ($reflector->getParameters() as $key => $parameter) { 209 | $instance = $this->transformDependency( 210 | $parameter, $parameters 211 | ); 212 | 213 | if (! is_null($instance)) { 214 | $instanceCount++; 215 | 216 | $this->spliceIntoParameters($parameters, $key, $instance); 217 | } elseif (! isset($values[$key - $instanceCount]) && 218 | $parameter->isDefaultValueAvailable()) { 219 | $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue()); 220 | } 221 | } 222 | 223 | return $parameters; 224 | } 225 | 226 | } 227 | ``` 228 | 在解决方法的参数依赖时会应用到PHP反射的`ReflectionMethod`类来对控制器方法进行方向工程, 通过反射对象获取到参数后会判断现有参数的类型提示(type hint)是否是一个类对象参数,如果是类对象参数并且在现有参数中没有相同类的对象那么就会通过服务容器来`make`出类对象。 229 | 230 | ``` 231 | protected function transformDependency(ReflectionParameter $parameter, $parameters) 232 | { 233 | $class = $parameter->getClass(); 234 | if ($class && ! $this->alreadyInParameters($class->name, $parameters)) { 235 | return $parameter->isDefaultValueAvailable() 236 | ? $parameter->getDefaultValue() 237 | : $this->container->make($class->name); 238 | } 239 | } 240 | 241 | protected function alreadyInParameters($class, array $parameters) 242 | { 243 | return ! is_null(Arr::first($parameters, function ($value) use ($class) { 244 | return $value instanceof $class; 245 | })); 246 | } 247 | ``` 248 | 解析出类对象后需要将类对象插入到参数列表中去 249 | 250 | ``` 251 | protected function spliceIntoParameters(array &$parameters, $offset, $value) 252 | { 253 | array_splice( 254 | $parameters, $offset, 0, [$value] 255 | ); 256 | } 257 | ``` 258 | *** 我们之前讲服务容器时,里面讲的服务解析解决的是类构造方法的参数依赖,而这里resolveClassMethodDependencies解决的是具体某个方法的参数依赖,它是Laravel对method dependency injection概念的实现。*** 259 | 260 | 当路由的参数数组与服务容器构造的类对象数量之和不足以覆盖控制器方法参数个数时,就要去判断该参数是否具有默认参数,也就是会执行`resolveMethodDependencies`方法`foreach`块里的`else if`分支将参数的默认参数插入到方法的参数列表`$parameters`中去。 261 | 262 | ``` 263 | } elseif (! isset($values[$key - $instanceCount]) && 264 | $parameter->isDefaultValueAvailable()) { 265 | $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue()); 266 | } 267 | ``` 268 | 269 | ### 调用控制器方法 270 | 271 | 解决完method的参数依赖后就该调用方法了,这个很简单, 如果控制器有callAction方法就会调用callAction方法,否则的话就直接调用方法。 272 | 273 | ``` 274 | public function dispatch(Route $route, $controller, $method) 275 | { 276 | $parameters = $this->resolveClassMethodDependencies( 277 | $route->parametersWithoutNulls(), $controller, $method 278 | ); 279 | 280 | if (method_exists($controller, 'callAction')) { 281 | return $controller->callAction($method, $parameters); 282 | } 283 | 284 | return $controller->{$method}(...array_values($parameters)); 285 | } 286 | ``` 287 | 288 | 执行完拿到结果后,按照上面`runRouteWithinStack`里的逻辑,结果会被转换成响应对象。然后响应对象会依次经过之前应用过的所有中间件的后置操作,最后返回给客户端。 289 | 290 | 291 | 上一篇: [中间件](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Middleware.md) 292 | 293 | 下一篇: [Request](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Request.md) 294 | -------------------------------------------------------------------------------- /articles/Cookie.md: -------------------------------------------------------------------------------- 1 | # Laravel Cookie源码分析 2 | 3 | 4 | 5 | ### 使用Cookie的方法 6 | 7 | 为了安全起见,Laravel 框架创建的所有 Cookie 都经过加密并使用一个认证码进行签名,这意味着如果客户端修改了它们则需要对其进行有效性验证。我们使用 `Illuminate\Http\Request` 实例的 `cookie` 方法从请求中获取 Cookie 的值: 8 | 9 | ``` 10 | $value = $request->cookie('name'); 11 | ``` 12 | 13 | 也可以使用Facade `Cookie`来读取Cookie的值: 14 | 15 | ``` 16 | Cookie::get('name', '');//第二个参数的意思是读取不到name的cookie值的话,返回空字符串 17 | ``` 18 | 19 | 20 | 21 | **添加Cookie到响应** 22 | 23 | 可以使用 响应对象的`cookie` 方法将一个 Cookie 添加到返回的 `Illuminate\Http\Response` 实例中,你需要传递 Cookie 的名称、值、以及有效期(分钟)到这个方法: 24 | 25 | ``` 26 | return response('Learn Laravel Kernel')->cookie( 27 | 'cookie-name', 'cookie-value', $minutes 28 | ); 29 | ``` 30 | 31 | 响应对象的`cookie` 方法接收的参数和 PHP 原生函数 `setcookie` 的参数一致: 32 | 33 | ``` 34 | return response('Learn Laravel Kernel')->cookie( 35 | 'cookie-name', 'cookie-value', $minutes, $path, $domain, $secure, $httpOnly 36 | ); 37 | ``` 38 | 39 | 还可使用Facade `Cookie`的`queue`方法以队列的形式将Cookie添加到响应: 40 | 41 | ``` 42 | Cookie::queue('cookie-name', 'cookie-value'); 43 | ``` 44 | 45 | `queue` 方法接收 Cookie 实例或创建 Cookie 所必要的参数作为参数,这些 Cookie 会在响应被发送到浏览器之前添加到响应中。 46 | 47 | 48 | 49 | 接下来我们来分析一下Laravel中Cookie服务的实现原理。 50 | 51 | 52 | 53 | ### Cookie服务注册 54 | 55 | 之前在讲服务提供器的文章里我们提到过,Laravel在BootStrap阶段会通过服务提供器将框架中涉及到的所有服务注册到服务容器里,这样在用到具体某个服务时才能从服务容器中解析出服务来,所以`Cookie`服务的注册也不例外,在`config/app.php`中我们能找到Cookie对应的服务提供器和门面。 56 | 57 | ``` 58 | 'providers' => [ 59 | 60 | /* 61 | * Laravel Framework Service Providers... 62 | */ 63 | ...... 64 | Illuminate\Cookie\CookieServiceProvider::class, 65 | ...... 66 | ] 67 | 68 | 'aliases' => [ 69 | ...... 70 | 'Cookie' => Illuminate\Support\Facades\Cookie::class, 71 | ...... 72 | ] 73 | ``` 74 | 75 | Cookie服务的服务提供器是 `Illuminate\Cookie\CookieServiceProvider` ,其源码如下: 76 | 77 | ``` 78 | app->singleton('cookie', function ($app) { 94 | $config = $app->make('config')->get('session'); 95 | 96 | return (new CookieJar)->setDefaultPathAndDomain( 97 | $config['path'], $config['domain'], $config['secure'], $config['same_site'] ?? null 98 | ); 99 | }); 100 | } 101 | } 102 | ``` 103 | 104 | 在`CookieServiceProvider`里将`\Illuminate\Cookie\CookieJar`类的对象注册为Cookie服务,在实例化时会从Laravel的`config/session.php`配置中读取出`path`、`domain`、`secure`这些参数来设置Cookie服务用的默认路径和域名等参数,我们来看一下`CookieJar`里`setDefaultPathAndDomain`的实现: 105 | 106 | ``` 107 | namespace Illuminate\Cookie; 108 | 109 | class CookieJar implements JarContract 110 | { 111 | /** 112 | * 设置Cookie的默认路径和Domain 113 | * 114 | * @param string $path 115 | * @param string $domain 116 | * @param bool $secure 117 | * @param string $sameSite 118 | * @return $this 119 | */ 120 | public function setDefaultPathAndDomain($path, $domain, $secure = false, $sameSite = null) 121 | { 122 | list($this->path, $this->domain, $this->secure, $this->sameSite) = [$path, $domain, $secure, $sameSite]; 123 | 124 | return $this; 125 | } 126 | } 127 | ``` 128 | 129 | 它只是把这些默认参数保存到`CookieJar`对象的属性中,等到`make`生成`\Symfony\Component\HttpFoundation\Cookie`对象时才会使用它们。 130 | 131 | ### 生成Cookie 132 | 133 | 上面说了生成Cookie用的是`Response`对象的`cookie`方法,`Response`的是利用Laravel的全局函数`cookie`来生成Cookie对象然后设置到响应头里的,有点乱我们来看一下源码 134 | 135 | ``` 136 | class Response extends BaseResponse 137 | { 138 | /** 139 | * Add a cookie to the response. 140 | * 141 | * @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie 142 | * @return $this 143 | */ 144 | public function cookie($cookie) 145 | { 146 | return call_user_func_array([$this, 'withCookie'], func_get_args()); 147 | } 148 | 149 | /** 150 | * Add a cookie to the response. 151 | * 152 | * @param \Symfony\Component\HttpFoundation\Cookie|mixed $cookie 153 | * @return $this 154 | */ 155 | public function withCookie($cookie) 156 | { 157 | if (is_string($cookie) && function_exists('cookie')) { 158 | $cookie = call_user_func_array('cookie', func_get_args()); 159 | } 160 | 161 | $this->headers->setCookie($cookie); 162 | 163 | return $this; 164 | } 165 | } 166 | ``` 167 | 168 | 看一下全局函数`cookie`的实现: 169 | 170 | ``` 171 | /** 172 | * Create a new cookie instance. 173 | * 174 | * @param string $name 175 | * @param string $value 176 | * @param int $minutes 177 | * @param string $path 178 | * @param string $domain 179 | * @param bool $secure 180 | * @param bool $httpOnly 181 | * @param bool $raw 182 | * @param string|null $sameSite 183 | * @return \Illuminate\Cookie\CookieJar|\Symfony\Component\HttpFoundation\Cookie 184 | */ 185 | function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = false, $httpOnly = true, $raw = false, $sameSite = null) 186 | { 187 | $cookie = app(CookieFactory::class); 188 | 189 | if (is_null($name)) { 190 | return $cookie; 191 | } 192 | 193 | return $cookie->make($name, $value, $minutes, $path, $domain, $secure, $httpOnly, $raw, $sameSite); 194 | } 195 | ``` 196 | 197 | 通过`cookie`函数的@return标注我们能知道它返回的是一个`Illuminate\Cookie\CookieJar`对象或者是`\Symfony\Component\HttpFoundation\Cookie`对象。既`cookie`函数在参数`name`为空时返回一个`CookieJar`对象,否则调用`CookieJar`的`make`方法返回一个`\Symfony\Component\HttpFoundation\Cookie`对象。 198 | 199 | 200 | 201 | 拿到`Cookie`对象后程序接着流程往下走把Cookie设置到`Response`对象的`headers属性`里,`headers`属性引用了`\Symfony\Component\HttpFoundation\ResponseHeaderBag`对象 202 | 203 | ``` 204 | class ResponseHeaderBag extends HeaderBag 205 | { 206 | public function setCookie(Cookie $cookie) 207 | { 208 | $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; 209 | $this->headerNames['set-cookie'] = 'Set-Cookie'; 210 | } 211 | } 212 | ``` 213 | 214 | 我们可以看到这里只是把`Cookie`对象暂存到了`headers`对象里,真正把Cookie发送到浏览器是在`Laravel`返回响应时发生的,在`Laravel`的`public/index.php`里: 215 | 216 | ``` 217 | $response->send(); 218 | ``` 219 | Laravel的`Response`继承自Symfony的`Response`,`send`方法定义在`Symfony`的`Response`里 220 | 221 | ``` 222 | namespace Symfony\Component\HttpFoundation; 223 | 224 | class Response 225 | { 226 | /** 227 | * Sends HTTP headers and content. 228 | * 229 | * @return $this 230 | */ 231 | public function send() 232 | { 233 | $this->sendHeaders(); 234 | $this->sendContent(); 235 | 236 | if (function_exists('fastcgi_finish_request')) { 237 | fastcgi_finish_request(); 238 | } elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) { 239 | static::closeOutputBuffers(0, true); 240 | } 241 | 242 | return $this; 243 | } 244 | 245 | public function sendHeaders() 246 | { 247 | // headers have already been sent by the developer 248 | if (headers_sent()) { 249 | return $this; 250 | } 251 | 252 | // headers 253 | foreach ($this->headers->allPreserveCase() as $name => $values) { 254 | foreach ($values as $value) { 255 | header($name.': '.$value, false, $this->statusCode); 256 | } 257 | } 258 | 259 | // status 260 | header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * Returns the headers, with original capitalizations. 267 | * 268 | * @return array An array of headers 269 | */ 270 | public function allPreserveCase() 271 | { 272 | $headers = array(); 273 | foreach ($this->all() as $name => $value) { 274 | $headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value; 275 | } 276 | 277 | return $headers; 278 | } 279 | 280 | public function all() 281 | { 282 | $headers = parent::all(); 283 | foreach ($this->getCookies() as $cookie) { 284 | $headers['set-cookie'][] = (string) $cookie; 285 | } 286 | 287 | return $headers; 288 | } 289 | } 290 | ``` 291 | 292 | 在`Response`的`send`方法里发送响应头时将Cookie数据设置到了Http响应首部的`Set-Cookie`字段里,这样当响应发送给浏览器后浏览器就能保存这些Cookie数据了。 293 | 294 | 至于用门面`Cookie::queue`以队列的形式设置Cookie其实也是将Cookie暂存到了`CookieJar`对象的`queued`属性里 295 | 296 | ``` 297 | namespace Illuminate\Cookie; 298 | class CookieJar implements JarContract 299 | { 300 | public function queue(...$parameters) 301 | { 302 | if (head($parameters) instanceof Cookie) { 303 | $cookie = head($parameters); 304 | } else { 305 | $cookie = call_user_func_array([$this, 'make'], $parameters); 306 | } 307 | 308 | $this->queued[$cookie->getName()] = $cookie; 309 | } 310 | 311 | public function queued($key, $default = null) 312 | { 313 | return Arr::get($this->queued, $key, $default); 314 | } 315 | } 316 | ``` 317 | 318 | 然后在`web`中间件组里边有一个`\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse`中间件,它在响应返回给客户端之前将暂存在`queued`属性里的Cookie设置到了响应的`headers`对象里: 319 | 320 | ``` 321 | namespace Illuminate\Cookie\Middleware; 322 | 323 | use Closure; 324 | use Illuminate\Contracts\Cookie\QueueingFactory as CookieJar; 325 | 326 | class AddQueuedCookiesToResponse 327 | { 328 | /** 329 | * The cookie jar instance. 330 | * 331 | * @var \Illuminate\Contracts\Cookie\QueueingFactory 332 | */ 333 | protected $cookies; 334 | 335 | /** 336 | * Create a new CookieQueue instance. 337 | * 338 | * @param \Illuminate\Contracts\Cookie\QueueingFactory $cookies 339 | * @return void 340 | */ 341 | public function __construct(CookieJar $cookies) 342 | { 343 | $this->cookies = $cookies; 344 | } 345 | 346 | /** 347 | * Handle an incoming request. 348 | * 349 | * @param \Illuminate\Http\Request $request 350 | * @param \Closure $next 351 | * @return mixed 352 | */ 353 | public function handle($request, Closure $next) 354 | { 355 | $response = $next($request); 356 | 357 | foreach ($this->cookies->getQueuedCookies() as $cookie) { 358 | $response->headers->setCookie($cookie); 359 | } 360 | 361 | return $response; 362 | } 363 | ``` 364 | 365 | 这样在`Response`对象调用`send`方法时也会把通过`Cookie::queue()`设置的Cookie数据设置到`Set-Cookie`响应首部中去了。 366 | 367 | 368 | 369 | 370 | 371 | ### 读取Cookie 372 | 373 | Laravel读取请求中的Cookie值`$value = $request->cookie('name');` 其实是Laravel的`Request`对象直接去读取`Symfony`请求对象的`cookies`来实现的, 我们在写`Laravel Request`对象的文章里有提到它依赖于`Symfony`的`Request`, `Symfony`的`Request`在实例化时会把PHP里那些`$_POST`、`$_COOKIE`全局变量抽象成了具体对象存储在了对应的属性中。 374 | 375 | ``` 376 | namespace Illuminate\Http; 377 | 378 | class Request extends SymfonyRequest implements Arrayable, ArrayAccess 379 | { 380 | public function cookie($key = null, $default = null) 381 | { 382 | return $this->retrieveItem('cookies', $key, $default); 383 | } 384 | 385 | protected function retrieveItem($source, $key, $default) 386 | { 387 | if (is_null($key)) { 388 | return $this->$source->all(); 389 | } 390 | //从Request的cookies属性中获取数据 391 | return $this->$source->get($key, $default); 392 | } 393 | } 394 | ``` 395 | 396 | 关于通过门面`Cookie::get()`读取Cookie的实现我们可以看下`Cookie`门面源码的实现,通过源码我们知道门面`Cookie`除了通过外观模式代理`Cookie`服务外自己也定义了两个方法: 397 | 398 | ``` 399 | cookie($key, null)); 417 | } 418 | 419 | /** 420 | * Retrieve a cookie from the request. 421 | * 422 | * @param string $key 423 | * @param mixed $default 424 | * @return string 425 | */ 426 | public static function get($key = null, $default = null) 427 | { 428 | return static::$app['request']->cookie($key, $default); 429 | } 430 | 431 | /** 432 | * Get the registered name of the component. 433 | * 434 | * @return string 435 | */ 436 | protected static function getFacadeAccessor() 437 | { 438 | return 'cookie'; 439 | } 440 | } 441 | ``` 442 | 443 | `Cookie::get()`和`Cookie::has()`是门面直接读取`Request`对象`cookies`属性里的Cookie数据。 444 | 445 | 446 | 447 | ### Cookie加密 448 | 449 | 关于对Cookie的加密可以看一下`Illuminate\Cookie\Middleware\EncryptCookies`中间件的源码,它的子类`App\Http\Middleware\EncryptCookies`是Laravel`web`中间件组里的一个中间件,如果想让客户端的Javascript程序能够读Laravel设置的Cookie则需要在`App\Http\Middleware\EncryptCookies`的`$exception`里对Cookie名称进行声明。 450 | 451 | 452 | 453 | 454 | 455 | Laravel中Cookie模块大致的实现原理就梳理完了,希望大家看了我的源码分析后能够清楚Laravel Cookie实现的基本流程这样在遇到困惑或者无法通过文档找到解决方案时可以通过阅读源码看看它的实现机制再相应的设计解决方案。 456 | 457 | 上一篇: [Session模块源码解析](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Session.md) 458 | 459 | 下一篇: [Contracts契约](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Contracts.md) 460 | -------------------------------------------------------------------------------- /articles/Database1.md: -------------------------------------------------------------------------------- 1 | # Database 基础介绍 2 | 3 | 在我们学习和使用一个开发框架时,无论使用什么框架,如何连接数据库、对数据库进行增删改查都是学习的重点,在Laravel中我们可以通过两种方式与数据库进行交互: 4 | 5 | * `DB`, `DB`是与PHP底层的`PDO`直接进行交互的,通过查询构建器提供了一个方便的接口来创建及运行数据库查询语句。 6 | * `Eloquent Model`, `Eloquent`是建立在`DB`的查询构建器基础之上,对数据库进行了抽象的`ORM`,功能十分丰富让我们可以避免写复杂的SQL语句,并用优雅的方式解决了数据表之间的关联关系。 7 | 8 | 上面说的这两个部分都包括在了`Illuminate/Database`包里面,除了作为Laravel的数据库层`Illuminate/Database`还是一个PHP数据库工具集, 在任何项目里你都可以通过`composer install illuminate/databse`安装并使用它。 9 | 10 | ### Database服务注册和初始化 11 | 12 | Database也是作为一种服务注册到服务容器里提供给Laravel应用使用的,它的服务提供器是`Illuminate\Database\DatabaseServiceProvider` 13 | 14 | public function register() 15 | { 16 | Model::clearBootedModels(); 17 | 18 | $this->registerConnectionServices(); 19 | 20 | $this->registerEloquentFactory(); 21 | 22 | $this->registerQueueableEntityResolver(); 23 | } 24 | 25 | 第一步:`Model::clearBootedModels()`。在 Eloquent 服务启动之前为了保险起见需要清理掉已经booted的Model和全局查询作用域 26 | 27 | /** 28 | * Clear the list of booted models so they will be re-booted. 29 | * 30 | * @return void 31 | */ 32 | public static function clearBootedModels() 33 | { 34 | static::$booted = []; 35 | 36 | static::$globalScopes = []; 37 | } 38 | 39 | 第二步:注册ConnectionServices 40 | 41 | protected function registerConnectionServices() 42 | { 43 | $this->app->singleton('db.factory', function ($app) { 44 | return new ConnectionFactory($app); 45 | }); 46 | 47 | $this->app->singleton('db', function ($app) { 48 | return new DatabaseManager($app, $app['db.factory']); 49 | }); 50 | 51 | $this->app->bind('db.connection', function ($app) { 52 | return $app['db']->connection(); 53 | }); 54 | } 55 | 56 | * `db.factory`用来创建数据库连接实例,它将被注入到DatabaseManager中,在讲服务容器绑定时就说过了依赖注入的其中一个作用是延迟初始化对象,所以只要在用到数据库连接实例时它们才会被创建。 57 | * `db` DatabaseManger 作为Database面向外部的接口,`DB`这个Facade就是DatabaseManager的静态代理。应用中所有与Database有关的操作都是通过与这个接口交互来完成的。 58 | * `db.connection` 数据库连接实例,是与底层PDO接口进行交互的底层类,可用于数据库的查询、更新、创建等操作。 59 | 60 | 所以DatabaseManager作为接口与外部交互,在应用需要时通过ConnectionFactory创建了数据库连接实例,最后执行数据库的增删改查是由数据库连接实例来完成的。 61 | 62 | 第三步:注册Eloquent工厂 63 | 64 | protected function registerEloquentFactory() 65 | { 66 | $this->app->singleton(FakerGenerator::class, function ($app) { 67 | return FakerFactory::create($app['config']->get('app.faker_locale', 'en_US')); 68 | }); 69 | 70 | $this->app->singleton(EloquentFactory::class, function ($app) { 71 | return EloquentFactory::construct( 72 | $app->make(FakerGenerator::class), $this->app->databasePath('factories') 73 | ); 74 | }); 75 | } 76 | 77 | 启动数据库服务 78 | 79 | public function boot() 80 | { 81 | Model::setConnectionResolver($this->app['db']); 82 | 83 | Model::setEventDispatcher($this->app['events']); 84 | } 85 | 数据库服务的启动主要设置 Eloquent Model 的连接分析器(connection resolver),让model能够用db服务连接数据库。还有就是设置数据库事件的分发器 dispatcher,用于监听数据库的事件。 86 | 87 | ### DatabaseManager 88 | 89 | 上面说了DatabaseManager是整个数据库服务的接口,我们通过`DB`门面进行操作的时候实际上调用的就是DatabaseManager,它会通过数据库连接对象工厂(ConnectionFacotry)获得数据库连接对象(Connection),然后数据库连接对象会进行具体的CRUD操作。我们先看一下DatabaseManager的构造函数: 90 | 91 | public function __construct($app, ConnectionFactory $factory) 92 | { 93 | $this->app = $app; 94 | $this->factory = $factory; 95 | } 96 | 97 | ConnectionFactory是在上面介绍的绑定`db`服务的时候传递给DatabaseManager的。比如我们现在程序里执行了`DB::table('users')->get()`, 在DatabaseManager里并没有`table`方法然后就会触发魔术方法`__call`: 98 | 99 | ``` 100 | class DatabaseManager implements ConnectionResolverInterface 101 | { 102 | protected $app; 103 | protected $factory; 104 | protected $connections = []; 105 | 106 | public function __call($method, $parameters) 107 | { 108 | return $this->connection()->$method(...$parameters); 109 | } 110 | 111 | public function connection($name = null) 112 | { 113 | list($database, $type) = $this->parseConnectionName($name); 114 | 115 | $name = $name ?: $database; 116 | 117 | if (! isset($this->connections[$name])) { 118 | $this->connections[$name] = $this->configure( 119 | $this->makeConnection($database), $type 120 | ); 121 | } 122 | 123 | return $this->connections[$name]; 124 | } 125 | 126 | } 127 | ``` 128 | connection方法会返回数据库连接对象,这个过程首先是解析连接名称`parseConnectionName` 129 | 130 | protected function parseConnectionName($name) 131 | { 132 | $name = $name ?: $this->getDefaultConnection(); 133 | // 检查connection name 是否以::read, ::write结尾 比如'ucenter::read' 134 | return Str::endsWith($name, ['::read', '::write']) 135 | ? explode('::', $name, 2) : [$name, null]; 136 | } 137 | 138 | public function getDefaultConnection() 139 | { 140 | // laravel默认是mysql,这里假定是常用的mysql连接 141 | return $this->app['config']['database.default']; 142 | } 143 | 144 | 如果没有指定连接名称,Laravel会使用database配置里指定的默认连接名称, 接下来`makeConnection`方法会根据连接名称来创建连接实例: 145 | 146 | protected function makeConnection($name) 147 | { 148 | //假定$name是'mysql', 从config/database.php中获取'connections.mysql'的配置 149 | $config = $this->configuration($name); 150 | 151 | //首先去检查在应用启动时是否通过连接名注册了extension(闭包), 如果有则通过extension获得连接实例 152 | //比如在AppServiceProvider里通过DatabaseManager::extend('mysql', function () {...}) 153 | if (isset($this->extensions[$name])) { 154 | return call_user_func($this->extensions[$name], $config, $name); 155 | } 156 | 157 | //检查是否为连接配置指定的driver注册了extension, 如果有则通过extension获得连接实例 158 | if (isset($this->extensions[$driver])) { 159 | return call_user_func($this->extensions[$driver], $config, $name); 160 | } 161 | 162 | // 通过ConnectionFactory数据库连接对象工厂获取Mysql的连接类 163 | return $this->factory->make($config, $name); 164 | } 165 | 166 | ### ConnectionFactory 167 | 168 | 上面`makeConnection`方法使用了数据库连接对象工程来获取数据库连接对象,我们来看一下工厂的make方法: 169 | 170 | /** 171 | * 根据配置创建一个PDO连接 172 | * 173 | * @param array $config 174 | * @param string $name 175 | * @return \Illuminate\Database\Connection 176 | */ 177 | public function make(array $config, $name = null) 178 | { 179 | $config = $this->parseConfig($config, $name); 180 | 181 | if (isset($config['read'])) { 182 | return $this->createReadWriteConnection($config); 183 | } 184 | 185 | return $this->createSingleConnection($config); 186 | } 187 | 188 | protected function parseConfig(array $config, $name) 189 | { 190 | return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name); 191 | } 192 | 193 | 在建立连接之前, 先通过`parseConfig`向配置参数中添加默认的 prefix 属性与 name 属性。 194 | 195 | 接下来根据配置文件中是否设置了读写分离。如果设置了读写分离,那么就会调用 createReadWriteConnection 函数,生成具有读、写两个功能的 connection;否则的话,就会调用 createSingleConnection 函数,生成普通的连接对象。 196 | 197 | 198 | protected function createSingleConnection(array $config) 199 | { 200 | $pdo = $this->createPdoResolver($config); 201 | 202 | return $this->createConnection( 203 | $config['driver'], $pdo, $config['database'], $config['prefix'], $config 204 | ); 205 | } 206 | 207 | protected function createConnection($driver, $connection, $database, $prefix = '', array $config = []) 208 | { 209 | ...... 210 | switch ($driver) { 211 | case 'mysql': 212 | return new MySqlConnection($connection, $database, $prefix, $config); 213 | case 'pgsql': 214 | return new PostgresConnection($connection, $database, $prefix, $config); 215 | ...... 216 | } 217 | 218 | throw new InvalidArgumentException("Unsupported driver [$driver]"); 219 | } 220 | 221 | 222 | 223 | 创建数据库连接的方法`createConnection`里参数`$pdo`是一个闭包: 224 | 225 | ``` 226 | function () use ($config) { 227 | return $this->createConnector($config)->connect($config); 228 | }; 229 | ``` 230 | 这就引出了Database服务中另一部份连接器`Connector`, Connection对象是依赖连接器连接上数据库的,所以在探究Connection之前我们先来看看连接器Connector。 231 | 232 | ### Connector 233 | 在`illuminate/database`中连接器Connector是专门负责与PDO交互连接数据库的,我们接着上面讲到的闭包参数`$pdo`往下看 234 | 235 | `createConnector`方法会创建连接器: 236 | 237 | public function createConnector(array $config) 238 | { 239 | if (! isset($config['driver'])) { 240 | throw new InvalidArgumentException('A driver must be specified.'); 241 | } 242 | 243 | if ($this->container->bound($key = "db.connector.{$config['driver']}")) { 244 | return $this->container->make($key); 245 | } 246 | 247 | switch ($config['driver']) { 248 | case 'mysql': 249 | return new MySqlConnector; 250 | case 'pgsql': 251 | return new PostgresConnector; 252 | case 'sqlite': 253 | return new SQLiteConnector; 254 | case 'sqlsrv': 255 | return new SqlServerConnector; 256 | } 257 | 258 | throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]"); 259 | } 260 | 261 | 这里我们还是以mysql举例看一下Mysql的连接器。 262 | 263 | ``` 264 | class MySqlConnector extends Connector implements ConnectorInterface 265 | { 266 | public function connect(array $config) 267 | { 268 | //生成PDO连接数据库时用的DSN连接字符串 269 | $dsn = $this->getDsn($config); 270 | //获取要传给PDO的选项参数 271 | $options = $this->getOptions($config); 272 | //创建一个PDO连接对象 273 | $connection = $this->createConnection($dsn, $config, $options); 274 | 275 | if (! empty($config['database'])) { 276 | $connection->exec("use `{$config['database']}`;"); 277 | } 278 | 279 | //为连接设置字符集和collation 280 | $this->configureEncoding($connection, $config); 281 | //设置time zone 282 | $this->configureTimezone($connection, $config); 283 | //为数据库会话设置sql mode 284 | $this->setModes($connection, $config); 285 | 286 | return $connection; 287 | } 288 | } 289 | ``` 290 | 291 | 这样就通过连接器与PHP底层的PDO交互连接上数据库了。 292 | 293 | ### Connection 294 | 所有类型数据库的Connection类都是继承了Connection父类: 295 | 296 | ``` 297 | class MySqlConnection extends Connection 298 | { 299 | ...... 300 | } 301 | 302 | class Connection implements ConnectionInterface 303 | { 304 | public function __construct($pdo, $database = '', $tablePrefix = '', array $config = []) 305 | { 306 | $this->pdo = $pdo; 307 | 308 | $this->database = $database; 309 | 310 | $this->tablePrefix = $tablePrefix; 311 | 312 | $this->config = $config; 313 | 314 | $this->useDefaultQueryGrammar(); 315 | 316 | $this->useDefaultPostProcessor(); 317 | } 318 | ...... 319 | public function table($table) 320 | { 321 | return $this->query()->from($table); 322 | } 323 | ...... 324 | public function query() 325 | { 326 | return new QueryBuilder( 327 | $this, $this->getQueryGrammar(), $this->getPostProcessor() 328 | ); 329 | } 330 | ...... 331 | } 332 | ``` 333 | 334 | Connection就是DatabaseManager代理的数据库连接对象了, 所以最开始执行的代码`DB::table('users')->get()`经过我们上面讲的历程,最终是由Connection来完成执行的,table方法返回了一个QueryBuilder对象,这个对象里定义里那些我们经常用到的`where`, `get`, `first`等方法, 它会根据调用的方法生成对应的SQL语句,最后通过Connection对象执行来获得最终的结果。 详细内容我们等到以后讲查询构建器的时候再看。 335 | 336 | ### 总结 337 | 338 | 说的东西有点多,我们来总结下文章里讲到的Database的这几个组件的角色 339 | 340 | | 名称 | 作用 | 341 | | ---------- | --- | 342 | | DB| DatabaseManager的静态代理| 343 | | DatabaseManager | Database面向外部的接口,应用中所有与Database有关的操作都是通过与这个接口交互来完成的。 | 344 | | ConnectionFactory | 创建数据库连接对象的类工厂 | 345 | | Connection | 数据库连接对象,执行数据库操作最后都是通过它与PHP底层的PDO交互来完成的| 346 | | Connector | 作为Connection的成员专门负责通过PDO连接数据库| 347 | 348 | 349 | 我们需要先理解了这几个组件的作用,在这些基础之上再去顺着看查询构建器的代码。 350 | 351 | 352 | 上一篇: [Response](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Response.md) 353 | 354 | 下一篇: [Database 查询构建器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database2.md) 355 | -------------------------------------------------------------------------------- /articles/Database3.md: -------------------------------------------------------------------------------- 1 | # Database 模型CRUD 2 | 3 | 上篇文章我们讲了Database的查询构建器Query Builder, 学习了Query Builder为构建生成SQL语句而提供的Fluent Api的代码实现。这篇文章我们来学习Laravel Database地另外一个重要的部分: Eloquent Model。 4 | 5 | Eloquent Model把数据表的属性、关联关系等抽象到了每个Model类中,所以Model类是对数据表的抽象,而Model对象则是对表中单条记录的抽象。Eloquent Model以上文讲到的Query Builder为基础提供了Eloquent Builder与数据库进行交互,此外还提供了模型关联优雅地解决了多个数据表之间的关联关系。 6 | 7 | ### 加载Eloquent Builder 8 | 9 | Eloquent Builder是在上文说到的Query Builder的基础上实现的,我们还是通过具体的例子来看,上文用到的: 10 | 11 | DB::table('user')->where('name', 'James')->where('age', 27)->get(); 12 | 13 | 把它改写为使用Model的方式后就变成了 14 | 15 | User::where('name', 'James')->where('age', 27)->get(); 16 | 17 | 在Model类文件里我们并没有找到`where`、`find`、`first`这些常用的查询方法,我们都知道当调用一个不存在的类方法时PHP会触发魔术方法`__callStatic`, 调用不存在的实例方法会触发`__call`, 很容易就猜到上面这些方法就是通过这两个魔术方法来动态调用的,下面让我们看一下源码。 18 | 19 | ``` 20 | namespace Illuminate\Database\Eloquent; 21 | abstract class Model implements ... 22 | { 23 | public function __call($method, $parameters) 24 | { 25 | if (in_array($method, ['increment', 'decrement'])) { 26 | return $this->$method(...$parameters); 27 | } 28 | 29 | return $this->newQuery()->$method(...$parameters); 30 | } 31 | 32 | public static function __callStatic($method, $parameters) 33 | { 34 | return (new static)->$method(...$parameters); 35 | } 36 | 37 | // new Eloquent Builder 38 | public function newQuery() 39 | { 40 | return $this->registerGlobalScopes($this->newQueryWithoutScopes()); 41 | } 42 | 43 | public function newQueryWithoutScopes() 44 | { 45 | $builder = $this->newEloquentBuilder($this->newBaseQueryBuilder()); 46 | 47 | //设置builder的Model实例,这样在构建和执行query时就能使用model中的信息了 48 | return $builder->setModel($this) 49 | ->with($this->with) 50 | ->withCount($this->withCount); 51 | } 52 | 53 | //创建数据库连接的QueryBuilder 54 | protected function newBaseQueryBuilder() 55 | { 56 | $connection = $this->getConnection(); 57 | 58 | return new QueryBuilder( 59 | $connection, $connection->getQueryGrammar(), $connection->getPostProcessor() 60 | ); 61 | } 62 | 63 | } 64 | 65 | ``` 66 | 67 | ### Model查询 68 | 69 | 通过上面的那些代码我们可以看到对Model调用的这些查询相关的方法最后都会通过`__call`转而去调用Eloquent Builder实例的这些方法,Eloquent Builder与底层数据库交互的部分都是依赖Query Builder来实现的,我们看到在实例化Eloquent Builder的时候把数据库连接的QueryBuilder对象传给了它的构造方法, 下面就去看一下Eloquent Builder的源码。 70 | 71 | ``` 72 | namespace Illuminate\Database\Eloquent; 73 | class Builder 74 | { 75 | public function __construct(QueryBuilder $query) 76 | { 77 | $this->query = $query; 78 | } 79 | 80 | public function where($column, $operator = null, $value = null, $boolean = 'and') 81 | { 82 | if ($column instanceof Closure) { 83 | $query = $this->model->newQueryWithoutScopes(); 84 | 85 | $column($query); 86 | 87 | $this->query->addNestedWhereQuery($query->getQuery(), $boolean); 88 | } else { 89 | $this->query->where(...func_get_args()); 90 | } 91 | 92 | return $this; 93 | } 94 | 95 | public function get($columns = ['*']) 96 | { 97 | $builder = $this->applyScopes(); 98 | 99 | //如果获取到了model还会load要预加载的模型关联,避免运行n+1次查询 100 | if (count($models = $builder->getModels($columns)) > 0) { 101 | $models = $builder->eagerLoadRelations($models); 102 | } 103 | 104 | return $builder->getModel()->newCollection($models); 105 | } 106 | 107 | public function getModels($columns = ['*']) 108 | { 109 | return $this->model->hydrate( 110 | $this->query->get($columns)->all() 111 | )->all(); 112 | } 113 | 114 | //将查询出来的结果转换成Model对象组成的Collection 115 | public function hydrate(array $items) 116 | { 117 | //新建一个model实例 118 | $instance = $this->newModelInstance(); 119 | 120 | return $instance->newCollection(array_map(function ($item) use ($instance) { 121 | return $instance->newFromBuilder($item); 122 | }, $items)); 123 | } 124 | 125 | //first 方法就是应用limit 1,get返回的集合后用Arr::first()从集合中取出model对象 126 | public function first($columns = ['*']) 127 | { 128 | return $this->take(1)->get($columns)->first(); 129 | } 130 | } 131 | 132 | //newModelInstance newFromBuilder 定义在\Illuminate\Database\EloquentModel类文件里 133 | 134 | public function newFromBuilder($attributes = [], $connection = null) 135 | { 136 | //新建实例,并且把它的exists属性设成true, save时会根据这个属性判断是insert还是update 137 | $model = $this->newInstance([], true); 138 | 139 | $model->setRawAttributes((array) $attributes, true); 140 | 141 | $model->setConnection($connection ?: $this->getConnectionName()); 142 | 143 | $model->fireModelEvent('retrieved', false); 144 | 145 | return $model; 146 | } 147 | ``` 148 | 149 | 代码里Eloquent Builder的where方法在接到调用请求后直接把请求转给来Query Builder的`where`方法,然后get方法也是先通过Query Builder的`get`方法执行查询拿到结果数组后再通过`newFromBuilder`方法把结果数组转换成Model对象构成的集合,而另外一个比较常用的方法`first`也是在`get`方法的基础上实现的,对query应用limit 1,再从`get`方法返回的集合中用 `Arr::first()`取出model对象返回给调用者。 150 | 151 | 152 | 153 | ### Model更新 154 | 看完了Model查询的实现我们再来看一下update、create和delete的实现,还是从一开始的查询例子继续扩展: 155 | 156 | $user = User::where('name', 'James')->where('age', 27)->first(); 157 | 158 | 现在通过Model查询我们获取里一个User Model的实例,我们现在要把这个用户的age改成28岁: 159 | 160 | $user->age = 28; 161 | $user->save(); 162 | 163 | 我们知道model的属性对应的是数据表的字段,在上面get方法返回Model实例集合时我们看到过把数据记录的字段和字段值都赋值给了Model实例的$attributes属性, Model实例访问和设置这些字段对应的属性时是通过`__get`和`__set`魔术方法动态获取和设置这些属性值的。 164 | 165 | ``` 166 | abstract class Model implements ... 167 | { 168 | public function __get($key) 169 | { 170 | return $this->getAttribute($key); 171 | } 172 | 173 | public function __set($key, $value) 174 | { 175 | $this->setAttribute($key, $value); 176 | } 177 | 178 | public function getAttribute($key) 179 | { 180 | if (! $key) { 181 | return; 182 | } 183 | 184 | //如果attributes数组的index里有$key或者$key对应一个属性访问器`'get' . $key . 'Attribute'` 则从这里取出$key对应的值 185 | //否则就尝试去获取模型关联的值 186 | if (array_key_exists($key, $this->attributes) || 187 | $this->hasGetMutator($key)) { 188 | return $this->getAttributeValue($key); 189 | } 190 | 191 | if (method_exists(self::class, $key)) { 192 | return; 193 | } 194 | //获取模型关联的值 195 | return $this->getRelationValue($key); 196 | } 197 | 198 | public function getAttributeValue($key) 199 | { 200 | $value = $this->getAttributeFromArray($key); 201 | 202 | if ($this->hasGetMutator($key)) { 203 | return $this->mutateAttribute($key, $value); 204 | } 205 | 206 | if ($this->hasCast($key)) { 207 | return $this->castAttribute($key, $value); 208 | } 209 | 210 | if (in_array($key, $this->getDates()) && 211 | ! is_null($value)) { 212 | return $this->asDateTime($value); 213 | } 214 | 215 | return $value; 216 | } 217 | 218 | protected function getAttributeFromArray($key) 219 | { 220 | if (isset($this->attributes[$key])) { 221 | return $this->attributes[$key]; 222 | } 223 | } 224 | 225 | public function setAttribute($key, $value) 226 | { 227 |   //如果$key存在属性修改器则去调用$key地属性修改器`'set' . $key . 'Attribute'` 比如`setNameAttribute` 228 |        if ($this->hasSetMutator($key)) { 229 | $method = 'set'.Str::studly($key).'Attribute'; 230 | 231 | return $this->{$method}($value); 232 | } 233 | 234 | elseif ($value && $this->isDateAttribute($key)) { 235 | $value = $this->fromDateTime($value); 236 | } 237 | 238 | if ($this->isJsonCastable($key) && ! is_null($value)) { 239 | $value = $this->castAttributeAsJson($key, $value); 240 | } 241 | 242 | if (Str::contains($key, '->')) { 243 | return $this->fillJsonAttribute($key, $value); 244 | } 245 | 246 | $this->attributes[$key] = $value; 247 | 248 | return $this; 249 | } 250 | } 251 | ``` 252 | 253 | 如果Model定义的属性修改器那么在设置属性的时候会去执行修改器,在我们的例子中并没有用到属性修改器。当执行`$user->age = 28`时, User Model实例里$attributes属性会变成 254 | 255 | ``` 256 | protected $attributes = [ 257 | ... 258 | 'age' => 28, 259 | ... 260 | ] 261 | ``` 262 | 263 | 设置好属性新的值之后执行Eloquent Model的save方法就会更新数据库里对应的记录,下面我们看看save方法里的逻辑: 264 | 265 | ``` 266 | abstract class Model implements ... 267 | { 268 | public function save(array $options = []) 269 | { 270 | $query = $this->newQueryWithoutScopes(); 271 | 272 | if ($this->fireModelEvent('saving') === false) { 273 | return false; 274 | } 275 | //查询出来的Model实例的exists属性都是true 276 | if ($this->exists) { 277 | $saved = $this->isDirty() ? 278 | $this->performUpdate($query) : true; 279 | } 280 | 281 | else { 282 | $saved = $this->performInsert($query); 283 | 284 | if (! $this->getConnectionName() && 285 | $connection = $query->getConnection()) { 286 | $this->setConnection($connection->getName()); 287 | } 288 | } 289 | 290 | if ($saved) { 291 | $this->finishSave($options); 292 | } 293 | 294 | return $saved; 295 | } 296 | 297 | //判断对字段是否有更改 298 | public function isDirty($attributes = null) 299 | { 300 | return $this->hasChanges( 301 | $this->getDirty(), is_array($attributes) ? $attributes : func_get_args() 302 | ); 303 | } 304 | 305 |    //数据表字段会保存在$attributes和$original两个属性里,update前通过比对两个数组里各字段的值找出被更改的字段 306 | public function getDirty() 307 | { 308 | $dirty = []; 309 | 310 | foreach ($this->getAttributes() as $key => $value) { 311 | if (! $this->originalIsEquivalent($key, $value)) { 312 | $dirty[$key] = $value; 313 | } 314 | } 315 | 316 | return $dirty; 317 | } 318 | 319 | protected function performUpdate(Builder $query) 320 | { 321 | if ($this->fireModelEvent('updating') === false) { 322 | return false; 323 | } 324 | 325 | if ($this->usesTimestamps()) { 326 | $this->updateTimestamps(); 327 | } 328 | 329 | $dirty = $this->getDirty(); 330 | 331 | if (count($dirty) > 0) { 332 | $this->setKeysForSaveQuery($query)->update($dirty); 333 | 334 | $this->fireModelEvent('updated', false); 335 | 336 | $this->syncChanges(); 337 | } 338 | 339 | return true; 340 | } 341 | 342 | //为查询设置where primary key = xxx 343 | protected function setKeysForSaveQuery(Builder $query) 344 | { 345 | $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); 346 | 347 | return $query; 348 | } 349 | } 350 | ``` 351 | 在save里会根据Model实例的`exists`属性来判断是执行update还是insert, 这里我们用的这个例子是update,在update时程序通过比对`$attributes`和`$original`两个array属性里各字段的字段值找被更改的字段(获取Model对象时会把数据表字段会保存在`$attributes`和`$original`两个属性),如果没有被更改的字段那么update到这里就结束了,有更改那么就继续去执行`performUpdate`方法,`performUpdate`方法会执行Eloquent Builder的update方法, 而Eloquent Builder依赖的还是数据库连接的Query Builder实例去最后执行的数据库update。 352 | 353 | ### Model写入 354 | 355 | 刚才说通过Eloquent Model获取模型时(在`newFromBuilder`方法里)会把Model实例的`exists`属性设置为true,那么对于新建的Model实例这个属性的值是false,在执行`save`方法时就会去执行`performInsert`方法 356 | 357 | protected function performInsert(Builder $query) 358 | { 359 | if ($this->fireModelEvent('creating') === false) { 360 | return false; 361 | } 362 | //设置created_at和updated_at属性 363 | if ($this->usesTimestamps()) { 364 | $this->updateTimestamps(); 365 | } 366 | 367 | $attributes = $this->attributes; 368 | //如果表的主键自增insert数据并把新记录的id设置到属性里 369 | if ($this->getIncrementing()) { 370 | $this->insertAndSetId($query, $attributes); 371 | } 372 | //否则直接简单的insert 373 | else { 374 | if (empty($attributes)) { 375 | return true; 376 | } 377 | 378 | $query->insert($attributes); 379 | } 380 | 381 | // 把exists设置成true, 下次在save就会去执行update了 382 | $this->exists = true; 383 | 384 | $this->wasRecentlyCreated = true; 385 | //触发created事件 386 | $this->fireModelEvent('created', false); 387 | 388 | return true; 389 | } 390 | 391 | 392 | `performInsert`里如果表是主键自增的,那么在insert后会设置新记录主键ID的值到Model实例的属性里,同时还会帮我们维护时间字段和`exists`属性。 393 | 394 | ### Model删除 395 | 396 | Eloquent Model的delete操作也是一样, 通过Eloquent Builder去执行数据库连接的Query Builder里的delete方法删除数据库记录: 397 | 398 | //Eloquent Model 399 | public function delete() 400 | { 401 | if (is_null($this->getKeyName())) { 402 | throw new Exception('No primary key defined on model.'); 403 | } 404 | 405 | if (! $this->exists) { 406 | return; 407 | } 408 | 409 | if ($this->fireModelEvent('deleting') === false) { 410 | return false; 411 | } 412 | 413 | $this->touchOwners(); 414 | 415 | $this->performDeleteOnModel(); 416 | 417 | $this->fireModelEvent('deleted', false); 418 | 419 | return true; 420 | } 421 | 422 | protected function performDeleteOnModel() 423 | { 424 | $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete(); 425 | 426 | $this->exists = false; 427 | } 428 | 429 | //Eloquent Builder 430 | public function delete() 431 | { 432 | if (isset($this->onDelete)) { 433 | return call_user_func($this->onDelete, $this); 434 | } 435 | 436 | return $this->toBase()->delete(); 437 | } 438 | 439 | //Query Builder 440 | public function delete($id = null) 441 | { 442 | if (! is_null($id)) { 443 | $this->where($this->from.'.id', '=', $id); 444 | } 445 | 446 | return $this->connection->delete( 447 | $this->grammar->compileDelete($this), $this->cleanBindings( 448 | $this->grammar->prepareBindingsForDelete($this->bindings) 449 | ) 450 | ); 451 | } 452 | 453 | Query Builder的实现细节我们在上一篇文章里已经说过了这里不再赘述,如果好奇Query Builder是怎么执行SQL操作的可以回去翻看上一篇文章。 454 | 455 | ### 总结 456 | 457 | 本文我们详细地看了Eloquent Model是怎么执行CRUD的,就像开头说的Eloquent Model通过Eloquent Builder来完成数据库操作,而Eloquent Builder是在Query Builder的基础上做了进一步封装, Eloquent Builder会把这些CRUD方法的调用转给Query Builder里对应的方法来完成操作,所以在Query Builder里能使用的方法到Eloquent Model中同样都能使用。 458 | 459 | 除了对数据表、基本的CRUD的抽象外,模型另外的一个重要的特点是模型关联,它帮助我们优雅的解决了数据表间的关联关系。我们在之后的文章再来详细看模型关联部分的实现。 460 | 461 | 上一篇: [Database 查询构建器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database2.md) 462 | 463 | 下一篇: [Database 模型关联](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database4.md) 464 | -------------------------------------------------------------------------------- /articles/DecoratorPattern.md: -------------------------------------------------------------------------------- 1 | # 装饰模式 (Decorator Pattern) 2 | 3 | 装饰模式能够实现动态的为对象添加功能,是从一个对象外部来给对象添加功能。通常有两种方式可以实现给一个类或对象增加行为: 4 | 5 | - 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。 6 | - 组合机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator) 7 | 8 | 显然,为了扩展对象功能频繁修改父类或者派生子类这种方式并不可取。在面向对象的设计中,我们应该尽量使用对象组合,而不是对象继承来扩展和复用功能。装饰器模式就是基于对象组合的方式,可以很灵活的给对象添加所需要的功能。装饰器模式的本质就是动态组合。动态是手段,组合才是目的。总之,装饰模式是通过把复杂的功能简单化,分散化,然后在运行期间,根据需要来动态组合的这样一个模式。 9 | 10 | 11 | 12 | ### 装饰模式定义 13 | 14 | 装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。 15 | 16 | ### 装饰模式的优点 17 | 18 | - 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。 19 | - 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。 20 | - 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。 21 | 22 | ### 模式结构和说明 23 | 24 | ![装饰器模式UML](https://user-gold-cdn.xitu.io/2018/6/3/163c449dbb766f7d) 25 | 26 | - 聚合关系用一条带空心菱形箭头的直线表示,上图表示Component聚合到Decorator上,或者说Decorator由Component组成。 27 | - 继承关系用一条带空心箭头的直接表示 28 | - [看懂UML类图请看这个文档](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html) 29 | 30 | **Component**:组件对象的接口,可以给这些对象动态的添加职责; 31 | 32 | **ConcreteComponent**:具体的组件对象,实现了组件接口。该对象通常就是被装饰器装饰的原始对象,可以给这个对象添加职责; 33 | 34 | **Decorator**:所有装饰器的父类,需要定义一个与Component接口一致的接口(主要是为了实现装饰器功能的复用,即具体的装饰器A可以装饰另外一个具体的装饰器B,因为装饰器类也是一个Component),并持有一个Component对象,该对象其实就是被装饰的对象。如果不继承Component接口类,则只能为某个组件添加单一的功能,即装饰器对象不能再装饰其他的装饰器对象。 35 | 36 | **ConcreteDecorator**:具体的装饰器类,实现具体要向被装饰对象添加的功能。用来装饰具体的组件对象或者另外一个具体的装饰器对象。 37 | 38 | 39 | 40 | ### 装饰器的示例代码 41 | 42 | 1.Component抽象类, 可以给这些对象动态的添加职责 43 | 44 | ``` 45 | abstract class Component 46 | { 47 | abstract public function operation(); 48 | } 49 | ``` 50 | 51 | 2.Component的实现类 52 | 53 | ``` 54 | class ConcreteComponent extends Component 55 | { 56 | public function operation() 57 | { 58 | echo __CLASS__ . '|' . __METHOD__ . "\r\n"; 59 | } 60 | } 61 | ``` 62 | 63 | 3.装饰器的抽象类,维持一个指向组件对象的接口对象, 并定义一个与组件接口一致的接口 64 | 65 | ``` 66 | abstract class Decorator extends Component 67 | { 68 | /** 69 | * 持有Component的对象 70 | */ 71 | protected $component; 72 | 73 | /** 74 | * 构造方法传入 75 | */ 76 | public function __construct(Component $component) 77 | { 78 | $this->component = $component; 79 | } 80 | 81 | abstract public function operation(); 82 | } 83 | ``` 84 | 85 | 4.装饰器的具体实现类,向组件对象添加职责,beforeOperation(),afterOperation()为前后添加的职责。 86 | 87 | ``` 88 | class ConcreteDecoratorA extends Decorator 89 | { 90 | //在调用父类的operation方法的前置操作 91 | public function beforeOperation() 92 | { 93 | echo __CLASS__ . '|' . __METHOD__ . "\r\n"; 94 | } 95 | 96 | //在调用父类的operation方法的后置操作 97 | public function afterOperation() 98 | { 99 | echo __CLASS__ . '|' . __METHOD__ . "\r\n"; 100 | } 101 | 102 | public function operation() 103 | { 104 | $this->beforeOperation(); 105 | $this->component->operation();//这里可以选择性的调用父类的方法,如果不调用则相当于完全改写了方法,实现了新的功能 106 | $this->afterOperation(); 107 | } 108 | } 109 | 110 | class ConcreteDecoratorB extends Decorator 111 | { 112 | //在调用父类的operation方法的前置操作 113 | public function beforeOperation() 114 | { 115 | echo __CLASS__ . '|' . __METHOD__ . "\r\n"; 116 | } 117 | 118 | //在调用父类的operation方法的后置操作 119 | public function afterOperation() 120 | { 121 | echo __CLASS__ . '|' . __METHOD__ . "\r\n"; 122 | } 123 | 124 | public function operation() 125 | { 126 | $this->beforeOperation(); 127 | $this->component->operation();//这里可以选择性的调用父类的方法,如果不调用则相当于完全改写了方法,实现了新的功能 128 | $this->afterOperation(); 129 | } 130 | } 131 | ``` 132 | 133 | 5.客户端使用装饰器 134 | 135 | ``` 136 | class Client 137 | { 138 | public function main() 139 | { 140 | $component = new ConcreteComponent(); 141 | $decoratorA = new ConcreteDecoratorA($component); 142 | $decoratorB = new ConcreteDecoratorB($decoratorA); 143 | $decoratorB->operation(); 144 | } 145 | } 146 | 147 | $client = new Client(); 148 | $client->main(); 149 | ``` 150 | 151 | 6.运行结果 152 | 153 | ``` 154 | oncreteDecoratorB|ConcreteDecoratorB::beforeOperation 155 | ConcreteDecoratorA|ConcreteDecoratorA::beforeOperation 156 | ConcreteComponent|ConcreteComponent::operation 157 | ConcreteDecoratorA|ConcreteDecoratorA::afterOperation 158 | ConcreteDecoratorB|ConcreteDecoratorB::afterOperation 159 | ``` 160 | 161 | 162 | 163 | ### 装饰模式需要注意的问题 164 | 165 | - 一个装饰类的接口必须与被装饰类的接口保持相同,对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待。 166 | - 尽量保持具体组件类ConcreteComponent的轻量,不要把主逻辑之外的辅助逻辑和状态放在具体组件类中,可以通过装饰类对其进行扩展。 如果只有一个具体组件类而没有抽象组件类,那么抽象装饰类可以作为具体组件类的直接子类。 167 | 168 | 169 | 170 | ### 适用环境 171 | 172 | - 需要在不影响组件对象的情况下,以动态、透明的方式给对象添加职责。 173 | - 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时可以考虑使用装饰类。 174 | 175 | 上一篇: [路由](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Route.md) 176 | 177 | 下一篇: [中间件](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Middleware.md) 178 | -------------------------------------------------------------------------------- /articles/ENV.md: -------------------------------------------------------------------------------- 1 | `Laravel`在启动时会加载项目中的`.env`文件。对于应用程序运行的环境来说,不同的环境有不同的配置通常是很有用的。 例如,你可能希望在本地使用测试的`Mysql`数据库而在上线后希望项目能够自动切换到生产`Mysql`数据库。本文将会详细介绍 `env` 文件的使用与源码的分析。 2 | 3 | 4 | 5 | ## Env文件的使用 6 | 7 | ### 多环境env的设置 8 | 9 | 项目中`env`文件的数量往往是跟项目的环境数量相同,假如一个项目有开发、测试、生产三套环境那么在项目中应该有三个`.env.dev`、`.env.test`、`.env.prod`三个环境配置文件与环境相对应。三个文件中的配置项应该完全一样,而具体配置的值应该根据每个环境的需要来设置。 10 | 11 | 接下来就是让项目能够根据环境加载不同的`env`文件了。具体有三种方法,可以按照使用习惯来选择使用: 12 | 13 | - 在环境的nginx配置文件里设置`APP_ENV`环境变量`fastcgi_param APP_ENV dev;` 14 | 15 | - 设置服务器上运行PHP的用户的环境变量,比如在`www`用户的`/home/www/.bashrc`中添加`export APP_ENV dev` 16 | 17 | - 在部署项目的持续集成任务或者部署脚本里执行`cp .env.dev .env ` 18 | 19 | 针对前两种方法,`Laravel`会根据`env('APP_ENV')`加载到的变量值去加载对应的文件`.env.dev`、`.env.test`这些。 具体在后面源码里会说,第三种比较好理解就是在部署项目时将环境的配置文件覆盖到`.env`文件里这样就不需要在环境的系统和`nginx`里做额外的设置了。 20 | 21 | ### 自定义env文件的路径与文件名 22 | 23 | `env`文件默认放在项目的根目录中,`laravel` 为用户提供了自定义 `ENV` 文件路径或文件名的函数, 24 | 25 | 例如,若想要自定义 `env` 路径,可以在 `bootstrap` 文件夹中 `app.php` 中使用`Application`实例的`useEnvironmentPath`方法: 26 | 27 | ```php 28 | $app = new Illuminate\Foundation\Application( 29 | realpath(__DIR__.'/../') 30 | ); 31 | 32 | $app->useEnvironmentPath('/customer/path') 33 | ``` 34 | 35 | 若想要自定义 `env` 文件名称,就可以在 `bootstrap` 文件夹中 `app.php` 中使用`Application`实例的`loadEnvironmentFrom`方法: 36 | 37 | ```php 38 | $app = new Illuminate\Foundation\Application( 39 | realpath(__DIR__.'/../') 40 | ); 41 | 42 | $app->loadEnvironmentFrom('customer.env') 43 | ``` 44 | 45 | 46 | 47 | ## Laravel 加载ENV配置 48 | 49 | `Laravel`加载`ENV`的是在框架处理请求之前,bootstrap过程中的`LoadEnvironmentVariables`阶段中完成的。 50 | 51 | 我们来看一下`\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables`的源码来分析下`Laravel`是怎么加载`env`中的配置的。 52 | 53 | 54 | 55 | ``` 56 | configurationIsCached()) { 76 | return; 77 | } 78 | 79 | $this->checkForSpecificEnvironmentFile($app); 80 | 81 | try { 82 | (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); 83 | } catch (InvalidPathException $e) { 84 | // 85 | } 86 | } 87 | 88 | /** 89 | * Detect if a custom environment file matching the APP_ENV exists. 90 | * 91 | * @param \Illuminate\Contracts\Foundation\Application $app 92 | * @return void 93 | */ 94 | protected function checkForSpecificEnvironmentFile($app) 95 | { 96 | if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption('--env')) { 97 | if ($this->setEnvironmentFilePath( 98 | $app, $app->environmentFile().'.'.$input->getParameterOption('--env') 99 | )) { 100 | return; 101 | } 102 | } 103 | 104 | if (! env('APP_ENV')) { 105 | return; 106 | } 107 | 108 | $this->setEnvironmentFilePath( 109 | $app, $app->environmentFile().'.'.env('APP_ENV') 110 | ); 111 | } 112 | 113 | /** 114 | * Load a custom environment file. 115 | * 116 | * @param \Illuminate\Contracts\Foundation\Application $app 117 | * @param string $file 118 | * @return bool 119 | */ 120 | protected function setEnvironmentFilePath($app, $file) 121 | { 122 | if (file_exists($app->environmentPath().'/'.$file)) { 123 | $app->loadEnvironmentFrom($file); 124 | 125 | return true; 126 | } 127 | 128 | return false; 129 | } 130 | } 131 | ``` 132 | 133 | 在他的启动方法`bootstrap`中,`Laravel`会检查配置是否缓存过以及判断应该应用那个`env`文件,针对上面说的根据环境加载配置文件的三种方法中的头两种,因为系统或者nginx环境变量中设置了`APP_ENV`,所以Laravel会在`checkForSpecificEnvironmentFile`方法里根据 `APP_ENV`的值设置正确的配置文件的具体路径, 比如`.env.dev`或者`.env.test`,而针对第三中情况则是默认的`.env`, 具体可以参看下面的`checkForSpecificEnvironmentFile`还有相关的Application里的两个方法的源码: 134 | 135 | ``` 136 | protected function checkForSpecificEnvironmentFile($app) 137 | { 138 | if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption('--env')) { 139 | if ($this->setEnvironmentFilePath( 140 | $app, $app->environmentFile().'.'.$input->getParameterOption('--env') 141 | )) { 142 | return; 143 | } 144 | } 145 | 146 | if (! env('APP_ENV')) { 147 | return; 148 | } 149 | 150 | $this->setEnvironmentFilePath( 151 | $app, $app->environmentFile().'.'.env('APP_ENV') 152 | ); 153 | } 154 | 155 | namespace Illuminate\Foundation; 156 | class Application .... 157 | { 158 | 159 | public function environmentPath() 160 | { 161 | return $this->environmentPath ?: $this->basePath; 162 | } 163 | 164 | public function environmentFile() 165 | { 166 | return $this->environmentFile ?: '.env'; 167 | } 168 | } 169 | ``` 170 | 171 | 判断好后要读取的配置文件的路径后,接下来就是加载`env`里的配置了。 172 | 173 | ```php 174 | (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); 175 | ``` 176 | 177 | `Laravel`使用的是`Dotenv`的PHP版本`vlucas/phpdotenv` 178 | 179 | ```php 180 | class Dotenv 181 | { 182 | public function __construct($path, $file = '.env') 183 | { 184 | $this->filePath = $this->getFilePath($path, $file); 185 | $this->loader = new Loader($this->filePath, true); 186 | } 187 | 188 | public function load() 189 | { 190 | return $this->loadData(); 191 | } 192 | 193 | protected function loadData($overload = false) 194 | { 195 | $this->loader = new Loader($this->filePath, !$overload); 196 | 197 | return $this->loader->load(); 198 | } 199 | } 200 | ``` 201 | 202 | 它依赖`/Dotenv/Loader`来加载数据: 203 | 204 | ```php 205 | class Loader 206 | { 207 | public function load() 208 | { 209 | $this->ensureFileIsReadable(); 210 | 211 | $filePath = $this->filePath; 212 | $lines = $this->readLinesFromFile($filePath); 213 | foreach ($lines as $line) { 214 | if (!$this->isComment($line) && $this->looksLikeSetter($line)) { 215 | $this->setEnvironmentVariable($line); 216 | } 217 | } 218 | 219 | return $lines; 220 | } 221 | } 222 | ``` 223 | 224 | `Loader`读取配置时`readLinesFromFile`函数会用`file`函数将配置从文件中一行行地读取到数组中去,然后排除以`#`开头的注释,针对内容中包含`=`的行去调用`setEnvironmentVariable`方法去把文件行中的环境变量配置到项目中去: 225 | 226 | ``` 227 | namespace Dotenv; 228 | class Loader 229 | { 230 | public function setEnvironmentVariable($name, $value = null) 231 | { 232 | list($name, $value) = $this->normaliseEnvironmentVariable($name, $value); 233 | 234 | $this->variableNames[] = $name; 235 | 236 | // Don't overwrite existing environment variables if we're immutable 237 | // Ruby's dotenv does this with `ENV[key] ||= value`. 238 | if ($this->immutable && $this->getEnvironmentVariable($name) !== null) { 239 | return; 240 | } 241 | 242 | // If PHP is running as an Apache module and an existing 243 | // Apache environment variable exists, overwrite it 244 | if (function_exists('apache_getenv') && function_exists('apache_setenv') && apache_getenv($name)) { 245 | apache_setenv($name, $value); 246 | } 247 | 248 | if (function_exists('putenv')) { 249 | putenv("$name=$value"); 250 | } 251 | 252 | $_ENV[$name] = $value; 253 | $_SERVER[$name] = $value; 254 | } 255 | 256 | public function getEnvironmentVariable($name) 257 | { 258 | switch (true) { 259 | case array_key_exists($name, $_ENV): 260 | return $_ENV[$name]; 261 | case array_key_exists($name, $_SERVER): 262 | return $_SERVER[$name]; 263 | default: 264 | $value = getenv($name); 265 | return $value === false ? null : $value; // switch getenv default to null 266 | } 267 | } 268 | } 269 | ``` 270 | 271 | `Dotenv`实例化`Loader`的时候把`Loader`对象的`$immutable`属性设置成了`false`,`Loader`设置变量的时候如果通过`getEnvironmentVariable`方法读取到了变量值,那么就会跳过该环境变量的设置。所以`Dotenv`默认情况下不会覆盖已经存在的环境变量,这个很关键,比如说在`docker`的容器编排文件里,我们会给`PHP`应用容器设置关于`Mysql`容器的两个环境变量 272 | 273 | ``` 274 | environment: 275 | - "DB_PORT=3306" 276 | - "DB_HOST=database" 277 | ``` 278 | 279 | 这样在容器里设置好环境变量后,即使`env`文件里的`DB_HOST`为`homestead`用`env`函数读取出来的也还是容器里之前设置的`DB_HOST`环境变量的值`database`(docker中容器链接默认使用服务名称,在编排文件中我把mysql容器的服务名称设置成了database, 所以php容器要通过database这个host来连接mysql容器)。因为用我们在持续集成中做自动化测试的时候通常都是在容器里进行测试,所以`Dotenv`不会覆盖已存在环境变量这个行为就相当重要这样我就可以只设置容器里环境变量的值完成测试而不用更改项目里的`env`文件,等到测试完成后直接去将项目部署到环境上就可以了。 280 | 281 | 如果检查环境变量不存在那么接着Dotenv就会把环境变量通过PHP内建函数`putenv`设置到环境中去,同时也会存储到`$_ENV`和`$_SERVER`这两个全局变量中。 282 | 283 | ## 在项目中读取env配置 284 | 285 | 在Laravel应用程序中可以使用`env()`函数去读取环境变量的值,比如获取数据库的HOST: 286 | 287 | ``` 288 | env('DB_HOST`, 'localhost'); 289 | ``` 290 | 291 | 传递给 `env` 函数的第二个值是「默认值」。如果给定的键不存在环境变量,则会使用该值。 292 | 293 | 我们来看看`env`函数的源码: 294 | 295 | ``` 296 | function env($key, $default = null) 297 | { 298 | $value = getenv($key); 299 | 300 | if ($value === false) { 301 | return value($default); 302 | } 303 | 304 | switch (strtolower($value)) { 305 | case 'true': 306 | case '(true)': 307 | return true; 308 | case 'false': 309 | case '(false)': 310 | return false; 311 | case 'empty': 312 | case '(empty)': 313 | return ''; 314 | case 'null': 315 | case '(null)': 316 | return; 317 | } 318 | 319 | if (strlen($value) > 1 && Str::startsWith($value, '"') && Str::endsWith($value, '"')) { 320 | return substr($value, 1, -1); 321 | } 322 | 323 | return $value; 324 | } 325 | ``` 326 | 327 | 它直接通过`PHP`内建函数`getenv`读取环境变量。 328 | 329 | 我们看到了在加载配置和读取配置的时候,使用了`putenv`和`getenv`两个函数。`putenv`设置的环境变量只在请求期间存活,请求结束后会恢复环境之前的设置。因为如果php.ini中的`variables_order`配置项成了 `GPCS`不包含`E`的话,那么php程序中是无法通过`$_ENV`读取环境变量的,所以使用`putenv`动态地设置环境变量让开发人员不用去关注服务器上的配置。而且在服务器上给运行用户配置的环境变量会共享给用户启动的所有进程,这就不能很好的保护比如`DB_PASSWORD`、`API_KEY`这种私密的环境变量,所以这种配置用`putenv`设置能更好的保护这些配置信息,`getenv`方法能获取到系统的环境变量和`putenv`动态设置的环境变量。 330 | 331 | 上一篇: [Contracts契约](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Contracts.md) 332 | 333 | 下一篇: [HTTP内核](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/HttpKernel.md) 334 | -------------------------------------------------------------------------------- /articles/Event.md: -------------------------------------------------------------------------------- 1 | # 事件系统 2 | 3 | Laravel 的事件提供了一个简单的观察者实现,能够订阅和监听应用中发生的各种事件。事件机制是一种很好的应用解耦方式,因为一个事件可以拥有多个互不依赖的监听器。`laravel` 中事件系统由两部分构成,一个是事件的名称,事件的名称可以是个字符串,例如 `event.email`,也可以是一个事件类,例如 `App\Events\OrderShipped`;另一个是事件的 监听器`listener`,可以是一个闭包,还可以是监听类,例如 `App\Listeners\SendShipmentNotification`。 4 | 5 | 6 | 7 | 我们还是通过官方文档里给出的这个例子来向下分析事件系统的源码实现,不过在应用注册事件和监听器之前,Laravel在应用启动时会先注册处理事件用的`events`服务。 8 | 9 | ### Laravel注册事件服务 10 | 11 | Laravel应用在创建时注册的基础服务里就有`Event`服务 12 | 13 | ``` 14 | namespace Illuminate\Foundation; 15 | 16 | class Application extends Container implements ... 17 | { 18 | public function __construct($basePath = null) 19 | { 20 | ... 21 | $this->registerBaseServiceProviders(); 22 | ... 23 | } 24 | 25 | protected function registerBaseServiceProviders() 26 | { 27 | $this->register(new EventServiceProvider($this)); 28 | 29 | $this->register(new LogServiceProvider($this)); 30 | 31 | $this->register(new RoutingServiceProvider($this)); 32 | } 33 | } 34 | ``` 35 | 36 | 其中的 `EventServiceProvider` 是 `/Illuminate/Events/EventServiceProvider` 37 | 38 | ``` 39 | public function register() 40 | { 41 | $this->app->singleton('events', function ($app) { 42 | return (new Dispatcher($app))->setQueueResolver(function () use ($app) { 43 | return $app->make(QueueFactoryContract::class); 44 | }); 45 | }); 46 | } 47 | ``` 48 | 49 | `Illuminate\Events\Dispatcher` 就是 `events`服务真正的实现类,而`Event`门面时`events`服务的静态代理,事件系统相关的方法都是由`Illuminate\Events\Dispatcher`来提供的。 50 | 51 | ### 应用中注册事件和监听 52 | 53 | 我们还是通过官方文档里给出的这个例子来向下分析事件系统的源码实现,注册事件和监听器有两种方法,`App\Providers\EventServiceProvider` 有个 `listen` 数组包含所有的事件(键)以及事件对应的监听器(值)来注册所有的事件监听器,可以灵活地根据需求来添加事件。 54 | 55 | ``` 56 | /** 57 | * 应用程序的事件监听器映射。 58 | * 59 | * @var array 60 | */ 61 | protected $listen = [ 62 | 'App\Events\OrderShipped' => [ 63 | 'App\Listeners\SendShipmentNotification', 64 | ], 65 | ]; 66 | ``` 67 | 68 | 也可以在 `App\Providers\EventServiceProvider` 类的 `boot` 方法中注册基于事件的闭包。 69 | 70 | ``` 71 | /** 72 | * 注册应用程序中的任何其他事件。 73 | * 74 | * @return void 75 | */ 76 | public function boot() 77 | { 78 | parent::boot(); 79 | 80 | Event::listen('event.name', function ($foo, $bar) { 81 | // 82 | }); 83 | } 84 | ``` 85 | 86 | 87 | 88 | 可以看到`\App\Providers\EventProvider`类的主要工作就是注册应用中的事件,这个注册类的主要作用是事件系统的启动,这个类继承自 `\Illuminate\Foundation\Support\Providers\EventServiceProvide`。 89 | 90 | 我们在将服务提供器的时候说过,Laravel应用在注册完所有的服务后会通过`\Illuminate\Foundation\Bootstrap\BootProviders`调用所有Provider的`boot`方法来启动这些服务,所以Laravel应用中事件和监听器的注册就发生在 `\Illuminate\Foundation\Support\Providers\EventServiceProvide`类的`boot`方法中,我们来看一下: 91 | 92 | ``` 93 | public function boot() 94 | { 95 | foreach ($this->listens() as $event => $listeners) { 96 | foreach ($listeners as $listener) { 97 | Event::listen($event, $listener); 98 | } 99 | } 100 | 101 | foreach ($this->subscribe as $subscriber) { 102 | Event::subscribe($subscriber); 103 | } 104 | } 105 | ``` 106 | 107 | 可以看到事件系统的启动是通过`events`服务的监听和订阅方法来创建事件与对应的监听器还有系统里的事件订阅者。 108 | 109 | ``` 110 | namespace Illuminate\Events; 111 | class Dispatcher implements DispatcherContract 112 | { 113 | public function listen($events, $listener) 114 | { 115 | foreach ((array) $events as $event) { 116 | if (Str::contains($event, '*')) { 117 | $this->setupWildcardListen($event, $listener); 118 | } else { 119 | $this->listeners[$event][] = $this->makeListener($listener); 120 | } 121 | } 122 | } 123 | 124 | protected function setupWildcardListen($event, $listener) 125 | { 126 | $this->wildcards[$event][] = $this->makeListener($listener, true); 127 | } 128 | } 129 | ``` 130 | 131 | 对于包含通配符的事件名,会被统一放入 `wildcards` 数组中,`makeListener`是用来创建事件对应的`listener`的: 132 | 133 | ``` 134 | class Dispatcher implements DispatcherContract 135 | { 136 | public function makeListener($listener, $wildcard = false) 137 | { 138 | if (is_string($listener)) {//如果是监听器是类,去创建监听类 139 | return $this->createClassListener($listener, $wildcard); 140 | } 141 | 142 | return function ($event, $payload) use ($listener, $wildcard) { 143 | if ($wildcard) { 144 | return $listener($event, $payload); 145 | } else { 146 | return $listener(...array_values($payload)); 147 | } 148 | }; 149 | } 150 | } 151 | ``` 152 | 153 | 创建`listener`的时候,会判断监听对象是监听类还是闭包函数。 154 | 155 | 对于闭包监听来说,`makeListener` 会再包装一层返回一个闭包函数作为事件的监听者。 156 | 157 | 对于监听类来说,会继续通过 `createClassListener` 来创建监听者 158 | 159 | ``` 160 | class Dispatcher implements DispatcherContract 161 | { 162 | public function createClassListener($listener, $wildcard = false) 163 | { 164 | return function ($event, $payload) use ($listener, $wildcard) { 165 | if ($wildcard) { 166 | return call_user_func($this->createClassCallable($listener), $event, $payload); 167 | } else { 168 | return call_user_func_array( 169 | $this->createClassCallable($listener), $payload 170 | ); 171 | } 172 | }; 173 | } 174 | 175 | protected function createClassCallable($listener) 176 | { 177 | list($class, $method) = $this->parseClassCallable($listener); 178 | 179 | if ($this->handlerShouldBeQueued($class)) { 180 | //如果当前监听类是队列的话,会将任务推送给队列 181 | return $this->createQueuedHandlerCallable($class, $method); 182 | } else { 183 | return [$this->container->make($class), $method]; 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | 对于通过监听类的字符串来创建监听者也是返回的一个闭包,如果当前监听类是要执行队列任务的话,返回的闭包是在执行后会将任务推送给队列,如果是普通监听类返回的闭包中会将监听对象make出来,执行对象的`handle`方法。 所以监听者返回闭包都是为了包装好事件注册时的上下文,等待事件触发的时候调用闭包来执行任务。 190 | 191 | 创建完listener后就会把它放到`listener`数组中以对应的事件名称为键的数组里,在`listener`数组中一个事件名称对应的数组里可以有多个`listener`, 就像我们之前讲观察者模式时`Subject`类中的`observers`数组一样,只不过Laravel比那个复杂一些,它的`listener`数组里会记录多个`Subject`和对应`观察者`的对应关系。 192 | 193 | ### 触发事件 194 | 195 | 可以用事件名或者事件类的对象来触发事件,触发事件时用的是`Event::fire(new OrdershipmentNotification)`, 同样它也来自`events`服务 196 | 197 | ``` 198 | public function fire($event, $payload = [], $halt = false) 199 | { 200 | return $this->dispatch($event, $payload, $halt); 201 | } 202 | 203 | public function dispatch($event, $payload = [], $halt = false) 204 | { 205 | //如果参数$event事件对象,那么就将对象的类名作为事件名称,对象本身作为携带数据的荷载通过`listener`方法 206 | //的$payload参数的实参传递给listener 207 | list($event, $payload) = $this->parseEventAndPayload( 208 | $event, $payload 209 | ); 210 | 211 | if ($this->shouldBroadcast($payload)) { 212 | $this->broadcastEvent($payload[0]); 213 | } 214 | 215 | $responses = []; 216 | 217 | foreach ($this->getListeners($event) as $listener) { 218 | $response = $listener($event, $payload); 219 | 220 | //如果触发事件时传递了halt参数,并且listener返回了值,那么就不会再去调用事件剩下的listener 221 | //否则就将返回值加入到返回值列表中,等所有listener执行完了一并返回 222 | if ($halt && ! is_null($response)) { 223 | return $response; 224 | } 225 | //如果一个listener返回了false, 那么将不会再调用事件剩下的listener 226 | if ($response === false) { 227 | break; 228 | } 229 | 230 | $responses[] = $response; 231 | } 232 | 233 | return $halt ? null : $responses; 234 | } 235 | 236 | protected function parseEventAndPayload($event, $payload) 237 | { 238 | if (is_object($event)) { 239 | list($payload, $event) = [[$event], get_class($event)]; 240 | } 241 | 242 | return [$event, Arr::wrap($payload)]; 243 | } 244 | 245 | //获取事件名对应的所有listener 246 | public function getListeners($eventName) 247 | { 248 | $listeners = isset($this->listeners[$eventName]) ? $this->listeners[$eventName] : []; 249 | 250 | $listeners = array_merge( 251 | $listeners, $this->getWildcardListeners($eventName) 252 | ); 253 | 254 | return class_exists($eventName, false) 255 | ? $this->addInterfaceListeners($eventName, $listeners) 256 | : $listeners; 257 | } 258 | ``` 259 | 260 | 261 | 262 | 事件触发后,会从之前注册事件生成的`listeners`中找到事件名称对应的所有`listener`闭包,然后调用这些闭包来执行监听器中的任务,需要注意的是: 263 | 264 | - 如果事件名参数事件对象,那么会用事件对象的类名作为事件名,其本身会作为时间参数传递给listener。 265 | - 如果触发事件时传递了halt参数,在listener返回非`false`后那么事件就不会往下继续传播给剩余的listener了,否则所有listener的返回值会在所有listener执行往后作为一个数组统一返回。 266 | - 如果一个listener返回了布尔值`false`那么事件会立即停止向剩余的listener传播。 267 | 268 | 269 | 270 | Laravel的事件系统原理还是跟之前讲的观察者模式一样,不过框架的作者功力深厚,巧妙的结合应用了闭包来实现了事件系统,还有针对需要队列处理的事件,应用事件在一些比较复杂的业务场景中能利用关注点分散原则有效地解耦应用中的代码逻辑,当然也不是什么情况下都能适合应用事件来编写代码,我之前写过一篇文章[Laravel事件驱动编程](https://juejin.im/post/5b1f5db05188257d7270a194)来说明事件的应用场景,感兴趣的可以去看看。 271 | 272 | 上一篇: [观察者模式](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Observer.md) 273 | 274 | 下一篇: [用户认证系统(基础介绍)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Auth1.md) 275 | -------------------------------------------------------------------------------- /articles/Exception.md: -------------------------------------------------------------------------------- 1 | 异常处理是编程中十分重要但也最容易被人忽视的语言特性,它为开发者提供了处理程序运行时错误的机制,对于程序设计来说正确的异常处理能够防止泄露程序自身细节给用户,给开发者提供完整的错误回溯堆栈,同时也能提高程序的健壮性。 2 | 3 | 这篇文章我们来简单梳理一下Laravel中提供的异常处理能力,然后讲一些在开发中使用异常处理的实践,如何使用自定义异常、如何扩展Laravel的异常处理能力。 4 | 5 | 6 | ### 注册异常Handler 7 | 8 | 这里又要回到我们说过很多次的Kernel处理请求前的bootstrap阶段,在bootstrap阶段的`Illuminate\Foundation\Bootstrap\HandleExceptions` 部分中Laravel设置了系统异常处理行为并注册了全局的异常处理器: 9 | 10 | ``` 11 | class HandleExceptions 12 | { 13 | public function bootstrap(Application $app) 14 | { 15 | $this->app = $app; 16 | 17 | error_reporting(-1); 18 | 19 | set_error_handler([$this, 'handleError']); 20 | 21 | set_exception_handler([$this, 'handleException']); 22 | 23 | register_shutdown_function([$this, 'handleShutdown']); 24 | 25 | if (! $app->environment('testing')) { 26 | ini_set('display_errors', 'Off'); 27 | } 28 | } 29 | 30 | 31 | public function handleError($level, $message, $file = '', $line = 0, $context = []) 32 | { 33 | if (error_reporting() & $level) { 34 | throw new ErrorException($message, 0, $level, $file, $line); 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | `set_exception_handler([$this, 'handleException'])`将`HandleExceptions`的`handleException`方法注册为程序的全局处理器方法: 41 | 42 | ``` 43 | public function handleException($e) 44 | { 45 | if (! $e instanceof Exception) { 46 | $e = new FatalThrowableError($e); 47 | } 48 | 49 | $this->getExceptionHandler()->report($e); 50 | 51 | if ($this->app->runningInConsole()) { 52 | $this->renderForConsole($e); 53 | } else { 54 | $this->renderHttpResponse($e); 55 | } 56 | } 57 | 58 | protected function getExceptionHandler() 59 | { 60 | return $this->app->make(ExceptionHandler::class); 61 | } 62 | 63 | // 渲染CLI请求的异常响应 64 | protected function renderForConsole(Exception $e) 65 | { 66 | $this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); 67 | } 68 | 69 | // 渲染HTTP请求的异常响应 70 | protected function renderHttpResponse(Exception $e) 71 | { 72 | $this->getExceptionHandler()->render($this->app['request'], $e)->send(); 73 | } 74 | ``` 75 | 76 | 在处理器里主要通过`ExceptionHandler`的`report`方法上报异常、这里是记录异常到`storage/laravel.log`文件中,然后根据请求类型渲染异常的响应生成输出给到客户端。这里的ExceptionHandler就是`\App\Exceptions\Handler`类的实例,它是在项目最开始注册到服务容器中的: 77 | 78 | ``` 79 | // bootstrap/app.php 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Create The Application 84 | |-------------------------------------------------------------------------- 85 | */ 86 | 87 | $app = new Illuminate\Foundation\Application( 88 | realpath(__DIR__.'/../') 89 | ); 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Bind Important Interfaces 94 | |-------------------------------------------------------------------------- 95 | */ 96 | ...... 97 | 98 | $app->singleton( 99 | Illuminate\Contracts\Debug\ExceptionHandler::class, 100 | App\Exceptions\Handler::class 101 | ); 102 | 103 | ``` 104 | 105 | 这里再顺便说一下`set_error_handler`函数,它的作用是注册错误处理器函数,因为在一些年代久远的代码或者类库中大多是采用PHP那件函数`trigger_error`函数来抛出错误的,异常处理器只能处理Exception不能处理Error,所以为了能够兼容老类库通常都会使用`set_error_handler`注册全局的错误处理器方法,在方法中捕获到错误后将错误转化成异常再重新抛出,这样项目中所有的代码没有被正确执行时都能抛出异常实例了。 106 | 107 | ``` 108 | /** 109 | * Convert PHP errors to ErrorException instances. 110 | * 111 | * @param int $level 112 | * @param string $message 113 | * @param string $file 114 | * @param int $line 115 | * @param array $context 116 | * @return void 117 | * 118 | * @throws \ErrorException 119 | */ 120 | public function handleError($level, $message, $file = '', $line = 0, $context = []) 121 | { 122 | if (error_reporting() & $level) { 123 | throw new ErrorException($message, 0, $level, $file, $line); 124 | } 125 | } 126 | ``` 127 | 128 | 129 | 130 | ### 常用的Laravel异常实例 131 | 132 | `Laravel`中针对常见的程序异常情况抛出了相应的异常实例,这让开发者能够捕获这些运行时异常并根据自己的需要来做后续处理(比如:在catch中调用另外一个补救方法、记录异常到日志文件、发送报警邮件、短信) 133 | 134 | 在这里我列一些开发中常遇到异常,并说明他们是在什么情况下被抛出的,平时编码中一定要注意在程序里捕获这些异常做好异常处理才能让程序更健壮。 135 | 136 | - `Illuminate\Database\QueryException` Laravel中执行SQL语句发生错误时会抛出此异常,它也是使用率最高的异常,用来捕获SQL执行错误,比方执行Update语句时很多人喜欢判断SQL执行后判断被修改的行数来判断UPDATE是否成功,但有的情景里执行的UPDATE语句并没有修改记录值,这种情况就没法通过被修改函数来判断UPDATE是否成功了,另外在事务执行中如果捕获到QueryException 可以在catch代码块中回滚事务。 137 | - `Illuminate\Database\Eloquent\ModelNotFoundException` 通过模型的`findOrFail`和`firstOrFail`方法获取单条记录时如果没有找到会抛出这个异常(`find`和`first`找不到数据时会返回NULL)。 138 | - `Illuminate\Validation\ValidationException` 请求未通过Laravel的FormValidator验证时会抛出此异常。 139 | - `Illuminate\Auth\Access\AuthorizationException` 用户请求未通过Laravel的策略(Policy)验证时抛出此异常 140 | - `Symfony\Component\Routing\Exception\MethodNotAllowedException` 请求路由时HTTP Method不正确 141 | - `Illuminate\Http\Exceptions\HttpResponseException` Laravel的处理HTTP请求不成功时抛出此异常 142 | 143 | 144 | 145 | ### 扩展Laravel的异常处理器 146 | 147 | 上面说了Laravel把`\App\Exceptions\Handler` 注册成功了全局的异常处理器,代码中没有被`catch`到的异常,最后都会被`\App\Exceptions\Handler`捕获到,处理器先上报异常记录到日志文件里然后渲染异常响应再发送响应给客户端。但是自带的异常处理器的方法并不好用,很多时候我们想把异常上报到邮件或者是错误日志系统中,下面的例子是将异常上报到Sentry系统中,Sentry是一个错误收集服务非常好用: 148 | 149 | ``` 150 | public function report(Exception $exception) 151 | { 152 | if (app()->bound('sentry') && $this->shouldReport($exception)) { 153 | app('sentry')->captureException($exception); 154 | } 155 | 156 | parent::report($exception); 157 | } 158 | ``` 159 | 160 | 161 | 162 | 还有默认的渲染方法在表单验证时生成响应的JSON格式往往跟我们项目里统一的`JOSN`格式不一样这就需要我们自定义渲染方法的行为。 163 | 164 | ``` 165 | public function render($request, Exception $exception) 166 | { 167 | //如果客户端预期的是JSON响应, 在API请求未通过Validator验证抛出ValidationException后 168 | //这里来定制返回给客户端的响应. 169 | if ($exception instanceof ValidationException && $request->expectsJson()) { 170 | return $this->error(422, $exception->errors()); 171 | } 172 | 173 | if ($exception instanceof ModelNotFoundException && $request->expectsJson()) { 174 | //捕获路由模型绑定在数据库中找不到模型后抛出的NotFoundHttpException 175 | return $this->error(424, 'resource not found.'); 176 | } 177 | 178 | 179 | if ($exception instanceof AuthorizationException) { 180 | //捕获不符合权限时抛出的 AuthorizationException 181 | return $this->error(403, "Permission does not exist."); 182 | } 183 | 184 | return parent::render($request, $exception); 185 | } 186 | ``` 187 | 188 | 189 | 190 | 自定义后,在请求未通过`FormValidator`验证时会抛出`ValidationException`, 之后异常处理器捕获到异常后会把错误提示格式化为项目统一的JSON响应格式并输出给客户端。这样在我们的控制器中就完全省略了判断表单验证是否通过如果不通过再输出错误响应给客户端的逻辑了,将这部分逻辑交给了统一的异常处理器来执行能让控制器方法瘦身不少。 191 | 192 | 193 | 194 | ### 使用自定义异常 195 | 196 | 这部分内容其实不是针对`Laravel`框架自定义异常,在任何项目中都可以应用我这里说的自定义异常。 197 | 198 | 我见过很多人在`Repository`或者`Service`类的方法中会根据不同错误返回不同的数组,里面包含着响应的错误码和错误信息,这么做当然是可以满足开发需求的,但是并不能记录发生异常时的应用的运行时上下文,发生错误时没办法记录到上下文信息就非常不利于开发者进行问题定位。 199 | 200 | 下面的是一个自定义的异常类 201 | 202 | ``` 203 | namespace App\Exceptions\; 204 | 205 | use RuntimeException; 206 | use Throwable; 207 | 208 | class UserManageException extends RuntimeException 209 | { 210 | /** 211 | * The primitive arguments that triggered this exception 212 | * 213 | * @var array 214 | */ 215 | public $primitives; 216 | /** 217 | * QueueManageException constructor. 218 | * @param array $primitives 219 | * @param string $message 220 | * @param int $code 221 | * @param Throwable|null $previous 222 | */ 223 | public function __construct(array $primitives, $message = "", $code = 0, Throwable $previous = null) 224 | { 225 | parent::__construct($message, $code, $previous); 226 | $this->primitives = $primitives; 227 | } 228 | 229 | /** 230 | * get the primitive arguments that triggered this exception 231 | */ 232 | public function getPrimitives() 233 | { 234 | return $this->primitives; 235 | } 236 | } 237 | ``` 238 | 239 | 定义完异常类我们就能在代码逻辑中抛出异常实例了 240 | 241 | ``` 242 | class UserRepository 243 | { 244 | 245 | public function updateUserFavorites(User $user, $favoriteData) 246 | { 247 | ...... 248 | if (!$executionOne) { 249 | throw new UserManageException(func_get_args(), 'Update user favorites error', '501'); 250 | } 251 | 252 | ...... 253 | if (!$executionTwo) { 254 | throw new UserManageException(func_get_args(), 'Another Error', '502'); 255 | } 256 | 257 | return true; 258 | } 259 | } 260 | 261 | class UserController extends ... 262 | { 263 | public function updateFavorites(User $user, Request $request) 264 | { 265 | ....... 266 | $favoriteData = $request->input('favorites'); 267 | try { 268 | $this->userRepo->updateUserFavorites($user, $favoritesData); 269 | } catch (UserManageException $ex) { 270 | ....... 271 | } 272 | } 273 | } 274 | ``` 275 | 276 | 除了上面`Repository`列出的情况更多的时候我们是在捕获到上面列举的通用异常后在`catch`代码块中抛出与业务相关的更细化的异常实例方便开发者定位问题,我们将上面的`updateUserFavorites` 按照这种策略修改一下 277 | 278 | ``` 279 | public function updateUserFavorites(User $user, $favoriteData) 280 | { 281 | try { 282 | // database execution 283 | 284 | // database execution 285 | } catch (QueryException $queryException) { 286 | throw new UserManageException(func_get_args(), 'Error Message', '501' , $queryException); 287 | } 288 | 289 | return true; 290 | } 291 | ``` 292 | 293 | 在上面定义`UserMangeException`类的时候第四个参数`$previous`是一个实现了`Throwable`接口类实例,在这种情景下我们因为捕获到了`QueryException`的异常实例而抛出了`UserManagerException`的实例,然后通过这个参数将`QueryException`实例传递给`PHP`异常的堆栈,这提供给我们回溯整个异常的能力来获取更多上下文信息,而不是仅仅只是当前抛出的异常实例的上下文信息, 在错误收集系统可以使用类似下面的代码来获取所有异常的信息。 294 | 295 | ``` 296 | while($e instanceof \Exception) { 297 | echo $e->getMessage(); 298 | $e = $e->getPrevious(); 299 | } 300 | ``` 301 | 302 | 303 | 304 | 异常处理是`PHP`非常重要但又容易让开发者忽略的功能,这篇文章简单解释了`Laravel`内部异常处理的机制以及扩展`Laravel`异常处理的方式方法。更多的篇幅着重分享了一些异常处理的编程实践,这些正是我希望每个读者都能看明白并实践下去的一些编程习惯,包括之前分享的`Interface`的应用也是一样。 305 | 306 | 上一篇: [Console内核](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ConsoleKernel.md) 307 | -------------------------------------------------------------------------------- /articles/FacadePattern.md: -------------------------------------------------------------------------------- 1 | # 外观模式 2 | 3 | 外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。 4 | 5 | Laravel中我们常用到的`Route`、`Redis`、`Auth`这些Facade就是外观模式的具体实现, 在Laravel中设计了多个外观类,每个外观类继承自统一的抽象外观类,在抽象外观类里提供了通过外观类访问其背后子系统的基础方法。 6 | 7 | 对于新的业务需求,不要修改原有外观类,而应该增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。 8 | 9 | 10 | 11 | 下面是一个简单的外观模式的例子,并没有引入抽象外观类,在介绍Laravel Facade的文章中我们会看到Laravel里提供了一个抽象外观类从而让我们能够方便的根据需要增加新子系统的外观类,并让外观类能够正确代理到其对应的子系统(或者叫服务)。 12 | 13 | 14 | 15 | ### 模式结构 16 | 17 | 外观模式包含如下角色: 18 | 19 | - Facade 外观角色 20 | - SubSystem 子系统角色 21 | 22 | ![UML类图](https://user-gold-cdn.xitu.io/2018/7/31/164eeca9a4b7bff7) 23 | 24 | ### 代码示例 25 | 26 | ``` 27 | operation(); 33 | } 34 | } 35 | 36 | class Facade 37 | { 38 | private $systemA; 39 | private $systemB; 40 | 41 | public function __construct() 42 | { 43 | $this->systemA = new SystemA; 44 | $this->systemB = new SystemB; 45 | } 46 | 47 | public function operation() 48 | { 49 | $this->systemA->operationA(); 50 | $this->systemB->operationB(); 51 | } 52 | } 53 | 54 | class SystemA 55 | { 56 | public function operationA() 57 | { 58 | // 59 | } 60 | } 61 | 62 | class SystemB 63 | { 64 | public function operationB() 65 | { 66 | // 67 | } 68 | } 69 | ``` 70 | 71 | ### 模式分析 72 | 73 | 根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。 -外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。 - 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。 -外观模式的目的在于降低系统的复杂程度。 -外观模式从很大程度上提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观角色即可调用相关功能。 74 | 75 | ### 缺点 76 | 77 | 外观模式的缺点 78 | 79 | - 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。 80 | - 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。 81 | 82 | ### 模式扩展 83 | 84 | - 一个系统有多个外观类 85 | 86 | 在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类。在很多情况下为了节约系统资源,一般将外观类设计为单例类。当然这并不意味着在整个系统里只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。 87 | 88 | - 不要试图通过外观类为子系统增加新行为 89 | 90 | 不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。 91 | 92 | - 抽象外观类的引入 93 | 94 | 外观模式最大的缺点在于违背了“开闭原则”,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。 95 | 96 | ### 总结 97 | 98 | - 在外观模式中,外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。 99 | - 外观模式包含两个角色:外观角色是在客户端直接调用的角色,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理;在软件系统中可以同时有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能。 100 | - 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。 101 | - 外观模式主要优点在于对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易,它实现了子系统与客户之间的松耦合关系,并降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程;其缺点在于不能很好地限制客户使用子系统类,而且在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。 102 | - 外观模式适用情况包括:要为一个复杂子系统提供一个简单接口;客户程序与多个子系统之间存在很大的依赖性;在层次化结构中,需要定义系统中每一层的入口,使得层与层之间不直接产生联系。 103 | 104 | 105 | 上一篇: [服务提供器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ServiceProvider.md) 106 | 107 | 下一篇: [Facades](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Facades.md) 108 | -------------------------------------------------------------------------------- /articles/Facades.md: -------------------------------------------------------------------------------- 1 | # Facades 2 | 3 | ### 什么是Facades 4 | Facades是我们在Laravel应用开发中使用频率很高的一个组件,叫组件不太合适,其实它们是一组静态类接口或者说代理,让开发者能简单的访问绑定到服务容器里的各种服务。Laravel文档中对Facades的解释如下: 5 | >Facades 为应用程序的 服务容器 中可用的类提供了一个「静态」接口。Laravel 本身附带许多的 facades,甚至你可能在不知情的状况下已经在使用他们!Laravel 「facades」作为在服务容器内基类的「静态代理」,拥有简洁、易表达的语法优点,同时维持着比传统静态方法更高的可测试性和灵活性。 6 | 7 | 我们经常用的Route就是一个Facade, 它是`\Illuminate\Support\Facades\Route`类的别名,这个Facade类代理的是注册到服务容器里的`router`服务,所以通过Route类我们就能够方便地使用router服务中提供的各种服务,而其中涉及到的服务解析完全是隐式地由Laravel完成的,这在一定程度上让应用程序代码变的简洁了不少。下面我们会大概看一下Facades从被注册进Laravel框架到被应用程序使用这中间的流程。Facades是和ServiceProvider紧密配合的所以如果你了解了中间的这些流程对开发自定义Laravel组件会很有帮助。 8 | 9 | ### 注册Facades 10 | 11 | 说到Facades注册又要回到再介绍其它核心组建时提到过很多次的Bootstrap阶段了,在让请求通过中间件和路由之前有一个启动应用程序的过程: 12 | 13 | //Class: \Illuminate\Foundation\Http\Kernel 14 | 15 | protected function sendRequestThroughRouter($request) 16 | { 17 | $this->app->instance('request', $request); 18 | 19 | Facade::clearResolvedInstance('request'); 20 | 21 | $this->bootstrap(); 22 | 23 | return (new Pipeline($this->app)) 24 | ->send($request) 25 | ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) 26 | ->then($this->dispatchToRouter()); 27 | } 28 | 29 | //引导启动Laravel应用程序 30 | public function bootstrap() 31 | { 32 | if (! $this->app->hasBeenBootstrapped()) { 33 | /**依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数 34 | $bootstrappers = [ 35 | 'Illuminate\Foundation\Bootstrap\DetectEnvironment', 36 | 'Illuminate\Foundation\Bootstrap\LoadConfiguration', 37 | 'Illuminate\Foundation\Bootstrap\ConfigureLogging', 38 | 'Illuminate\Foundation\Bootstrap\HandleExceptions', 39 | 'Illuminate\Foundation\Bootstrap\RegisterFacades', 40 | 'Illuminate\Foundation\Bootstrap\RegisterProviders', 41 | 'Illuminate\Foundation\Bootstrap\BootProviders', 42 | ];*/ 43 | $this->app->bootstrapWith($this->bootstrappers()); 44 | } 45 | } 46 | 47 | 在启动应用的过程中`Illuminate\Foundation\Bootstrap\RegisterFacades`这个阶段会注册应用程序里用到的Facades。 48 | 49 | ``` 50 | class RegisterFacades 51 | { 52 | /** 53 | * Bootstrap the given application. 54 | * 55 | * @param \Illuminate\Contracts\Foundation\Application $app 56 | * @return void 57 | */ 58 | public function bootstrap(Application $app) 59 | { 60 | Facade::clearResolvedInstances(); 61 | 62 | Facade::setFacadeApplication($app); 63 | 64 | AliasLoader::getInstance(array_merge( 65 | $app->make('config')->get('app.aliases', []), 66 | $app->make(PackageManifest::class)->aliases() 67 | ))->register(); 68 | } 69 | } 70 | ``` 71 | 在这里会通过`AliasLoader`类的实例将为所有Facades注册别名,Facades和别名的对应关系存放在`config/app.php`文件的`$aliases`数组中 72 | 73 | 'aliases' => [ 74 | 75 | 'App' => Illuminate\Support\Facades\App::class, 76 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, 77 | 'Auth' => Illuminate\Support\Facades\Auth::class, 78 | ...... 79 | 'Route' => Illuminate\Support\Facades\Route::class, 80 | ...... 81 | ] 82 | 83 | 看一下AliasLoader里是如何注册这些别名的 84 | 85 | // class: Illuminate\Foundation\AliasLoader 86 | public static function getInstance(array $aliases = []) 87 | { 88 | if (is_null(static::$instance)) { 89 | return static::$instance = new static($aliases); 90 | } 91 | 92 | $aliases = array_merge(static::$instance->getAliases(), $aliases); 93 | 94 | static::$instance->setAliases($aliases); 95 | 96 | return static::$instance; 97 | } 98 | 99 | public function register() 100 | { 101 | if (! $this->registered) { 102 | $this->prependToLoaderStack(); 103 | 104 | $this->registered = true; 105 | } 106 | } 107 | 108 | protected function prependToLoaderStack() 109 | { 110 | // 把AliasLoader::load()放入自动加载函数队列中,并置于队列头部 111 | spl_autoload_register([$this, 'load'], true, true); 112 | } 113 | 114 | 通过上面的代码段可以看到AliasLoader将load方法注册到了SPL __autoload函数队列的头部。看一下load方法的源码: 115 | 116 | public function load($alias) 117 | { 118 | if (isset($this->aliases[$alias])) { 119 | return class_alias($this->aliases[$alias], $alias); 120 | } 121 | } 122 | 123 | 在load方法里把`$aliases`配置里的Facade类创建了对应的别名,比如当我们使用别名类`Route`时PHP会通过AliasLoader的load方法为`Illuminate\Support\Facades\Route`类创建一个别名类`Route`,所以我们在程序里使用别`Route`其实使用的就是`Illuminate\Support\Facades\Route`类。 124 | 125 | ### 解析Facade代理的服务 126 | 127 | 把Facades注册到框架后我们在应用程序里就能使用其中的Facade了,比如注册路由时我们经常用`Route::get('/uri', 'Controller@action);`,那么`Route`是怎么代理到路由服务的呢,这就涉及到在Facade里服务的隐式解析了, 我们看一下Route类的源码: 128 | 129 | ``` 130 | namespace Illuminate\Support\Facades; 131 | class Route extends Facade 132 | { 133 | /** 134 | * Get the registered name of the component. 135 | * 136 | * @return string 137 | */ 138 | protected static function getFacadeAccessor() 139 | { 140 | return 'router'; 141 | } 142 | } 143 | ``` 144 | 只有简单的一个方法,并没有`get`, `post`, `delete`等那些路由方法, 父类里也没有,不过我们知道调用类不存在的静态方法时会触发PHP的`__callStatic`静态方法 145 | 146 | ``` 147 | namespace Illuminate\Support\Facades; 148 | 149 | abstract class Facade 150 | { 151 | public static function __callStatic($method, $args) 152 | { 153 | $instance = static::getFacadeRoot(); 154 | 155 | if (! $instance) { 156 | throw new RuntimeException('A facade root has not been set.'); 157 | } 158 | 159 | return $instance->$method(...$args); 160 | } 161 | 162 | //获取Facade根对象 163 | public static function getFacadeRoot() 164 | { 165 | return static::resolveFacadeInstance(static::getFacadeAccessor()); 166 | } 167 | 168 | /** 169 | * 从服务容器里解析出Facade对应的服务 170 | */ 171 | protected static function resolveFacadeInstance($name) 172 | { 173 | if (is_object($name)) { 174 | return $name; 175 | } 176 | 177 | if (isset(static::$resolvedInstance[$name])) { 178 | return static::$resolvedInstance[$name]; 179 | } 180 | 181 | return static::$resolvedInstance[$name] = static::$app[$name]; 182 | } 183 | } 184 | ``` 185 | 通过上面的分析我们可以看到Facade类的父类`Illuminate\Support\Facades\Facade`是Laravel提供的一个抽象外观类从而让我们能够方便的根据需要增加新的子系统的外观类,并让外观类能够正确代理到其对应的子系统(或者叫服务)。 186 | 187 | 通过在子类Route Facade里设置的accessor(字符串router), 从服务容器中解析出对应的服务,router服务是在应用程序初始化时的registerBaseServiceProviders阶段(具体可以看Application的构造方法)被`\Illuminate\Routing\RoutingServiceProvider`注册到服务容器里的: 188 | 189 | ``` 190 | class RoutingServiceProvider extends ServiceProvider 191 | { 192 | /** 193 | * Register the service provider. 194 | * 195 | * @return void 196 | */ 197 | public function register() 198 | { 199 | $this->registerRouter(); 200 | ...... 201 | } 202 | 203 | /** 204 | * Register the router instance. 205 | * 206 | * @return void 207 | */ 208 | protected function registerRouter() 209 | { 210 | $this->app->singleton('router', function ($app) { 211 | return new Router($app['events'], $app); 212 | }); 213 | } 214 | ...... 215 | } 216 | ``` 217 | router服务对应的类就是`\Illuminate\Routing\Router`, 所以Route Facade实际上代理的就是这个类,Route::get实际上调用的是`\Illuminate\Routing\Router`对象的get方法。 218 | 219 | /** 220 | * Register a new GET route with the router. 221 | * 222 | * @param string $uri 223 | * @param \Closure|array|string|null $action 224 | * @return \Illuminate\Routing\Route 225 | */ 226 | public function get($uri, $action = null) 227 | { 228 | return $this->addRoute(['GET', 'HEAD'], $uri, $action); 229 | } 230 | 231 | 232 | 补充两点: 233 | 234 | 1. 解析服务时用的`static::$app`是在最开始的`RegisterFacades`里设置的,它引用的是服务容器。 235 | 236 | 2. static::$app['router'];以数组访问的形式能够从服务容器解析出router服务是因为服务容器实现了SPL的ArrayAccess接口, 对这个没有概念的可以看下官方文档[ArrayAccess](http://php.net/manual/zh/class.arrayaccess.php) 237 | 238 | ### 总结 239 | 240 | 通过梳理Facade的注册和使用流程我们可以看到Facade和服务提供器(ServiceProvider)是紧密配合的,所以如果以后自己写Laravel自定义服务时除了通过组件的ServiceProvider将服务注册进服务容器,还可以在组件中提供一个Facade让应用程序能够方便的访问你写的自定义服务。 241 | 242 | 上一篇: [外观模式](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/FacadePattern.md) 243 | 244 | 下一篇: [路由](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Route.md) 245 | -------------------------------------------------------------------------------- /articles/Farewell.md: -------------------------------------------------------------------------------- 1 | ### 结束语 2 | 3 | 自从这个系列的第一篇文章开始已经过去一年了,在整个系列中一共写了20多篇文章来探讨了我认为的Larave框架最核心部分的设计思路、代码实现。通过更新文章自己在软件设计、文字表达方面都有所提高,也非常感谢关注文章更新的人们给予我的支持和鼓励。 4 | 5 | 在系列刚开篇的时候我说过目的是希望自己和读者通过学习Laravel核心的代码给自己在软件设计上带来提高,这些提高主要是指两方面: 6 | 7 | - 通过学习Laravel核心的代码来辅助理解软件设计行业中经常提及的核心概念,通过学习像`IocContainer`、面向对象的五大原则`SOLID` 是怎么应用到框架设计中去的来指导应该如何去做软件开发设计。这方面对你的收益应该是跳出Laravel框架和PHP语言层面的,当你需要切换到其他框架和语言时这些收益仍会反馈给你。 8 | - 熟练掌握Laravel的使用,虽然很多人说框架只是一个工具不应该花太多时间在工具的研究上,但是现实时开发者群体大部分人并没有在头部的那几家大公司,也不架构师,大部分人的工作还是在写业务代码那么既然你需要Laravel这个工具帮你完成每天的任务,那么为了尽可能高效率高质量的完成项目,确实是需要多了去看看框架的源码,了解一些框架常用的方法在positive和negative时的行为到底是什么(各种情况下的返回值和抛出的异常),知道怎么使用ORM才能让查询更高效等等,这些内容往往在框架的文档都是很少提及的,需要去看源码了解一下,如果你只会文档里提到的那些典型的用法显然不能算是熟练掌握的。 9 | 10 | 11 | 12 | Laravel整个框架设计到的内容有很多,其他的组件我也就不再一一去写文章梳理了, 相信你在认真看完这个系列的文章后,假如你在使用其他组件过程中遇到了诡异的问题,或者好奇框架是怎么帮你实现功能的?你完全有能力去梳理其他组件的源码实现来解决你的疑惑。 13 | 14 | 15 | 16 | 最后还是回到上面说的,框架只是工具如果想要在软件行业有所发展还是要把更多的精力投入到内功修炼上,所谓内功就是这些经过时间沉淀下来的基础知识,框架层出不穷,但是它们应用的基础知识却甚少改变。数据库、HTTP、算法和数据结构这些都是编程的内功,只有内功深厚了才能解决遇到的复杂问题。 17 | 18 | 推荐几个我认为挺好的修炼内功的专栏给大家: 19 | 20 | [程序员的数据基础课]() 21 | 22 | [MySQL实战45讲]() 23 | 24 | [数据结构与算法]() 25 | 26 | [算法面试通关40讲]() 27 | 28 | [许式伟的架构课](http://gk.link/a/102UP) 29 | 30 | 31 | 32 | 当然还有日新月异的前端知识也是需要会基础的用法的,最起码了解一下团队内部使用的前端框架的基础知识,这样对咱们做系统设计也会有帮助。 33 | -------------------------------------------------------------------------------- /articles/HttpKernel.md: -------------------------------------------------------------------------------- 1 | # Http Kernel 2 | 3 | Http Kernel是Laravel中用来串联框架的各个核心组件来网络请求的,简单的说只要是通过`public/index.php`来启动框架的都会用到Http Kernel,而另外的类似通过`artisan`命令、计划任务、队列启动框架进行处理的都会用到Console Kernel, 今天我们先梳理一下Http Kernel做的事情。 4 | 5 | 6 | 7 | ### 内核绑定 8 | 9 | 既然Http Kernel是Laravel中用来串联框架的各个部分处理网络请求的,我们来看一下内核是怎么加载到Laravel中应用实例中来的,在`public/index.php`中我们就会看见首先就会通过`bootstrap/app.php`这个脚手架文件来初始化应用程序: 10 | 11 | 12 | 13 | 下面是 `bootstrap/app.php` 的代码,包含两个主要部分**创建应用实例**和**绑定内核至 APP 服务容器** 14 | 15 | ``` 16 | singleton( 24 | Illuminate\Contracts\Http\Kernel::class, 25 | App\Http\Kernel::class 26 | ); 27 | 28 | $app->singleton( 29 | Illuminate\Contracts\Console\Kernel::class, 30 | App\Console\Kernel::class 31 | ); 32 | 33 | $app->singleton( 34 | Illuminate\Contracts\Debug\ExceptionHandler::class, 35 | App\Exceptions\Handler::class 36 | ); 37 | 38 | return $app; 39 | ``` 40 | 41 | HTTP 内核继承自 Illuminate\Foundation\Http\Kernel类,在 HTTP 内核中 内它定义了中间件相关数组, 中间件提供了一种方便的机制来过滤进入应用的 HTTP 请求和加工流出应用的HTTP响应。 42 | 43 | ``` 44 | [ 70 | \App\Http\Middleware\EncryptCookies::class, 71 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 72 | \Illuminate\Session\Middleware\StartSession::class, 73 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 74 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 75 | \App\Http\Middleware\VerifyCsrfToken::class, 76 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 77 | ], 78 | 'api' => [ 79 | 'throttle:60,1', 80 | 'bindings', 81 | ], 82 | ]; 83 | /** 84 | * The application's route middleware. 85 | * 86 | * These middleware may be assigned to groups or used individually. 87 | * 88 | * @var array 89 | */ 90 | protected $routeMiddleware = [ 91 | 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 92 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 93 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 94 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 95 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 96 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 97 | ]; 98 | } 99 | ``` 100 | 101 | 102 | 103 | 在其父类 「Illuminate\Foundation\Http\Kernel」 内部定义了属性名为 「bootstrappers」 的 引导程序 数组: 104 | 105 | ``` 106 | protected $bootstrappers = [ 107 | \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, 108 | \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, 109 | \Illuminate\Foundation\Bootstrap\HandleExceptions::class, 110 | \Illuminate\Foundation\Bootstrap\RegisterFacades::class, 111 | \Illuminate\Foundation\Bootstrap\RegisterProviders::class, 112 | \Illuminate\Foundation\Bootstrap\BootProviders::class, 113 | ]; 114 | ``` 115 | 116 | 117 | 118 | 引导程序组中 包括完成环境检测、配置加载、异常处理、Facades 注册、服务提供者注册、启动服务这六个引导程序。 119 | 120 | 有关中间件和引导程序相关内容的讲解可以浏览我们之前相关章节的内容。 121 | 122 | ### 应用解析内核 123 | 124 | 在将应用初始化阶段将Http内核绑定至应用的服务容器后,紧接着在`public/index.php`中我们可以看到使用了服务容器的`make`方法将Http内核实例解析了出来: 125 | 126 | ``` 127 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 128 | ``` 129 | 130 | 在实例化内核时,将在 HTTP 内核中定义的中间件注册到了 [路由器](https://github.com/laravel/framework/blob/5.6/src/Illuminate/Routing/Router.php),注册完后就可以在实际处理 HTTP 请求前调用路由上应用的中间件实现过滤请求的目的: 131 | 132 | ``` 133 | namespace Illuminate\Foundation\Http; 134 | ... 135 | class Kernel implements KernelContract 136 | { 137 | /** 138 | * Create a new HTTP kernel instance. 139 | * 140 | * @param \Illuminate\Contracts\Foundation\Application $app 141 | * @param \Illuminate\Routing\Router $router 142 | * @return void 143 | */ 144 | public function __construct(Application $app, Router $router) 145 | { 146 | $this->app = $app; 147 | $this->router = $router; 148 | 149 | $router->middlewarePriority = $this->middlewarePriority; 150 | 151 | foreach ($this->middlewareGroups as $key => $middleware) { 152 | $router->middlewareGroup($key, $middleware); 153 | } 154 | 155 | foreach ($this->routeMiddleware as $key => $middleware) { 156 | $router->aliasMiddleware($key, $middleware); 157 | } 158 | } 159 | } 160 | 161 | namespace Illuminate/Routing; 162 | class Router implements RegistrarContract, BindingRegistrar 163 | { 164 | /** 165 | * Register a group of middleware. 166 | * 167 | * @param string $name 168 | * @param array $middleware 169 | * @return $this 170 | */ 171 | public function middlewareGroup($name, array $middleware) 172 | { 173 | $this->middlewareGroups[$name] = $middleware; 174 | 175 | return $this; 176 | } 177 | 178 | /** 179 | * Register a short-hand name for a middleware. 180 | * 181 | * @param string $name 182 | * @param string $class 183 | * @return $this 184 | */ 185 | public function aliasMiddleware($name, $class) 186 | { 187 | $this->middleware[$name] = $class; 188 | 189 | return $this; 190 | } 191 | } 192 | ``` 193 | 194 | ### 处理HTTP请求 195 | 196 | 通过服务解析完成Http内核实例的创建后就可以用HTTP内核实例来处理HTTP请求了 197 | 198 | ``` 199 | //public/index.php 200 | $response = $kernel->handle( 201 | $request = Illuminate\Http\Request::capture() 202 | ); 203 | ``` 204 | 205 | 在处理请求之前会先通过`Illuminate\Http\Request`的 `capture()` 方法以进入应用的HTTP请求的信息为基础创建出一个 Laravel Request请求实例,在后续应用剩余的生命周期中`Request`请求实例就是对本次HTTP请求的抽象,关于[Laravel Request请求实例](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Request.md)的讲解可以参考以前的章节。 206 | 207 | 将HTTP请求抽象成`Laravel Request请求实例`后,请求实例会被传导进入到HTTP内核的`handle`方法内部,请求的处理就是由`handle`方法来完成的。 208 | 209 | ``` 210 | namespace Illuminate\Foundation\Http; 211 | 212 | class Kernel implements KernelContract 213 | { 214 | /** 215 | * Handle an incoming HTTP request. 216 | * 217 | * @param \Illuminate\Http\Request $request 218 | * @return \Illuminate\Http\Response 219 | */ 220 | public function handle($request) 221 | { 222 | try { 223 | $request->enableHttpMethodParameterOverride(); 224 | 225 | $response = $this->sendRequestThroughRouter($request); 226 | } catch (Exception $e) { 227 | $this->reportException($e); 228 | 229 | $response = $this->renderException($request, $e); 230 | } catch (Throwable $e) { 231 | $this->reportException($e = new FatalThrowableError($e)); 232 | 233 | $response = $this->renderException($request, $e); 234 | } 235 | 236 | $this->app['events']->dispatch( 237 | new Events\RequestHandled($request, $response) 238 | ); 239 | 240 | return $response; 241 | } 242 | } 243 | ``` 244 | 245 | `handle` 方法接收一个请求对象,并最终生成一个响应对象。其实`handle`方法我们已经很熟悉了在讲解很多模块的时候都是以它为出发点逐步深入到模块的内部去讲解模块内的逻辑的,其中`sendRequestThroughRouter`方法在服务提供者和中间件都提到过,它会加载在内核中定义的引导程序来引导启动应用然后会将使用`Pipeline`对象传输HTTP请求对象流经框架中定义的HTTP中间件们和路由中间件们来完成过滤请求最终将请求传递给处理程序(控制器方法或者路由中的闭包)由处理程序返回相应的响应。关于`handle`方法的注解我直接引用以前章节的讲解放在这里,具体更详细的分析具体是如何引导启动应用以及如何将传输流经各个中间件并到达处理程序的内容请查看[服务提供器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ServiceProvider.md)、[中间件](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Middleware.md)还有[路由](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Route.md)这三个章节。 246 | 247 | ``` 248 | protected function sendRequestThroughRouter($request) 249 | { 250 | $this->app->instance('request', $request); 251 | 252 | Facade::clearResolvedInstance('request'); 253 | 254 | $this->bootstrap(); 255 | 256 | return (new Pipeline($this->app)) 257 | ->send($request) 258 | ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) 259 | ->then($this->dispatchToRouter()); 260 | } 261 | 262 | /*引导启动Laravel应用程序 263 | 1. DetectEnvironment 检查环境 264 | 2. LoadConfiguration 加载应用配置 265 | 3. ConfigureLogging 配置日至 266 | 4. HandleException 注册异常处理的Handler 267 | 5. RegisterFacades 注册Facades 268 | 6. RegisterProviders 注册Providers 269 | 7. BootProviders 启动Providers 270 | */ 271 | public function bootstrap() 272 | { 273 | if (! $this->app->hasBeenBootstrapped()) { 274 | /**依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数 275 | $bootstrappers = [ 276 | 'Illuminate\Foundation\Bootstrap\DetectEnvironment', 277 | 'Illuminate\Foundation\Bootstrap\LoadConfiguration', 278 | 'Illuminate\Foundation\Bootstrap\ConfigureLogging', 279 | 'Illuminate\Foundation\Bootstrap\HandleExceptions', 280 | 'Illuminate\Foundation\Bootstrap\RegisterFacades', 281 | 'Illuminate\Foundation\Bootstrap\RegisterProviders', 282 | 'Illuminate\Foundation\Bootstrap\BootProviders', 283 | ];*/ 284 | $this->app->bootstrapWith($this->bootstrappers()); 285 | } 286 | } 287 | ``` 288 | 289 | ### 发送响应 290 | 291 | 经过上面的几个阶段后我们最终拿到了要返回的响应,接下来就是发送响应了。 292 | 293 | ``` 294 | //public/index.php 295 | $response = $kernel->handle( 296 | $request = Illuminate\Http\Request::capture() 297 | ); 298 | 299 | // 发送响应 300 | $response->send(); 301 | ``` 302 | 303 | 发送响应由 `Illuminate\Http\Response`的`send()`方法完成父类其定义在父类`Symfony\Component\HttpFoundation\Response`中。 304 | 305 | ``` 306 | public function send() 307 | { 308 | $this->sendHeaders();// 发送响应头部信息 309 | $this->sendContent();// 发送报文主题 310 | 311 | if (function_exists('fastcgi_finish_request')) { 312 | fastcgi_finish_request(); 313 | } elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) { 314 | static::closeOutputBuffers(0, true); 315 | } 316 | return $this; 317 | } 318 | ``` 319 | 320 | 关于Response对象的详细分析可以参看我们之前讲解Laravel Response对象的章节。 321 | 322 | ### 终止应用程序 323 | 324 | 响应发送后,HTTP内核会调用`terminable`中间件做一些后续的处理工作。比如,Laravel 内置的「session」中间件会在响应发送到浏览器之后将会话数据写入存储器中。 325 | 326 | ``` 327 | // public/index.php 328 | // 终止程序 329 | $kernel->terminate($request, $response); 330 | ``` 331 | 332 | 333 | 334 | ``` 335 | //Illuminate\Foundation\Http\Kernel 336 | public function terminate($request, $response) 337 | { 338 | $this->terminateMiddleware($request, $response); 339 | $this->app->terminate(); 340 | } 341 | 342 | // 终止中间件 343 | protected function terminateMiddleware($request, $response) 344 | { 345 | $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge( 346 | $this->gatherRouteMiddleware($request), 347 | $this->middleware 348 | ); 349 | foreach ($middlewares as $middleware) { 350 | if (! is_string($middleware)) { 351 | continue; 352 | } 353 | list($name, $parameters) = $this->parseMiddleware($middleware); 354 | $instance = $this->app->make($name); 355 | if (method_exists($instance, 'terminate')) { 356 | $instance->terminate($request, $response); 357 | } 358 | } 359 | } 360 | ``` 361 | 362 | Http内核的`terminate`方法会调用`teminable`中间件的`terminate`方法,调用完成后从HTTP请求进来到返回响应整个应用程序的生命周期就结束了。 363 | 364 | ### 总结 365 | 366 | 本节介绍的HTTP内核起到的主要是串联作用,其中设计到的初始化应用、引导应用、将HTTP请求抽象成Request对象、传递Request对象通过中间件到达处理程序生成响应以及响应发送给客户端。这些东西在之前的章节里都有讲过,并没有什么新的东西,希望通过这篇文章能让大家把之前文章里讲到的每个点串成一条线,这样对Laravel整体是怎么工作的会有更清晰的概念。 367 | 368 | 上一篇: [加载和读取ENV配置](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ENV.md) 369 | 370 | 下一篇: [Console内核](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ConsoleKernel.md) 371 | 372 | -------------------------------------------------------------------------------- /articles/IocContainer.md: -------------------------------------------------------------------------------- 1 | # 服务容器(IocContainer) 2 | 3 | Laravel的核心是IocContainer, 文档中称其为“服务容器”,服务容器是一个用于管理类依赖和执行依赖注入的强大工具,Laravel中的功能模块比如 Route、Eloquent ORM、Request、Response等等等等,实际上都是与核心无关的类模块提供的,这些类从注册到实例化,最终被我们所使用,其实都是 laravel 的服务容器负责的。 4 | 5 | 如果对服务容器是什么没有清晰概念的话推荐一篇博文来了解一下服务容器的来龙去脉:[laravel神奇的服务容器](https://www.insp.top/learn-laravel-container) 6 | 7 | 服务容器中有两个概念[控制反转(IOC)和依赖注入(DI)](http://blog.csdn.net/doris_crazy/article/details/18353197): 8 | 9 | >依赖注入和控制反转是对同一件事情的不同描述,它们描述的角度不同。依赖注入是从应用程序的角度在描述,应用程序依赖容器创建并注入它所需要的外部资源。而控制反转是从容器的角度在描述,容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。 10 | 11 | 在Laravel中框架把自带的各种服务绑定到服务容器,我们也可以绑定自定义服务到容器。当应用程序需要使用某一个服务时,服务容器会讲服务解析出来同时自动解决服务之间的依赖然后交给应用程序使用。 12 | 13 | 本篇就来探讨一下Laravel中的服务绑定和解析是如何实现的 14 | 15 | ## 服务绑定 16 | 常用的绑定服务到容器的方法有instance, bind, singleton, alias。下面我们分别来看一下。 17 | 18 | ### instance 19 | 将一个已存在的对象绑定到服务容器里,随后通过名称解析该服务时,容器将总返回这个绑定的实例。 20 | 21 | $api = new HelpSpot\API(new HttpClient); 22 | $this->app->instance('HelpSpot\Api', $api); 23 | 24 | 会把对象注册到服务容器的$instances属性里 25 | 26 | [ 27 | 'HelpSpot\Api' => $api//$api是API类的对象,这里简写了 28 | ] 29 | 30 | ### bind 31 | 绑定服务到服务容器 32 | 33 | 有三种绑定方式: 34 | 35 | 1.绑定自身 36 | $this->app->bind('HelpSpot\API', null); 37 | 38 | 2.绑定闭包 39 | $this->app->bind('HelpSpot\API', function () { 40 | return new HelpSpot\API(); 41 | });//闭包直接提供类实现方式 42 | $this->app->bind('HelpSpot\API', function ($app) { 43 | return new HelpSpot\API($app->make('HttpClient')); 44 | });//闭包返回需要依赖注入的类 45 | 3. 绑定接口和实现 46 | $this->app->bind('Illuminate\Tests\Container\IContainerContractStub', 'Illuminate\Tests\Container\ContainerImplementationStub'); 47 | 48 | 49 | 针对第一种情况,其实在bind方法内部会在绑定服务之前通过`getClosure()`为服务生成闭包,我们来看一下bind方法源码。 50 | 51 | public function bind($abstract, $concrete = null, $shared = false) 52 | { 53 | $abstract = $this->normalize($abstract); 54 | 55 | $concrete = $this->normalize($concrete); 56 | //如果$abstract为数组类似['Illuminate/ServiceName' => 'service_alias'] 57 | //抽取别名"service_alias"并且注册到$aliases[]中 58 | //注意:数组绑定别名的方式在5.4中被移除,别名绑定请使用下面的alias方法 59 | if (is_array($abstract)) { 60 | list($abstract, $alias) = $this->extractAlias($abstract); 61 | 62 | $this->alias($abstract, $alias); 63 | } 64 | 65 | $this->dropStaleInstances($abstract); 66 | 67 | if (is_null($concrete)) { 68 | $concrete = $abstract; 69 | } 70 | //如果只提供$abstract,则在这里为其生成concrete闭包 71 | if (! $concrete instanceof Closure) { 72 | $concrete = $this->getClosure($abstract, $concrete); 73 | } 74 | 75 | $this->bindings[$abstract] = compact('concrete', 'shared'); 76 | 77 | if ($this->resolved($abstract)) { 78 | $this->rebound($abstract); 79 | } 80 | } 81 | 82 | 83 | protected function getClosure($abstract, $concrete) 84 | { 85 | // $c 就是$container,即服务容器,会在回调时传递给这个变量 86 | return function ($c, $parameters = []) use ($abstract, $concrete) { 87 | $method = ($abstract == $concrete) ? 'build' : 'make'; 88 | 89 | return $c->$method($concrete, $parameters); 90 | }; 91 | } 92 | 93 | 94 | 95 | 96 | bind把服务注册到服务容器的$bindings属性里类似这样: 97 | 98 | $bindings = [ 99 | 'HelpSpot\API' => [//闭包绑定 100 | 'concrete' => function ($app, $paramters = []) { 101 | return $app->build('HelpSpot\API'); 102 | }, 103 | 'shared' => false//如果是singleton绑定,这个值为true 104 | ] 105 | 'Illuminate\Tests\Container\IContainerContractStub' => [//接口实现绑定 106 | 'concrete' => 'Illuminate\Tests\Container\ContainerImplementationStub', 107 | 'shared' => false 108 | ] 109 | ] 110 | 111 | 112 | ### singleton 113 | 114 | public function singleton($abstract, $concrete = null) 115 | { 116 | $this->bind($abstract, $concrete, true); 117 | } 118 | 119 | singleton 方法是bind方法的变种,绑定一个只需要解析一次的类或接口到容器,然后接下来对于容器的调用该服务将会返回同一个实例 120 | 121 | ### alias 122 | 把服务和服务别名注册到容器: 123 | 124 | public function alias($abstract, $alias) 125 | { 126 | $this->aliases[$alias] = $this->normalize($abstract); 127 | } 128 | alias 方法在上面讲bind方法里有用到过,它会把把服务别名和服务类的对应关系注册到服务容器的$aliases属性里。 129 | 例如: 130 | $this->app->alias('\Illuminate\ServiceName', 'service_alias'); 131 | 绑定完服务后在使用时就可以通过 132 | $this->app->make('service_alias'); 133 | 将服务对象解析出来,这样make的时候就不用写那些比较长的类名称了,对make方法的使用体验上有很大提升。 134 | 135 | 136 | ## 服务解析 137 | make: 从服务容器中解析出服务对象,该方法接收你想要解析的类名或接口名作为参数 138 | 139 | /** 140 | * Resolve the given type from the container. 141 | * 142 | * @param string $abstract 143 | * @param array $parameters 144 | * @return mixed 145 | */ 146 | public function make($abstract, array $parameters = []) 147 | { 148 | // getAlias方法会假定$abstract是绑定的别名,从$aliases找到映射的真实类型名 149 | // 如果没有映射则$abstract即为真实类型名,将$abstract原样返回 150 | $abstract = $this->getAlias($this->normalize($abstract)); 151 | 152 | // 如果服务是通过instance()方式绑定的,就直接解析返回绑定的service 153 | if (isset($this->instances[$abstract])) { 154 | return $this->instances[$abstract]; 155 | } 156 | 157 | // 获取$abstract接口对应的$concrete(接口的实现) 158 | $concrete = $this->getConcrete($abstract); 159 | 160 | if ($this->isBuildable($concrete, $abstract)) { 161 | $object = $this->build($concrete, $parameters); 162 | } else { 163 | // 如果时接口实现这种绑定方式,通过接口拿到实现后需要再make一次才能 164 | // 满足isBuildable的条件 ($abstract === $concrete) 165 | $object = $this->make($concrete, $parameters); 166 | } 167 | 168 | foreach ($this->getExtenders($abstract) as $extender) { 169 | $object = $extender($object, $this); 170 | } 171 | 172 | // 如果服务是以singleton方式注册进来的则,把构建好的服务对象放到$instances里, 173 | // 避免下次使用时重新构建 174 | if ($this->isShared($abstract)) { 175 | $this->instances[$abstract] = $object; 176 | } 177 | 178 | $this->fireResolvingCallbacks($abstract, $object); 179 | 180 | $this->resolved[$abstract] = true; 181 | 182 | return $object; 183 | } 184 | 185 | protected function getConcrete($abstract) 186 | { 187 | if (! is_null($concrete = $this->getContextualConcrete($abstract))) { 188 | return $concrete; 189 | } 190 | 191 | // 如果是$abstract之前没有注册类实现到服务容器里,则服务容器会认为$abstract本身就是接口的类实现 192 | if (! isset($this->bindings[$abstract])) { 193 | return $abstract; 194 | } 195 | 196 | return $this->bindings[$abstract]['concrete']; 197 | } 198 | 199 | protected function isBuildable($concrete, $abstract) 200 | { 201 | return $concrete === $abstract || $concrete instanceof Closure; 202 | } 203 | 204 | 205 | 206 | 通过对make方法的梳理我们发现,build方法的职能是构建解析出来的服务的对象的,下面看一下构建对象的具体流程。(构建过程中用到了[PHP类的反射][1]来实现服务的依赖注入) 207 | 208 | 209 | public function build($concrete, array $parameters = []) 210 | { 211 | // 如果是闭包直接执行闭包并返回(对应闭包绑定) 212 | if ($concrete instanceof Closure) { 213 | return $concrete($this, $parameters); 214 | } 215 | 216 | // 使用反射ReflectionClass来对实现类进行反向工程 217 | $reflector = new ReflectionClass($concrete); 218 | 219 | // 如果不能实例化,这应该是接口或抽象类,再或者就是构造函数是private的 220 | if (! $reflector->isInstantiable()) { 221 | if (! empty($this->buildStack)) { 222 | $previous = implode(', ', $this->buildStack); 223 | 224 | $message = "Target [$concrete] is not instantiable while building [$previous]."; 225 | } else { 226 | $message = "Target [$concrete] is not instantiable."; 227 | } 228 | 229 | throw new BindingResolutionException($message); 230 | } 231 | 232 | $this->buildStack[] = $concrete; 233 | 234 | // 获取构造函数 235 | $constructor = $reflector->getConstructor(); 236 | 237 | // 如果构造函数是空,说明没有任何依赖,直接new返回 238 | if (is_null($constructor)) { 239 | array_pop($this->buildStack); 240 | 241 | return new $concrete; 242 | } 243 | 244 | // 获取构造函数的依赖(形参),返回一组ReflectionParameter对象组成的数组表示每一个参数 245 | $dependencies = $constructor->getParameters(); 246 | 247 | $parameters = $this->keyParametersByArgument( 248 | $dependencies, $parameters 249 | ); 250 | 251 | // 构建构造函数需要的依赖 252 | $instances = $this->getDependencies( 253 | $dependencies, $parameters 254 | ); 255 | 256 | array_pop($this->buildStack); 257 | 258 | return $reflector->newInstanceArgs($instances); 259 | } 260 | 261 | // 获取依赖 262 | protected function getDependencies(array $parameters, array $primitives = []) 263 | { 264 | $dependencies = []; 265 | 266 | foreach ($parameters as $parameter) { 267 | $dependency = $parameter->getClass(); 268 | 269 | // 某一依赖值在$primitives中(如:app()->make(ApiService::class, ['clientId' => 'id'])调用时$primitives里会包含ApiService类构造方法中参数$clientId的参数值)已提供 270 | // $parameter->name返回参数名 271 | if (array_key_exists($parameter->name, $primitives)) { 272 | $dependencies[] = $primitives[$parameter->name]; 273 | } 274 | elseif (is_null($dependency)) { 275 | // 参数的ReflectionClass为null,说明是基本类型,如'int','string' 276 | $dependencies[] = $this->resolveNonClass($parameter); 277 | } else { 278 | // 参数是一个类的对象, 则用resolveClass去把对象解析出来 279 | $dependencies[] = $this->resolveClass($parameter); 280 | } 281 | } 282 | 283 | return $dependencies; 284 | } 285 | 286 | // 解析出依赖类的对象 287 | protected function resolveClass(ReflectionParameter $parameter) 288 | { 289 | try { 290 | // $parameter->getClass()->name返回的是类名(参数在typehint里声明的类型) 291 | // 然后递归继续make(在make时发现依赖类还有其他依赖,那么会继续make依赖的依赖 292 | // 直到所有依赖都被解决了build才结束) 293 | return $this->make($parameter->getClass()->name); 294 | } catch (BindingResolutionException $e) { 295 | if ($parameter->isOptional()) { 296 | return $parameter->getDefaultValue(); 297 | } 298 | 299 | throw $e; 300 | } 301 | } 302 | 303 | 304 | 服务容器就是laravel的核心, 它通过依赖注入很好的替我们解决对象之间的相互依赖关系,而又通过控制反转让外部来来定义具体的行为(Route, Eloquent这些都是外部模块,它们自己定义了行为规范,这些类从注册到实例化给你使用才是服务容器负责的)。 305 | 306 |   一个类要被容器所能够提取,必须要先注册至这个容器。既然 laravel 称这个容器叫做服务容器,那么我们需要某个服务,就得先注册、绑定这个服务到容器,那么提供服务并绑定服务至容器的东西就是服务提供器(ServiceProvider)。服务提供者主要分为两个部分:register(注册) 和 boot(引导、初始化)这就引出了我们后面要学习的内容。 307 | 308 | 上一篇: [类的反射和依赖注入][1] 309 | 310 | 下一篇: [服务提供器][3] 311 | 312 | 313 | [1]: https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/reflection.md 314 | [3]: https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/ServiceProvider.md 315 | -------------------------------------------------------------------------------- /articles/Middleware.md: -------------------------------------------------------------------------------- 1 | # 中间件 2 | 3 | 中间件(Middleware)在Laravel中起着过滤进入应用的HTTP请求对象(Request)和完善离开应用的HTTP响应对象(Reponse)的作用, 而且可以通过应用多个中间件来层层过滤请求、逐步完善响应。这样就做到了程序的解耦,如果没有中间件那么我们必须在控制器中来完成这些步骤,这无疑会造成控制器的臃肿。 4 | 5 | 举一个简单的例子,在一个电商平台上用户既可以是一个普通用户在平台上购物也可以在开店后是一个卖家用户,这两种用户的用户体系往往都是一套,那么在只有卖家用户才能访问的控制器里我们只需要应用两个中间件来完成卖家用户的身份认证: 6 | 7 | ``` 8 | class MerchantController extends Controller 9 | { 10 | public function __construct() 11 | { 12 | $this->middleware('auth'); 13 | $this->middleware('mechatnt_auth'); 14 | } 15 | } 16 | ``` 17 | 在auth中间件里做了通用的用户认证,成功后HTTP Request会走到merchant_auth中间件里进行商家用户信息的认证,两个中间件都通过后HTTP Request就能进入到要去的控制器方法中了。利用中间件,我们就能把这些认证代码抽离到对应的中间件中了,而且可以根据需求自由组合多个中间件来对HTTP Request进行过滤。 18 | 19 | 再比如Laravel自动给所有路由应用的`VerifyCsrfToken`中间件,在HTTP Requst进入应用走过`VerifyCsrfToken`中间件时会验证Token防止跨站请求伪造,在Http Response 离开应用前会给响应添加合适的Cookie。(laravel5.5开始CSRF中间件只自动应用到web路由上) 20 | 21 | 上面例子中过滤请求的叫前置中间件,完善响应的叫做后置中间件。用一张图可以标示整个流程: 22 | ![中间件图例](https://sfault-image.b0.upaiyun.com/323/572/3235720194-5a7874d77e8a2_articlex) 23 | 24 | 25 | 上面概述了下中间件在laravel中的角色,以及什么类型的代码应该从控制器挪到中间件里,至于如何定义和使用自己的laravel 中间件请参考[官方文档](https://d.laravel-china.org/docs/5.5/middleware)。 26 | 27 | 下面我们主要来看一下Laravel中是怎么实现中间件的,中间件的设计应用了一种叫做装饰器的设计模式,如果你还不知道什么是装饰器模式可以查阅设计模式相关的书,也可以翻看我之前的文章[装饰模式(DecoratorPattern)](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/DecoratorPattern.md)。 28 | 29 | Laravel实例化Application后,会从服务容器里解析出Http Kernel对象,通过类的名字也能看出来Http Kernel就是Laravel里负责HTTP请求和响应的核心。 30 | 31 | ``` 32 | /** 33 | * @var \App\Http\Kernel $kernel 34 | */ 35 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 36 | 37 | $response = $kernel->handle( 38 | $request = Illuminate\Http\Request::capture() 39 | ); 40 | 41 | $response->send(); 42 | 43 | $kernel->terminate($request, $response); 44 | ``` 45 | 在`index.php`里可以看到,从服务容器里解析出Http Kernel,因为在`bootstrap/app.php`里绑定了`Illuminate\Contracts\Http\Kernel`接口的实现类`App\Http\Kernel`所以$kernel实际上是`App\Http\Kernel`类的对象。 46 | 解析出Http Kernel后Laravel将进入应用的请求对象传递给Http Kernel的handle方法,在handle方法负责处理流入应用的请求对象并返回响应对象。 47 | 48 | /** 49 | * Handle an incoming HTTP request. 50 | * 51 | * @param \Illuminate\Http\Request $request 52 | * @return \Illuminate\Http\Response 53 | */ 54 | public function handle($request) 55 | { 56 | try { 57 | $request->enableHttpMethodParameterOverride(); 58 | 59 | $response = $this->sendRequestThroughRouter($request); 60 | } catch (Exception $e) { 61 | $this->reportException($e); 62 | 63 | $response = $this->renderException($request, $e); 64 | } catch (Throwable $e) { 65 | $this->reportException($e = new FatalThrowableError($e)); 66 | 67 | $response = $this->renderException($request, $e); 68 | } 69 | 70 | $this->app['events']->dispatch( 71 | new Events\RequestHandled($request, $response) 72 | ); 73 | 74 | return $response; 75 | } 76 | 77 | 中间件过滤应用的过程就发生在`$this->sendRequestThroughRouter($request)`里: 78 | 79 | /** 80 | * Send the given request through the middleware / router. 81 | * 82 | * @param \Illuminate\Http\Request $request 83 | * @return \Illuminate\Http\Response 84 | */ 85 | protected function sendRequestThroughRouter($request) 86 | { 87 | $this->app->instance('request', $request); 88 | 89 | Facade::clearResolvedInstance('request'); 90 | 91 | $this->bootstrap(); 92 | 93 | return (new Pipeline($this->app)) 94 | ->send($request) 95 | ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) 96 | ->then($this->dispatchToRouter()); 97 | } 98 | 99 | 这个方法的前半部分是对Application进行了初始化,在上一面讲解服务提供器的文章里有对这一部分的详细讲解。Laravel通过Pipeline(管道)对象来传输请求对象,在Pipeline中请求对象依次通过Http Kernel里定义的中间件的前置操作到达控制器的某个action或者直接闭包处理得到响应对象。 100 | 101 | 看下Pipeline里这几个方法: 102 | 103 | public function send($passable) 104 | { 105 | $this->passable = $passable; 106 | 107 | return $this; 108 | } 109 | 110 | public function through($pipes) 111 | { 112 | $this->pipes = is_array($pipes) ? $pipes : func_get_args(); 113 | 114 | return $this; 115 | } 116 | 117 | public function then(Closure $destination) 118 | { 119 | $firstSlice = $this->getInitialSlice($destination); 120 | 121 | //pipes 就是要通过的中间件 122 | $pipes = array_reverse($this->pipes); 123 | 124 | //$this->passable就是Request对象 125 | return call_user_func( 126 | array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable 127 | ); 128 | } 129 | 130 | 131 | protected function getInitialSlice(Closure $destination) 132 | { 133 | return function ($passable) use ($destination) { 134 | return call_user_func($destination, $passable); 135 | }; 136 | } 137 | 138 | //Http Kernel的dispatchToRouter是Piple管道的终点或者叫目的地 139 | protected function dispatchToRouter() 140 | { 141 | return function ($request) { 142 | $this->app->instance('request', $request); 143 | 144 | return $this->router->dispatch($request); 145 | }; 146 | } 147 | 148 | 上面的函数看起来比较晕,我们先来看下array_reduce里对它的callback函数参数的解释: 149 | 150 | >mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) 151 | 152 | >array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。 153 | 154 | >callback ( mixed $carry , mixed $item ) 155 | carry 156 | 携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial。item 携带了本次迭代的值。 157 | 158 | getInitialSlice方法,他的返回值是作为传递给callbakc函数的$carry参数的初始值,这个值现在是一个闭包,我把getInitialSlice和Http Kernel的dispatchToRouter这两个方法合并一下,现在$firstSlice的值为: 159 | 160 | ``` 161 | $destination = function ($request) { 162 | $this->app->instance('request', $request); 163 | return $this->router->dispatch($request); 164 | }; 165 | 166 | $firstSlice = function ($passable) use ($destination) { 167 | return call_user_func($destination, $passable); 168 | }; 169 | ``` 170 | 171 | 接下来我们看看array_reduce的callback: 172 | 173 | //Pipeline 174 | protected function getSlice() 175 | { 176 | return function ($stack, $pipe) { 177 | return function ($passable) use ($stack, $pipe) { 178 | try { 179 | $slice = parent::getSlice(); 180 | 181 | return call_user_func($slice($stack, $pipe), $passable); 182 | } catch (Exception $e) { 183 | return $this->handleException($passable, $e); 184 | } catch (Throwable $e) { 185 | return $this->handleException($passable, new FatalThrowableError($e)); 186 | } 187 | }; 188 | }; 189 | } 190 | 191 | //Pipleline的父类BasePipeline的getSlice方法 192 | protected function getSlice() 193 | { 194 | return function ($stack, $pipe) { 195 | return function ($passable) use ($stack, $pipe) { 196 | if ($pipe instanceof Closure) { 197 | return call_user_func($pipe, $passable, $stack); 198 | } elseif (! is_object($pipe)) { 199 | //解析中间件名称和参数 ('throttle:60,1') 200 | list($name, $parameters) = $this->parsePipeString($pipe); 201 | $pipe = $this->container->make($name); 202 | $parameters = array_merge([$passable, $stack], $parameters); 203 | } else{ 204 | $parameters = [$passable, $stack]; 205 | } 206 | //$this->method = handle 207 | return call_user_func_array([$pipe, $this->method], $parameters); 208 | }; 209 | }; 210 | } 211 | 212 | **注:在Laravel5.5版本里 getSlice这个方法的名称换成了carry, 两者在逻辑上没有区别,所以依然可以参照着5.5版本里中间件的代码来看本文。** 213 | 214 | getSlice会返回一个闭包函数, $stack在第一次调用getSlice时它的值是$firstSlice, 之后的调用中就它的值就是这里返回的值个闭包了: 215 | 216 | $stack = function ($passable) use ($stack, $pipe) { 217 | try { 218 | $slice = parent::getSlice(); 219 | 220 | return call_user_func($slice($stack, $pipe), $passable); 221 | } catch (Exception $e) { 222 | return $this->handleException($passable, $e); 223 | } catch (Throwable $e) { 224 | return $this->handleException($passable, new FatalThrowableError($e)); 225 | } 226 | }; 227 | 228 | getSlice返回的闭包里又会去调用父类的getSlice方法,他返回的也是一个闭包,在闭包会里解析出中间件对象、中间件参数(无则为空数组), 然后把$passable(请求对象), $stack和中间件参数作为中间件handle方法的参数进行调用。 229 | 230 | 上面封装的有点复杂,我们简化一下,其实getSlice的返回值就是: 231 | 232 | $stack = function ($passable) use ($stack, $pipe) { 233 | //解析中间件和中间件参数,中间件参数用$parameter代表,无参数时为空数组 234 | $parameters = array_merge([$passable, $stack], $parameters) 235 | return $pipe->handle($parameters) 236 | }; 237 | 238 | 239 | array_reduce每次调用callback返回的闭包都会作为参数$stack传递给下一次对callback的调用,array_reduce执行完成后就会返回一个嵌套了多层闭包的闭包,每层闭包用到的外部变量$stack都是上一次之前执行reduce返回的闭包,相当于把中间件通过闭包层层包裹包成了一个洋葱。 240 | 241 | 在then方法里,等到array_reduce执行完返回最终结果后就会对这个洋葱闭包进行调用: 242 | 243 | return call_user_func( array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable); 244 | 245 | 246 | 这样就能依次执行中间件handle方法,在handle方法里又会去再次调用之前说的reduce包装的洋葱闭包剩余的部分,这样一层层的把洋葱剥开直到最后。通过这种方式让请求对象依次流过了要通过的中间件,达到目的地Http Kernel 的`dispatchToRouter`方法。 247 | 248 | 通过剥洋葱的过程我们就能知道为什么在array_reduce之前要先对middleware数组进行反转, 因为包装是一个反向的过程, 数组$pipes中的第一个中间件会作为第一次reduce执行的结果被包装在洋葱闭包的最内层,所以只有反转后才能保证初始定义的中间件数组中第一个中间件的handle方法会被最先调用。 249 | 250 | 上面说了Pipeline传送请求对象的目的地是Http Kernel 的`dispatchToRouter`方法,其实到远没有到达最终的目的地,现在请求对象了只是刚通过了`\App\Http\Kernel`类里`$middleware`属性里罗列出的几个中间件: 251 | 252 | protected $middleware = [ 253 | \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, 254 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 255 | \App\Http\Middleware\TrimStrings::class, 256 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 257 | \App\Http\Middleware\TrustProxies::class, 258 | ]; 259 | 260 | 当请求对象进入Http Kernel的`dispatchToRouter`方法后,请求对象在被Router dispatch派发给路由时会进行收集路由上应用的中间件和控制器里应用的中间件。 261 | 262 | ``` 263 | namespace Illuminate\Foundation\Http; 264 | class Kernel implements KernelContract 265 | { 266 | protected function dispatchToRouter() 267 | { 268 | return function ($request) { 269 | $this->app->instance('request', $request); 270 | 271 | return $this->router->dispatch($request); 272 | }; 273 | } 274 | } 275 | 276 | 277 | namespace Illuminate\Routing; 278 | class Router implements RegistrarContract, BindingRegistrar 279 | { 280 | public function dispatch(Request $request) 281 | { 282 | $this->currentRequest = $request; 283 | 284 | return $this->dispatchToRoute($request); 285 | } 286 | 287 | public function dispatchToRoute(Request $request) 288 | { 289 | return $this->runRoute($request, $this->findRoute($request)); 290 | } 291 | 292 | protected function runRoute(Request $request, Route $route) 293 | { 294 | $request->setRouteResolver(function () use ($route) { 295 | return $route; 296 | }); 297 | 298 | $this->events->dispatch(new Events\RouteMatched($route, $request)); 299 | 300 | return $this->prepareResponse($request, 301 | $this->runRouteWithinStack($route, $request) 302 | ); 303 | } 304 | 305 | protected function runRouteWithinStack(Route $route, Request $request) 306 | { 307 | $shouldSkipMiddleware = $this->container->bound('middleware.disable') && 308 | $this->container->make('middleware.disable') === true; 309 | //收集路由和控制器里应用的中间件 310 | $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); 311 | 312 | return (new Pipeline($this->container)) 313 | ->send($request) 314 | ->through($middleware) 315 | ->then(function ($request) use ($route) { 316 | return $this->prepareResponse( 317 | $request, $route->run() 318 | ); 319 | }); 320 | 321 | } 322 | } 323 | 324 | namespace Illuminate\Routing; 325 | class Route 326 | { 327 | public function run() 328 | { 329 | $this->container = $this->container ?: new Container; 330 | try { 331 | if ($this->isControllerAction()) { 332 | return $this->runController(); 333 | } 334 | return $this->runCallable(); 335 | } catch (HttpResponseException $e) { 336 | return $e->getResponse(); 337 | } 338 | } 339 | 340 | } 341 | ``` 342 | 343 | 344 | 收集完路由和控制器里应用的中间件后,依然是利用Pipeline对象来传送请求对象通过收集上来的这些中间件然后到达最终的目的地,在那里会执行目的路由的run方法,run方法里面会判断路由对应的是一个控制器方法还是闭包然后进行相应地调用,最后把执行结果包装成Response对象。Response对象会依次通过上面应用的所有中间件的后置操作,最终离开应用被发送给客户端。 345 | 346 | 限于篇幅和为了文章的可读性,收集路由和控制器中间件然后执行路由对应的处理方法的过程我就不在这里详述了,感兴趣的同学可以自己去看Router的源码,本文的目的还是主要为了梳理laravel是如何设计中间件的以及如何执行它们的,希望能对感兴趣的朋友有帮助。 347 | 348 | 上一篇: [装饰模式](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/DecoratorPattern.md) 349 | 350 | 下一篇: [控制器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Controller.md) 351 | -------------------------------------------------------------------------------- /articles/Observer.md: -------------------------------------------------------------------------------- 1 | # 观察者模式 2 | 3 | Laravel的Event事件系统提供了一个简单的观察者模式实现,能够订阅和监听应用中发生的各种事件,在PHP的标准库(SPL)里甚至提供了三个接口`SplSubject`, `SplObserver`, `SplObjectStorage`来让开发者更容易地实现观察者模式,不过我还是想脱离SPL提供的接口和特定编程语言来说一下如何通过面向对象程序设计来实现观察者模式,示例是PHP代码不过用其他面向对象语言实现起来也是一样的。 4 | 5 | ### 模式定义 6 | 7 | 观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。 8 | 9 | 观察者模式的核心在于Subject和Observer接口,Subject(主题目标)包含一个给定的状态,观察者“订阅”这个主题,将主题的当前状态通知观察者,每次给定状态改变时所有观察者都会得到通知。 10 | 11 | 发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。 12 | 13 | 14 | 15 | ### 模式结构说明 16 | 17 | ![观察者模式UML](https://user-gold-cdn.xitu.io/2018/6/6/163d27e2a360d07e?imageslim) 18 | 19 | - Subject 目标抽象类 20 | - ConcreteSubject 具体目标 21 | - Observer 观察者抽象类 22 | - ConcreteObserver 具体观察者 23 | 24 | 25 | 26 | ### 应用举例 27 | 28 | 比如在设置用户(主题)的状态后要分别发送当前的状态描述信息给用户的邮箱和手机,我们可以使用两个观察者订阅用户的状态,一旦设置状态后主题就会通知的订阅了状态改变的观察者,在两个观察者里面我们可以分别来实现发送邮件信息和短信信息的功能。 29 | 30 | 1. 抽象目标类 31 | 32 | ``` 33 | abstract class Subject 34 | { 35 | protected $stateNow; 36 | protected $observers = []; 37 | 38 | public function attach(Observer $observer) 39 | { 40 | array_push($this->observers, $observer); 41 | } 42 | 43 | public function detach(Observer $ob) 44 | { 45 | $pos = 0; 46 | foreach ($this->observers as $viewer) { 47 | if ($viewer == $ob) { 48 | array_splice($this->observers, $pos, 1); 49 | } 50 | $pos++; 51 | } 52 | } 53 | 54 | public function notify() 55 | { 56 | foreach ($this->observers as $viewer) { 57 | $viewer->update($this); 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | 在抽象类中`attach` `detach` 和`notify`都是具体方法,这些是继承才能使用的方法,将由`Subject`的子类使用。 64 | 65 | 2. 具体目标类 66 | 67 | ``` 68 | class ConcreteSubject extends Subject 69 | { 70 | public function setState($state) 71 | { 72 | $this->stateNow = $state; 73 | $this->notify(); 74 | } 75 | 76 | public function getState() 77 | { 78 | return $this->stateNow; 79 | } 80 | } 81 | ``` 82 | 83 | 3. 抽象观察者 84 | 85 | ``` 86 | abstract class Observer 87 | { 88 | abstract public function update(Subject $subject); 89 | } 90 | ``` 91 | 92 | 在抽象观察者中,抽象方法`update`等待子类为它提供一个特定的实现。 93 | 94 | 4. 具体观察者 95 | 96 | ``` 97 | class ConcreteObserverDT extends Observer 98 | { 99 | private $currentState; 100 | 101 | public function update(Subject $subject) 102 | { 103 | $this->currentState = $subject->getState(); 104 | 105 | echo '
'. $this->currentState .'
'; 106 | } 107 | } 108 | 109 | class ConcreteObserverPhone extends Observer 110 | { 111 | private $currentState; 112 | 113 | public function update(Subject $subject) 114 | { 115 | $this->currentState = $subject->getState(); 116 | 117 | echo '
'. $this->currentState .'
'; 118 | } 119 | } 120 | ``` 121 | 122 | 在例子中为了理解起来简单,我们只是根据不同的客户端设置了不同的内容样式,实际应用中可以真正的调用邮件和短信服务来发送信息。 123 | 124 | 5. 使用观察者模式 125 | 126 | ``` 127 | class Client 128 | { 129 | public function __construct() 130 | { 131 | $sub = new ConcreteSubject(); 132 | 133 | $obDT = new ConcreteObserverDT(); 134 | $obPhone = new ConcreteObserverPhone(); 135 | 136 | $sub->attach($obDT); 137 | $sub->attach($obPhone); 138 | $sub->setState('Hello World'); 139 | } 140 | } 141 | 142 | $worker = new Client(); 143 | ``` 144 | 145 | 146 | 147 | ### 何时使用观察者模式 148 | 149 | - 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。 150 | - 一个对象必须通知其他对象,而并不知道这些对象是谁。 151 | - 基于事件触发机制来解耦复杂逻辑时,从整个逻辑的不同关键点抽象出不同的事件,主流程只需要关心最核心的逻辑并能正确地触发事件(Subject),其余相关功能实现由观察者或者叫订阅者来完成。 152 | 153 | 154 | 155 | ### 总结 156 | 157 | - 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 158 | - 模式包含四个角色:目标又称为主题,它是指被观察的对象;具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;观察者将对观察目标的改变做出反应;在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致。 159 | 160 | - 观察者模式的主要优点在于可以实现表示层和数据逻辑层的分离,并在观察目标和观察者之间建立一个抽象的耦合,支持广播通信;其主要缺点在于如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间,而且如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 161 | 162 | 上一篇: [Database 模型关联](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database4.md) 163 | 164 | 下一篇: [事件系统](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Event.md) 165 | -------------------------------------------------------------------------------- /articles/Request.md: -------------------------------------------------------------------------------- 1 | # Request 2 | 很多框架都会将来自客户端的请求抽象成类方便应用程序使用,在Laravel中也不例外。`Illuminate\Http\Request`类在Laravel框架中就是对客户端请求的抽象,它是构建在`Symfony`框架提供的Request组件基础之上的。今天这篇文章就简单来看看Laravel是怎么创建请求Request对象的,而关于Request对象为应用提供的能力我并不会过多去说,在我讲完创建过程后你也就知道去源码哪里找Request对象提供的方法了,网上有些速查表列举了一些Request提供的方法不过不够全并且有的也没有解释,所以我还是推荐在开发中如果好奇Request是否已经实现了你想要的能力时去Request的源码里看下有没有提供对应的方法,方法注释里都清楚地标明了每个方法的执行结果。下面让我们进入正题吧。 3 | 4 | ### 创建Request对象 5 | 我们可以在Laravel应用程序的`index.php`文件中看到,在Laravel应用程序正式启动完成前Request对象就已经被创建好了: 6 | 7 | ``` 8 | //public/index.php 9 | $app = require_once __DIR__.'/../bootstrap/app.php'; 10 | 11 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 12 | 13 | $response = $kernel->handle( 14 | //创建request对象 15 | $request = Illuminate\Http\Request::capture() 16 | ); 17 | ``` 18 | 19 | 客户端的HTTP请求是`Illuminate\Http\Request`类的对象 20 | 21 | ``` 22 | class Request extends SymfonyRequest implements Arrayable, ArrayAccess 23 | { 24 | //新建Request实例 25 | public static function capture() 26 | { 27 | static::enableHttpMethodParameterOverride(); 28 | 29 | return static::createFromBase(SymfonyRequest::createFromGlobals()); 30 | } 31 | } 32 | 33 | ``` 34 | 通过`Illuminate\Http\Request`类的源码可以看到它是继承自`Symfony Request`类的,所以`Illuminate\Http\Request`类中实现的很多功能都是以`Symfony Reques`提供的功能为基础来实现的。上面的代码就可以看到`capture`方法新建Request对象时也是依赖于`Symfony Request`类的实例的。 35 | 36 | ``` 37 | namespace Symfony\Component\HttpFoundation; 38 | class Request 39 | { 40 | /** 41 | * 根据PHP提供的超级全局数组来创建Smyfony Request实例 42 | * 43 | * @return static 44 | */ 45 | public static function createFromGlobals() 46 | { 47 | // With the php's bug #66606, the php's built-in web server 48 | // stores the Content-Type and Content-Length header values in 49 | // HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH fields. 50 | $server = $_SERVER; 51 | if ('cli-server' === PHP_SAPI) { 52 | if (array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) { 53 | $server['CONTENT_LENGTH'] = $_SERVER['HTTP_CONTENT_LENGTH']; 54 | } 55 | if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) { 56 | $server['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE']; 57 | } 58 | } 59 | 60 | $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server); 61 | 62 | if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') 63 | && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH')) 64 | ) { 65 | parse_str($request->getContent(), $data); 66 | $request->request = new ParameterBag($data); 67 | } 68 | 69 | return $request; 70 | } 71 | 72 | } 73 | ``` 74 | 75 | 上面的代码有一处需要额外解释一下,自PHP5.4开始PHP内建的builtin web server可以通过命令行解释器来启动,例如: 76 | 77 | > php -S localhost:8000 -t htdocs 78 | > 79 | > 80 | > 81 | > ``` 82 | > -S : Run with built-in web server. 83 | > -t Specify document root for built-in web server. 84 | > ``` 85 | > 86 | > 87 | 88 | 但是内建web server有一个bug是将`CONTENT_LENGTH`和`CONTENT_TYPE`这两个请求首部存储到了`HTTP_CONTENT_LENGTH`和`HTTP_CONTENT_TYPE`中,为了统一内建服务器和真正的server中的请求首部字段所以在这里做了特殊处理。 89 | 90 | 91 | 92 | Symfony Request 实例的创建是通过PHP中的超级全局数组来创建的,这些超级全局数组有`$_GET`,`$_POST`,`$_COOKIE`,`$_FILES`,`$_SERVER`涵盖了PHP中所有与HTTP请求相关的超级全局数组,创建Symfony Request实例时会根据这些全局数组创建Symfony Package里提供的`ParamterBag` `ServerBag` `FileBag` `HeaderBag`实例,这些Bag都是Symfony提供地针对不同HTTP组成部分的访问和设置API, 关于Symfony提供的`ParamterBag`这些实例有兴趣的读者自己去源码里看看吧,这里就不多说了。 93 | 94 | ``` 95 | class Request 96 | { 97 | 98 | /** 99 | * @param array $query The GET parameters 100 | * @param array $request The POST parameters 101 | * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) 102 | * @param array $cookies The COOKIE parameters 103 | * @param array $files The FILES parameters 104 | * @param array $server The SERVER parameters 105 | * @param string|resource|null $content The raw body data 106 | */ 107 | public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) 108 | { 109 | $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); 110 | } 111 | 112 | public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) 113 | { 114 | $this->request = new ParameterBag($request); 115 | $this->query = new ParameterBag($query); 116 | $this->attributes = new ParameterBag($attributes); 117 | $this->cookies = new ParameterBag($cookies); 118 | $this->files = new FileBag($files); 119 | $this->server = new ServerBag($server); 120 | $this->headers = new HeaderBag($this->server->getHeaders()); 121 | 122 | $this->content = $content; 123 | $this->languages = null; 124 | $this->charsets = null; 125 | $this->encodings = null; 126 | $this->acceptableContentTypes = null; 127 | $this->pathInfo = null; 128 | $this->requestUri = null; 129 | $this->baseUrl = null; 130 | $this->basePath = null; 131 | $this->method = null; 132 | $this->format = null; 133 | } 134 | 135 | } 136 | ``` 137 | 138 | 可以看到Symfony Request类除了上边说到的那几个,还有很多属性,这些属性在一起构成了对HTTP请求完整的抽象,我们可以通过实例属性方便地访问`Method`,`Charset`等这些HTTP请求的属性。 139 | 140 | 拿到Symfony Request实例后, Laravel会克隆这个实例并重设其中的一些属性: 141 | 142 | ``` 143 | namespace Illuminate\Http; 144 | class Request extends .... 145 | { 146 | //在Symfony request instance的基础上创建Request实例 147 | public static function createFromBase(SymfonyRequest $request) 148 | { 149 | if ($request instanceof static) { 150 | return $request; 151 | } 152 | 153 | $content = $request->content; 154 | 155 | $request = (new static)->duplicate( 156 | $request->query->all(), $request->request->all(), $request->attributes->all(), 157 | $request->cookies->all(), $request->files->all(), $request->server->all() 158 | ); 159 | 160 | $request->content = $content; 161 | 162 | $request->request = $request->getInputSource(); 163 | 164 | return $request; 165 | } 166 | 167 | public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) 168 | { 169 | return parent::duplicate($query, $request, $attributes, $cookies, $this->filterFiles($files), $server); 170 | } 171 | } 172 | //Symfony Request中的 duplicate方法 173 | public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) 174 | { 175 | $dup = clone $this; 176 | if (null !== $query) { 177 | $dup->query = new ParameterBag($query); 178 | } 179 | if (null !== $request) { 180 | $dup->request = new ParameterBag($request); 181 | } 182 | if (null !== $attributes) { 183 | $dup->attributes = new ParameterBag($attributes); 184 | } 185 | if (null !== $cookies) { 186 | $dup->cookies = new ParameterBag($cookies); 187 | } 188 | if (null !== $files) { 189 | $dup->files = new FileBag($files); 190 | } 191 | if (null !== $server) { 192 | $dup->server = new ServerBag($server); 193 | $dup->headers = new HeaderBag($dup->server->getHeaders()); 194 | } 195 | $dup->languages = null; 196 | $dup->charsets = null; 197 | $dup->encodings = null; 198 | $dup->acceptableContentTypes = null; 199 | $dup->pathInfo = null; 200 | $dup->requestUri = null; 201 | $dup->baseUrl = null; 202 | $dup->basePath = null; 203 | $dup->method = null; 204 | $dup->format = null; 205 | 206 | if (!$dup->get('_format') && $this->get('_format')) { 207 | $dup->attributes->set('_format', $this->get('_format')); 208 | } 209 | 210 | if (!$dup->getRequestFormat(null)) { 211 | $dup->setRequestFormat($this->getRequestFormat(null)); 212 | } 213 | 214 | return $dup; 215 | } 216 | ``` 217 | 218 | 219 | 220 | Request对象创建好后在Laravel应用中我们就能方便的应用它提供的能力了,在使用Request对象时如果你不知道它是否实现了你想要的功能,很简单直接去`Illuminate\Http\Request`的源码文件里查看就好了,所有方法都列在了这个源码文件里,比如: 221 | 222 | 223 | 224 | ``` 225 | /** 226 | * Get the full URL for the request. 227 | * 获取请求的URL(包含host, 不包括query string) 228 | * 229 | * @return string 230 | */ 231 | public function fullUrl() 232 | { 233 | $query = $this->getQueryString(); 234 | 235 | $question = $this->getBaseUrl().$this->getPathInfo() == '/' ? '/?' : '?'; 236 | 237 | return $query ? $this->url().$question.$query : $this->url(); 238 | } 239 | 240 | /** 241 | * Get the full URL for the request with the added query string parameters. 242 | * 获取包括了query string 的完整URL 243 | * 244 | * @param array $query 245 | * @return string 246 | */ 247 | public function fullUrlWithQuery(array $query) 248 | { 249 | $question = $this->getBaseUrl().$this->getPathInfo() == '/' ? '/?' : '?'; 250 | 251 | return count($this->query()) > 0 252 | ? $this->url().$question.http_build_query(array_merge($this->query(), $query)) 253 | : $this->fullUrl().$question.http_build_query($query); 254 | } 255 | ``` 256 | 257 | 258 | 259 | ### Request经过的驿站 260 | 261 | 创建完Request对象后, Laravel的Http Kernel会接着往下执行:加载服务提供器引导Laravel应用、启动应用、让Request经过基础的中间件、通过Router匹配查找Request对应的路由、执行匹配到的路由、Request经过路由上到中间件到达控制器方法。 262 | 263 | ### 总结 264 | 265 | 随着Request最终到达对应的控制器方法后它的使命基本上也就完成了, 在控制器方法里从Request中获取输入参数然后执行应用的某一业务逻辑获得结果,结果会被转化成Response响应对象返回给发起请求的客户端。 266 | 267 | 这篇文章主要梳理了Laravel中Request对象,主要是想让大家知道如何去查找Laravel中Request现有提供了哪些能力供我们使用避免我们在业务代码里重新造轮子去实现Request已经提供的方法。 268 | 269 | 上一篇: [控制器](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Controller.md) 270 | 271 | 下一篇: [Response](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Response.md) 272 | -------------------------------------------------------------------------------- /articles/Response.md: -------------------------------------------------------------------------------- 1 | # Response 2 | 3 | 前面两节我们分别讲了Laravel的控制器和Request对象,在讲Request对象的那一节我们看了Request对象是如何被创建出来的以及它支持的方法都定义在哪里,讲控制器时我们详细地描述了如何找到Request对应的控制器方法然后执行处理程序的,本节我们就来说剩下的那一部分,控制器方法的执行结果是如何被转换成响应对象Response然后返回给客户端的。 4 | 5 | 6 | 7 | ### 创建Response 8 | 9 | 让我们回到Laravel执行路由处理程序返回响应的代码块: 10 | 11 | ``` 12 | namespace Illuminate\Routing; 13 | class Router implements RegistrarContract, BindingRegistrar 14 | { 15 | protected function runRoute(Request $request, Route $route) 16 | { 17 | $request->setRouteResolver(function () use ($route) { 18 | return $route; 19 | }); 20 | 21 | $this->events->dispatch(new Events\RouteMatched($route, $request)); 22 | 23 | return $this->prepareResponse($request, 24 | $this->runRouteWithinStack($route, $request) 25 | ); 26 | } 27 | 28 | protected function runRouteWithinStack(Route $route, Request $request) 29 | { 30 | $shouldSkipMiddleware = $this->container->bound('middleware.disable') && 31 | $this->container->make('middleware.disable') === true; 32 | //收集路由和控制器里应用的中间件 33 | $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); 34 | 35 | return (new Pipeline($this->container)) 36 | ->send($request) 37 | ->through($middleware) 38 | ->then(function ($request) use ($route) { 39 | return $this->prepareResponse( 40 | $request, $route->run() 41 | ); 42 | }); 43 | 44 | } 45 | } 46 | ``` 47 | 48 | 49 | 50 | 在讲控制器的那一节里我们已经提到过`runRouteWithinStack`方法里是最终执行路由处理程序(控制器方法或者闭包处理程序)的地方,通过上面的代码我们也可以看到执行的结果会传递给`Router`的`prepareResponse`方法,当程序流返回到`runRoute`里后又执行了一次`prepareResponse`方法得到了要返回给客户端的Response对象, 下面我们就来详细看一下`prepareResponse`方法。 51 | 52 | ``` 53 | class Router implements RegistrarContract, BindingRegistrar 54 | { 55 | /** 56 | * 通过给定值创建Response对象 57 | * 58 | * @param \Symfony\Component\HttpFoundation\Request $request 59 | * @param mixed $response 60 | * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse 61 | */ 62 | public function prepareResponse($request, $response) 63 | { 64 | return static::toResponse($request, $response); 65 | } 66 | 67 | public static function toResponse($request, $response) 68 | { 69 | if ($response instanceof Responsable) { 70 | $response = $response->toResponse($request); 71 | } 72 | 73 | if ($response instanceof PsrResponseInterface) { 74 | $response = (new HttpFoundationFactory)->createResponse($response); 75 | } elseif (! $response instanceof SymfonyResponse && 76 | ($response instanceof Arrayable || 77 | $response instanceof Jsonable || 78 | $response instanceof ArrayObject || 79 | $response instanceof JsonSerializable || 80 | is_array($response))) { 81 | $response = new JsonResponse($response); 82 | } elseif (! $response instanceof SymfonyResponse) { 83 | $response = new Response($response); 84 | } 85 | 86 | if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) { 87 | $response->setNotModified(); 88 | } 89 | 90 | return $response->prepare($request); 91 | } 92 | } 93 | ``` 94 | 95 | 在上面的代码中我们看到有三种Response: 96 | 97 | 98 | 99 | | Class Name | Representation | 100 | | ------------------------------------------------------------ | --------------------------------- | 101 | | PsrResponseInterface(Psr\Http\Message\ResponseInterface的别名) | Psr规范中对服务端响应的定义 | 102 | | Illuminate\Http\JsonResponse (Symfony\Component\HttpFoundation\Response的子类) | Laravel中对服务端JSON响应的定义 | 103 | | Illuminate\Http\Response (Symfony\Component\HttpFoundation\Response的子类) | Laravel中对普通的非JSON响应的定义 | 104 | 105 | 通过`prepareResponse`中的逻辑可以看到,无论路由执行结果返回的是什么值最终都会被Laravel转换为成一个Response对象,而这些对象都是Symfony\Component\HttpFoundation\Response类或者其子类的对象。从这里也就能看出来跟Request一样Laravel的Response也是依赖Symfony框架的`HttpFoundation`组件来实现的。 106 | 107 | 我们来看一下Symfony\Component\HttpFoundation\Response的构造方法: 108 | 109 | ``` 110 | namespace Symfony\Component\HttpFoundation; 111 | class Response 112 | { 113 | public function __construct($content = '', $status = 200, $headers = array()) 114 | { 115 | $this->headers = new ResponseHeaderBag($headers); 116 | $this->setContent($content); 117 | $this->setStatusCode($status); 118 | $this->setProtocolVersion('1.0'); 119 | } 120 | //设置响应的Content 121 | public function setContent($content) 122 | { 123 | if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) { 124 | throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', gettype($content))); 125 | } 126 | 127 | $this->content = (string) $content; 128 | 129 | return $this; 130 | } 131 | } 132 | ``` 133 | 134 | 所以路由处理程序的返回值在创业Response对象时会设置到对象的content属性里,该属性的值就是返回给客户端的响应的响应内容。 135 | 136 | ### 设置Response headers 137 | 138 | 生成Response对象后就要执行对象的`prepare`方法了,该方法定义在`Symfony\Component\HttpFoundation\Resposne`类中,其主要目的是对Response进行微调使其能够遵从HTTP/1.1协议(RFC 2616)。 139 | 140 | ``` 141 | namespace Symfony\Component\HttpFoundation; 142 | class Response 143 | { 144 | //在响应被发送给客户端之前对其进行修订使其能遵从HTTP/1.1协议 145 | public function prepare(Request $request) 146 | { 147 | $headers = $this->headers; 148 | 149 | if ($this->isInformational() || $this->isEmpty()) { 150 | $this->setContent(null); 151 | $headers->remove('Content-Type'); 152 | $headers->remove('Content-Length'); 153 | } else { 154 | // Content-type based on the Request 155 | if (!$headers->has('Content-Type')) { 156 | $format = $request->getRequestFormat(); 157 | if (null !== $format && $mimeType = $request->getMimeType($format)) { 158 | $headers->set('Content-Type', $mimeType); 159 | } 160 | } 161 | 162 | // Fix Content-Type 163 | $charset = $this->charset ?: 'UTF-8'; 164 | if (!$headers->has('Content-Type')) { 165 | $headers->set('Content-Type', 'text/html; charset='.$charset); 166 | } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) { 167 | // add the charset 168 | $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset); 169 | } 170 | 171 | // Fix Content-Length 172 | if ($headers->has('Transfer-Encoding')) { 173 | $headers->remove('Content-Length'); 174 | } 175 | 176 | if ($request->isMethod('HEAD')) { 177 | // cf. RFC2616 14.13 178 | $length = $headers->get('Content-Length'); 179 | $this->setContent(null); 180 | if ($length) { 181 | $headers->set('Content-Length', $length); 182 | } 183 | } 184 | } 185 | 186 | // Fix protocol 187 | if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) { 188 | $this->setProtocolVersion('1.1'); 189 | } 190 | 191 | // Check if we need to send extra expire info headers 192 | if ('1.0' == $this->getProtocolVersion() && false !== strpos($this->headers->get('Cache-Control'), 'no-cache')) { 193 | $this->headers->set('pragma', 'no-cache'); 194 | $this->headers->set('expires', -1); 195 | } 196 | 197 | $this->ensureIEOverSSLCompatibility($request); 198 | 199 | return $this; 200 | } 201 | } 202 | ``` 203 | 204 | `prepare`里针对各种情况设置了相应的`response header` 比如`Content-Type`、`Content-Length`等等这些我们常见的首部字段。 205 | 206 | 207 | 208 | ### 发送Response 209 | 210 | 创建并设置完Response后它会流经路由和框架中间件的后置操作,在中间件的后置操作里一般都是对Response进行进一步加工,最后程序流回到Http Kernel那里, Http Kernel会把Response发送给客户端,我们来看一下这部分的代码。 211 | 212 | ``` 213 | //入口文件public/index.php 214 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 215 | 216 | $response = $kernel->handle( 217 | $request = Illuminate\Http\Request::capture() 218 | ); 219 | 220 | $response->send(); 221 | 222 | $kernel->terminate($request, $response); 223 | ``` 224 | 225 | ``` 226 | namespace Symfony\Component\HttpFoundation; 227 | class Response 228 | { 229 | public function send() 230 | { 231 | $this->sendHeaders(); 232 | $this->sendContent(); 233 | 234 | if (function_exists('fastcgi_finish_request')) { 235 | fastcgi_finish_request(); 236 | } elseif ('cli' !== PHP_SAPI) { 237 | static::closeOutputBuffers(0, true); 238 | } 239 | 240 | return $this; 241 | } 242 | 243 | //发送headers到客户端 244 | public function sendHeaders() 245 | { 246 | // headers have already been sent by the developer 247 | if (headers_sent()) { 248 | return $this; 249 | } 250 | 251 | // headers 252 | foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { 253 | foreach ($values as $value) { 254 | header($name.': '.$value, false, $this->statusCode); 255 | } 256 | } 257 | 258 | // status 259 | header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); 260 | 261 | // cookies 262 | foreach ($this->headers->getCookies() as $cookie) { 263 | if ($cookie->isRaw()) { 264 | setrawcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); 265 | } else { 266 | setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); 267 | } 268 | } 269 | 270 | return $this; 271 | } 272 | 273 | //发送响应内容到客户端 274 | public function sendContent() 275 | { 276 | echo $this->content; 277 | 278 | return $this; 279 | } 280 | } 281 | ``` 282 | 283 | `send`的逻辑就非常好理解了,把之前设置好的那些headers设置到HTTP响应的首部字段里,Content会echo后被设置到HTTP响应的主体实体中。最后PHP会把完整的HTTP响应发送给客户端。 284 | 285 | send响应后Http Kernel会执行`terminate`方法调用terminate中间件里的`terminate`方法,最后执行应用的`termiate`方法来结束整个应用生命周期(从接收请求开始到返回响应结束)。 286 | 287 | 288 | 上一篇: [Request](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Request.md) 289 | 290 | 下一篇: [Database 基础介绍](https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/Database1.md) 291 | -------------------------------------------------------------------------------- /articles/reflection.md: -------------------------------------------------------------------------------- 1 | # 类的反射和依赖注入 2 | 3 | 在讲服务容器之前我想先梳理下PHP反射相关的知识,PHP反射是程序实现依赖注入的基础,也是Laravel的服务容器实现服务解析的基础,如果你已经掌握了这方面基础知识,那么可以跳过本文直接看服务容器部分的内容。 4 | 5 | PHP具有完整的反射 API,提供了对类、接口、函数、方法和扩展进行逆向工程的能力。通过类的反射提供的能力我们能够知道类是如何被定义的,它有什么属性、什么方法、方法都有哪些参数,类文件的路径是什么等很重要的信息。也正式因为类的反射很多PHP框架才能实现依赖注入自动解决类与类之间的依赖关系,这给我们平时的开发带来了很大的方便。 本文主要是讲解如何利用类的反射来实现依赖注入(Dependency Injection),并不会去逐条讲述PHP Reflection里的每一个API,详细的API参考信息请查阅[官方文档][1] 6 | 7 | **再次声明这里实现的依赖注入非常简单,并不能应用到实际开发中去,可以参考后面的文章[服务容器(IocContainer)][2], 了解Laravel的服务容器是如何实现依赖注入的。** 8 | 9 | 为了更好地理解,我们通过一个例子来看类的反射,以及如何实现依赖注入。 10 | 下面这个类代表了坐标系里的一个点,有两个属性横坐标x和纵坐标y。 11 | ```php 12 | /** 13 | * Class Point 14 | */ 15 | class Point 16 | { 17 | public $x; 18 | public $y; 19 | 20 | /** 21 | * Point constructor. 22 | * @param int $x horizontal value of point's coordinate 23 | * @param int $y vertical value of point's coordinate 24 | */ 25 | public function __construct($x = 0, $y = 0) 26 | { 27 | $this->x = $x; 28 | $this->y = $y; 29 | } 30 | } 31 | ``` 32 | 接下来这个类代表圆形,可以看到在它的构造函数里有一个参数是`Point`类的,即`Circle`类是依赖与`Point`类的。 33 | 34 | ```php 35 | class Circle 36 | { 37 | /** 38 | * @var int 39 | */ 40 | public $radius;//半径 41 | 42 | /** 43 | * @var Point 44 | */ 45 | public $center;//圆心点 46 | 47 | const PI = 3.14; 48 | 49 | public function __construct(Point $point, $radius = 1) 50 | { 51 | $this->center = $point; 52 | $this->radius = $radius; 53 | } 54 | 55 | //打印圆点的坐标 56 | public function printCenter() 57 | { 58 | printf('center coordinate is (%d, %d)', $this->center->x, $this->center->y); 59 | } 60 | 61 | //计算圆形的面积 62 | public function area() 63 | { 64 | return 3.14 * pow($this->radius, 2); 65 | } 66 | } 67 | ``` 68 | ReflectionClass 69 | -- 70 | 71 | 下面我们通过反射来对`Circle`这个类进行反向工程。 72 | 把`Circle`类的名字传递给`reflectionClass`来实例化一个`ReflectionClass`类的对象。 73 | 74 | ```php 75 | $reflectionClass = new reflectionClass(Circle::class); 76 | //返回值如下 77 | object(ReflectionClass)#1 (1) { 78 | ["name"]=> 79 | string(6) "Circle" 80 | } 81 | ``` 82 | 反射出类的常量 83 | -- 84 | ```php 85 | $reflectionClass->getConstants(); 86 | ``` 87 | 返回一个由常量名称和值构成的关联数组 88 | ```php 89 | array(1) { 90 | ["PI"]=> 91 | float(3.14) 92 | } 93 | ``` 94 | 95 | 通过反射获取属性 96 | -- 97 | ```php 98 | $reflectionClass->getProperties(); 99 | ``` 100 | 返回一个由ReflectionProperty对象构成的数组 101 | ```php 102 | array(2) { 103 | [0]=> 104 | object(ReflectionProperty)#2 (2) { 105 | ["name"]=> 106 | string(6) "radius" 107 | ["class"]=> 108 | string(6) "Circle" 109 | } 110 | [1]=> 111 | object(ReflectionProperty)#3 (2) { 112 | ["name"]=> 113 | string(6) "center" 114 | ["class"]=> 115 | string(6) "Circle" 116 | } 117 | } 118 | ``` 119 | 反射出类中定义的方法 120 | -- 121 | ```php 122 | $reflectionClass->getMethods(); 123 | ``` 124 | 返回ReflectionMethod对象构成的数组 125 | ```php 126 | array(3) { 127 | [0]=> 128 | object(ReflectionMethod)#2 (2) { 129 | ["name"]=> 130 | string(11) "__construct" 131 | ["class"]=> 132 | string(6) "Circle" 133 | } 134 | [1]=> 135 | object(ReflectionMethod)#3 (2) { 136 | ["name"]=> 137 | string(11) "printCenter" 138 | ["class"]=> 139 | string(6) "Circle" 140 | } 141 | [2]=> 142 | object(ReflectionMethod)#4 (2) { 143 | ["name"]=> 144 | string(4) "area" 145 | ["class"]=> 146 | string(6) "Circle" 147 | } 148 | } 149 | ``` 150 | 我们还可以通过`getConstructor()`来单独获取类的构造方法,其返回值为一个`ReflectionMethod`对象。 151 | ```php 152 | $constructor = $reflectionClass->getConstructor(); 153 | ``` 154 | 反射出方法的参数 155 | -- 156 | ```php 157 | $parameters = $constructor->getParameters(); 158 | ``` 159 | 其返回值为ReflectionParameter对象构成的数组。 160 | ```php 161 | array(2) { 162 | [0]=> 163 | object(ReflectionParameter)#3 (1) { 164 | ["name"]=> 165 | string(5) "point" 166 | } 167 | [1]=> 168 | object(ReflectionParameter)#4 (1) { 169 | ["name"]=> 170 | string(6) "radius" 171 | } 172 | } 173 | ``` 174 | 175 | 依赖注入 176 | -- 177 | 好了接下来我们编写一个名为`make`的函数,传递类名称给`make`函数返回类的对象,在`make`里它会帮我们注入类的依赖,即在本例中帮我们注入`Point`对象给`Circle`类的构造方法。 178 | ```php 179 | //构建类的对象 180 | function make($className) 181 | { 182 | $reflectionClass = new ReflectionClass($className); 183 | $constructor = $reflectionClass->getConstructor(); 184 | $parameters = $constructor->getParameters(); 185 | $dependencies = getDependencies($parameters); 186 | 187 | return $reflectionClass->newInstanceArgs($dependencies); 188 | } 189 | 190 | //依赖解析 191 | function getDependencies($parameters) 192 | { 193 | $dependencies = []; 194 | foreach($parameters as $parameter) { 195 | $dependency = $parameter->getClass(); 196 | if (is_null($dependency)) { 197 | if($parameter->isDefaultValueAvailable()) { 198 | $dependencies[] = $parameter->getDefaultValue(); 199 | } else { 200 | //不是可选参数的为了简单直接赋值为字符串0 201 | //针对构造方法的必须参数这个情况 202 | //laravel是通过service provider注册closure到IocContainer, 203 | //在closure里可以通过return new Class($param1, $param2)来返回类的实例 204 | //然后在make时回调这个closure即可解析出对象 205 | //具体细节我会在另一篇文章里面描述 206 | $dependencies[] = '0'; 207 | } 208 | } else { 209 | //递归解析出依赖类的对象 210 | $dependencies[] = make($parameter->getClass()->name); 211 | } 212 | } 213 | 214 | return $dependencies; 215 | } 216 | ``` 217 | 定义好`make`方法后我们通过它来帮我们实例化Circle类的对象: 218 | ```php 219 | $circle = make('Circle'); 220 | $area = $circle->area(); 221 | /*var_dump($circle, $area); 222 | object(Circle)#6 (2) { 223 | ["radius"]=> 224 | int(1) 225 | ["center"]=> 226 | object(Point)#11 (2) { 227 | ["x"]=> 228 | int(0) 229 | ["y"]=> 230 | int(0) 231 | } 232 | } 233 | float(3.14)*/ 234 | ``` 235 | 236 | 通过上面这个实例我简单描述了一下如何利用PHP类的反射来实现依赖注入,Laravel的依赖注入也是通过这个思路来实现的,只不过设计的更精密大量地利用了闭包回调来应对各种复杂的依赖注入。 237 | 238 | 本文的[示例代码的下载链接][4] 239 | 240 | 241 | 下一篇:[Laravel服务容器][3] 242 | 243 | [1]: http://php.net/manual/zh/intro.reflection.php 244 | [2]: https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/IocContainer.md 245 | [3]: https://github.com/kevinyan815/Learning_Laravel_Kernel/blob/master/articles/IocContainer.md 246 | [4]: https://github.com/kevinyan815/php_reflection_dependency_injection_demo/blob/master/reflection.php 247 | -------------------------------------------------------------------------------- /codes/reflection_dependency_injection_demo.php: -------------------------------------------------------------------------------- 1 | center = $point; 19 | $this->radius = $radius; 20 | } 21 | 22 | public function printCenter() 23 | { 24 | printf('center coordinate is (%d, %d)', $this->center->x, $this->center->y); 25 | } 26 | 27 | public function area() 28 | { 29 | return 3.14 * pow($this->radius, 2); 30 | } 31 | } 32 | 33 | /** 34 | * Class Point 35 | */ 36 | class Point 37 | { 38 | public $x; 39 | public $y; 40 | 41 | /** 42 | * Point constructor. 43 | * @param int $x horizontal value of point's coordinate 44 | * @param int $y vertical value of point's coordinate 45 | */ 46 | public function __construct($x = 0, $y = 0) 47 | { 48 | $this->x = $x; 49 | $this->y = $y; 50 | } 51 | } 52 | 53 | 54 | $reflectionClass = new reflectionClass(Circle::class); 55 | 56 | 57 | function make($className) 58 | { 59 | $reflectionClass = new ReflectionClass($className); 60 | $constructor = $reflectionClass->getConstructor(); 61 | $parameters = $constructor->getParameters(); 62 | $dependencies = getDependencies($parameters); 63 | 64 | return $reflectionClass->newInstanceArgs($dependencies); 65 | } 66 | 67 | function getDependencies($parameters) 68 | { 69 | $dependencies = []; 70 | foreach($parameters as $parameter) { 71 | $dependency = $parameter->getClass(); 72 | if (is_null($dependency)) { 73 | if($parameter->isDefaultValueAvailable()) { 74 | $dependencies[] = $parameter->getDefaultValue(); 75 | } else { 76 | //to easily implement this function, I just assume 0 to built-in type parameters 77 | $dependencies[] = '0'; 78 | } 79 | } else { 80 | $dependencies[] = make($parameter->getClass()->name); 81 | } 82 | } 83 | 84 | return $dependencies; 85 | } 86 | 87 | $circle = make('Circle'); 88 | $area = $circle->area(); 89 | -------------------------------------------------------------------------------- /images/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/WX20200119-143845@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyan815/Learning_Laravel_Kernel/77048f9676738898cdf3201f823d4df4b7f7ff77/images/WX20200119-143845@2x.png -------------------------------------------------------------------------------- /images/WechatDonation.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyan815/Learning_Laravel_Kernel/77048f9676738898cdf3201f823d4df4b7f7ff77/images/WechatDonation.jpeg -------------------------------------------------------------------------------- /images/tWbHIMFsM3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinyan815/Learning_Laravel_Kernel/77048f9676738898cdf3201f823d4df4b7f7ff77/images/tWbHIMFsM3.png --------------------------------------------------------------------------------