├── Laravel Broadcast——广播系统源码剖析.md ├── Laravel Config—— 配置文件的加载与源码解析.md ├── Laravel Container——IoC 服务容器.md ├── Laravel Container——IoC 服务容器源码解析(服务器绑定).md ├── Laravel Container——IoC 服务容器源码解析(服务器解析).md ├── Laravel Container——服务容器的细节特性.md ├── Laravel Database——Eloquent Model 关联源码分析.md ├── Laravel Database——Eloquent Model 更新关联模型.md ├── Laravel Database——Eloquent Model 模型关系加载与查询.md ├── Laravel Database——Eloquent Model 源码分析(上).md ├── Laravel Database——Eloquent Model 源码分析(下).md ├── Laravel Database——分页原理与源码分析.md ├── Laravel Database——数据库服务的启动与连接.md ├── Laravel Database——数据库的 CRUD 操作.md ├── Laravel Database——查询构造器与语法编译器源码分析(上).md ├── Laravel Database——查询构造器与语法编译器源码分析(下).md ├── Laravel Database——查询构造器与语法编译器源码分析(中).md ├── Laravel ENV—— 环境变量的加载与源码解析.md ├── Laravel Event——事件系统的启动与运行源码分析.md ├── Laravel Exceptions——异常与错误处理.md ├── Laravel Facade——Facade 门面源码分析.md ├── Laravel HTTP—— RESTFul 风格路由的使用与源码分析.md ├── Laravel HTTP——Pipeline中间件处理源码分析.md ├── Laravel HTTP——SubstituteBindings 参数绑定中间件的使用与源码解析.md ├── Laravel HTTP——控制器方法的参数构建与运行.md ├── Laravel HTTP——路由.md ├── Laravel HTTP——路由中间件源码分析.md ├── Laravel HTTP——路由加载源码分析.md ├── Laravel HTTP——路由的匹配与参数绑定.md ├── Laravel HTTP——路由的正则编译.md ├── Laravel HTTP——重定向的使用与源码分析.md ├── Laravel Passport——OAuth2 API 认证系统源码解析.md ├── Laravel Passport——OAuth2 API 认证系统源码解析(下).md ├── Laravel Providers——服务提供者的注册与启动源码解析.md ├── Laravel Queue——消息队列任务与分发源码剖析.md ├── Laravel Queue——消息队列任务处理器源码剖析.md ├── Laravel Session——session 的启动与运行源码分析.md ├── PHP Composer-——-注册与运行源码分析.md ├── PHP Composer—— 初始化源码分析.md ├── PHP Composer——自动加载原理.md ├── README.md └── SUMMARY.md /Laravel Broadcast——广播系统源码剖析.md: -------------------------------------------------------------------------------- 1 | # Laravel Broadcast——广播系统源码剖析 2 | 3 | ## 前言 4 | 5 | 在现代的 `web` 应用程序中,`WebSockets` 被用来实现需要实时、即时更新的接口。当服务器上的数据被更新后,更新信息将通过 `WebSocket` 连接发送到客户端等待处理。相比于不停地轮询应用程序,`WebSocket` 是一种更加可靠和高效的选择。 6 | 7 | 我们先用一个电子商务网站作为例子来概览一下事件广播。当用户在查看自己的订单时,我们不希望他们必须通过刷新页面才能看到状态更新。我们希望一旦有更新时就主动将更新信息广播到客户端。 8 | 9 | `laravel` 的广播系统和队列系统类似,需要两个进程协作,一个是 `laravel` 的 `web` 后台系统,另一个是 `Socket.IO` 服务器系统。具体的流程是页面加载时,网页 `js` 程序 `Laravel Echo` 与 `Socket.IO` 服务器建立连接, `laravel` 发起通过驱动发布广播,`Socket.IO` 服务器接受广播内容,对连接的客户端网页推送信息,以达到网页实时更新的目的。 10 | 11 | `laravel` 发起广播的方式有两种,`redis` 与 `pusher`。对于 `redis` 来说,需要支持 `Socket.IO` 服务器系统,官方推荐 `nodejs` 为底层的 `tlaverdure/laravel-echo-server`。对于 `pusher` 来说,该第三方服务包含了驱动与 `Socket.IO` 服务器。 12 | 13 | 本文将会介绍 `redis` 为驱动的广播源码,由于 `laravel-echo-server` 是 `nodejs` 编写,本文也无法介绍 `Socket.IO` 方面的内容。 14 | 15 | ## 广播系统服务的启动 16 | 17 | 和其他服务类似,广播系统服务的注册实质上就是对 `Ioc` 容器注册门面类,广播系统的门面类是 `BroadcastManager`: 18 | 19 | ```php 20 | class BroadcastServiceProvider extends ServiceProvider 21 | { 22 | public function register() 23 | { 24 | $this->app->singleton(BroadcastManager::class, function ($app) { 25 | return new BroadcastManager($app); 26 | }); 27 | 28 | $this->app->singleton(BroadcasterContract::class, function ($app) { 29 | return $app->make(BroadcastManager::class)->connection(); 30 | }); 31 | 32 | $this->app->alias( 33 | BroadcastManager::class, BroadcastingFactory::class 34 | ); 35 | } 36 | } 37 | ``` 38 | 39 | 除了注册 `BroadcastManager`,`BroadcastServiceProvider` 还进行了广播驱动的启动: 40 | 41 | ```php 42 | public function connection($driver = null) 43 | { 44 | return $this->driver($driver); 45 | } 46 | 47 | public function driver($name = null) 48 | { 49 | $name = $name ?: $this->getDefaultDriver(); 50 | 51 | return $this->drivers[$name] = $this->get($name); 52 | } 53 | 54 | protected function get($name) 55 | { 56 | return isset($this->drivers[$name]) ? $this->drivers[$name] : $this->resolve($name); 57 | } 58 | 59 | protected function resolve($name) 60 | { 61 | $config = $this->getConfig($name); 62 | 63 | if (is_null($config)) { 64 | throw new InvalidArgumentException("Broadcaster [{$name}] is not defined."); 65 | } 66 | 67 | if (isset($this->customCreators[$config['driver']])) { 68 | return $this->callCustomCreator($config); 69 | } 70 | 71 | $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; 72 | 73 | if (! method_exists($this, $driverMethod)) { 74 | throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported."); 75 | } 76 | 77 | return $this->{$driverMethod}($config); 78 | } 79 | 80 | protected function createRedisDriver(array $config) 81 | { 82 | return new RedisBroadcaster( 83 | $this->app->make('redis'), Arr::get($config, 'connection') 84 | ); 85 | } 86 | ``` 87 | 88 | ## 广播信息的发布 89 | 90 | 广播信息的发布与事件的发布大致相同,要告知 `Laravel` 一个给定的事件是广播类型,只需在事件类中实现 `Illuminate\Contracts\Broadcasting\ShouldBroadcast` 接口即可。该接口已经被导入到所有由框架生成的事件类中,所以可以很方便地将它添加到自己的事件中。 91 | 92 | `ShouldBroadcast` 接口要求你实现一个方法:`broadcastOn`. `broadcastOn` 方法返回一个频道或一个频道数组,事件会被广播到这些频道。频道必须是 `Channel`、`PrivateChannel` 或 `PresenceChannel` 的实例。`Channel` 实例表示任何用户都可以订阅的公开频道,而 `PrivateChannels` 和 `PresenceChannels` 则表示需要 频道授权 的私有频道: 93 | 94 | ```php 95 | class ServerCreated implements ShouldBroadcast 96 | { 97 | use SerializesModels; 98 | 99 | public $user; 100 | 101 | //默认情况下,每一个广播事件都被添加到默认的队列上,默认的队列连接在 queue.php 配置文件中指定。可以通过在事件类中定义一个 broadcastQueue 属性来自定义广播器使用的队列。该属性用于指定广播使用的队列名称: 102 | public $broadcastQueue = 'your-queue-name'; 103 | 104 | public function __construct(User $user) 105 | { 106 | $this->user = $user; 107 | } 108 | 109 | public function broadcastOn() 110 | { 111 | return new PrivateChannel('user.'.$this->user->id); 112 | } 113 | 114 | //Laravel 默认会使用事件的类名作为广播名称来广播事件,自定义: 115 | public function broadcastAs() 116 | { 117 | return 'server.created'; 118 | } 119 | 120 | //想更细粒度地控制广播数据: 121 | public function broadcastWith() 122 | { 123 | return ['id' => $this->user->id]; 124 | } 125 | 126 | //有时,想在给定条件为 true ,才广播事件: 127 | public function broadcastWhen() 128 | { 129 | return $this->value > 100; 130 | } 131 | } 132 | ``` 133 | 134 | 然后,只需要像平时那样触发事件。一旦事件被触发,一个队列任务会自动广播事件到你指定的广播驱动器上。 135 | 136 | 当一个事件被广播时,它所有的 `public` 属性会自动被序列化为广播数据,这允许你在你的 `JavaScript` 应用中访问事件的公有数据。因此,举个例子,如果你的事件有一个公有的 `$user` 属性,它包含了一个 `Elouqent` 模型,那么事件的广播数据会是: 137 | 138 | ```php 139 | { 140 | "user": { 141 | "id": 1, 142 | "name": "Patrick Stewart" 143 | ... 144 | } 145 | } 146 | 147 | ``` 148 | 149 | ## 广播发布的源码 150 | 151 | 广播的发布与事件的触发是一体的,具体的流程我们已经在 `event` 的源码中介绍清楚了,现在我们来看唯一的不同: 152 | 153 | ```php 154 | public function dispatch($event, $payload = [], $halt = false) 155 | { 156 | list($event, $payload) = $this->parseEventAndPayload( 157 | $event, $payload 158 | ); 159 | 160 | if ($this->shouldBroadcast($payload)) { 161 | $this->broadcastEvent($payload[0]); 162 | } 163 | 164 | ... 165 | } 166 | 167 | protected function shouldBroadcast(array $payload) 168 | { 169 | return isset($payload[0]) && $payload[0] instanceof ShouldBroadcast; 170 | } 171 | 172 | protected function broadcastEvent($event) 173 | { 174 | $this->container->make(BroadcastFactory::class)->queue($event); 175 | } 176 | ``` 177 | 178 | 可见,关键之处在于 `BroadcastManager` 的 `quene` 方法: 179 | 180 | ```php 181 | public function queue($event) 182 | { 183 | $connection = $event instanceof ShouldBroadcastNow ? 'sync' : null; 184 | 185 | if (is_null($connection) && isset($event->connection)) { 186 | $connection = $event->connection; 187 | } 188 | 189 | $queue = null; 190 | 191 | if (isset($event->broadcastQueue)) { 192 | $queue = $event->broadcastQueue; 193 | } elseif (isset($event->queue)) { 194 | $queue = $event->queue; 195 | } 196 | 197 | $this->app->make('queue')->connection($connection)->pushOn( 198 | $queue, new BroadcastEvent(clone $event) 199 | ); 200 | } 201 | ``` 202 | 203 | 可见,`quene` 方法将广播事件包装为事件类,并且通过队列发布,我们接下来看这个事件类的处理: 204 | 205 | ```php 206 | class BroadcastEvent implements ShouldQueue 207 | { 208 | public function handle(Broadcaster $broadcaster) 209 | { 210 | $name = method_exists($this->event, 'broadcastAs') 211 | ? $this->event->broadcastAs() : get_class($this->event); 212 | 213 | $broadcaster->broadcast( 214 | array_wrap($this->event->broadcastOn()), $name, 215 | $this->getPayloadFromEvent($this->event) 216 | ); 217 | } 218 | 219 | protected function getPayloadFromEvent($event) 220 | { 221 | if (method_exists($event, 'broadcastWith')) { 222 | return array_merge( 223 | $event->broadcastWith(), ['socket' => data_get($event, 'socket')] 224 | ); 225 | } 226 | 227 | $payload = []; 228 | 229 | foreach ((new ReflectionClass($event))->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { 230 | $payload[$property->getName()] = $this->formatProperty($property->getValue($event)); 231 | } 232 | 233 | return $payload; 234 | } 235 | 236 | protected function formatProperty($value) 237 | { 238 | if ($value instanceof Arrayable) { 239 | return $value->toArray(); 240 | } 241 | 242 | return $value; 243 | } 244 | } 245 | ``` 246 | 247 | 可见该事件主要调用 `broadcaster` 的 `broadcast` 方法,我们这里讲 `redis` 的发布: 248 | 249 | ```php 250 | class RedisBroadcaster extends Broadcaster 251 | { 252 | public function broadcast(array $channels, $event, array $payload = []) 253 | { 254 | $connection = $this->redis->connection($this->connection); 255 | 256 | $payload = json_encode([ 257 | 'event' => $event, 258 | 'data' => $payload, 259 | 'socket' => Arr::pull($payload, 'socket'), 260 | ]); 261 | 262 | foreach ($this->formatChannels($channels) as $channel) { 263 | $connection->publish($channel, $payload); 264 | } 265 | } 266 | } 267 | 268 | protected function formatChannels(array $channels) 269 | { 270 | return array_map(function ($channel) { 271 | return (string) $channel; 272 | }, $channels); 273 | } 274 | ``` 275 | 276 | `broadcast` 方法运用了 `redis` 的 `publish` 方法,对 `redis` 进行了频道的信息发布。 277 | 278 | ## 频道授权 279 | 280 | 对于私有频道,用户只有被授权后才能监听。实现过程是用户向 `Laravel` 应用程序发起一个携带频道名称的 `HTTP` 请求,应用程序判断该用户是否能够监听该频道。在使用 `Laravel Echo` 时,上述 `HTTP` 请求会被自动发送;尽管如此,仍然需要定义适当的路由来响应这些请求。 281 | 282 | ### 定义授权路由 283 | 284 | 我们可以在 Laravel 里很容易地定义路由来响应频道授权请求。 285 | 286 | ```php 287 | Broadcast::routes(); 288 | 289 | ``` 290 | `Broadcast::routes` 方法会自动把它的路由放进 `web` 中间件组中;另外,如果你想对一些属性自定义,可以向该方法传递一个包含路由属性的数组 291 | 292 | ```php 293 | Broadcast::routes($attributes); 294 | 295 | ``` 296 | ### 定义授权回调 297 | 298 | 接下来,我们需要定义真正用于处理频道授权的逻辑。这是在 `routes/channels.php` 文件中完成。在该文件中,你可以用 `Broadcast::channel` 方法来注册频道授权回调函数: 299 | 300 | ```php 301 | Broadcast::channel('order.{orderId}', function ($user, $orderId) { 302 | return $user->id === Order::findOrNew($orderId)->user_id; 303 | }); 304 | ``` 305 | 306 | `channel` 方法接收两个参数:频道名称和一个回调函数,该回调通过返回 `true` 或 `false` 来表示用户是否被授权监听该频道。 307 | 308 | 所有的授权回调接收当前被认证的用户作为第一个参数,任何额外的通配符参数作为后续参数。在本例中,我们使用 `{orderId}` 占位符来表示频道名称的「ID」部分是通配符。 309 | 310 | ### 授权回调模型绑定 311 | 312 | 就像 `HTTP` 路由一样,频道路由也可以利用显式或隐式 路由模型绑定。例如,相比于接收一个字符串或数字类型的 `order ID`,你也可以请求一个真正的 `Order` 模型实例: 313 | 314 | ```php 315 | Broadcast::channel('order.{order}', function ($user, Order $order) { 316 | return $user->id === $order->user_id; 317 | }); 318 | 319 | ``` 320 | 321 | ## 频道授权源码分析 322 | 323 | ### 授权路由 324 | 325 | ```php 326 | class BroadcastManager implements FactoryContract 327 | { 328 | public function routes(array $attributes = null) 329 | { 330 | if ($this->app->routesAreCached()) { 331 | return; 332 | } 333 | 334 | $attributes = $attributes ?: ['middleware' => ['web']]; 335 | 336 | $this->app['router']->group($attributes, function ($router) { 337 | $router->post('/broadcasting/auth', BroadcastController::class.'@authenticate'); 338 | }); 339 | } 340 | } 341 | ``` 342 | 频道专门有 `Controller` 来处理授权服务: 343 | 344 | ```php 345 | class BroadcastController extends Controller 346 | { 347 | public function authenticate(Request $request) 348 | { 349 | return Broadcast::auth($request); 350 | } 351 | } 352 | 353 | ``` 354 | 当 `Socket Io` 服务器对 `javascript` 程序推送数据的时候,首先会经过该 `controller` 进行授权验证: 355 | 356 | ```php 357 | public function auth($request) 358 | { 359 | if (Str::startsWith($request->channel_name, ['private-', 'presence-']) && 360 | ! $request->user()) { 361 | throw new HttpException(403); 362 | } 363 | 364 | $channelName = Str::startsWith($request->channel_name, 'private-') 365 | ? Str::replaceFirst('private-', '', $request->channel_name) 366 | : Str::replaceFirst('presence-', '', $request->channel_name); 367 | 368 | return parent::verifyUserCanAccessChannel( 369 | $request, $channelName 370 | ); 371 | } 372 | ``` 373 | 374 | `verifyUserCanAccessChannel` 根据频道与其绑定的闭包函数来验证该频道是否可以通过授权: 375 | 376 | ```php 377 | protected function verifyUserCanAccessChannel($request, $channel) 378 | { 379 | foreach ($this->channels as $pattern => $callback) { 380 | if (! Str::is(preg_replace('/\{(.*?)\}/', '*', $pattern), $channel)) { 381 | continue; 382 | } 383 | 384 | $parameters = $this->extractAuthParameters($pattern, $channel, $callback); 385 | 386 | if ($result = $callback($request->user(), ...$parameters)) { 387 | return $this->validAuthenticationResponse($request, $result); 388 | } 389 | } 390 | 391 | throw new HttpException(403); 392 | } 393 | ``` 394 | 395 | 由于频道的命名经常带有 `userid` 等参数,因此判断频道之前首先要把 `channels` 中的频道名转为通配符 `*`,例如 `order.{userid}` 转为 order.*,之后进行正则匹配。 396 | 397 | `extractAuthParameters` 用于提取频道的闭包函数的参数,合并 `$request->user()` 之后调用闭包函数。 398 | 399 | ```php 400 | protected function extractAuthParameters($pattern, $channel, $callback) 401 | { 402 | $callbackParameters = (new ReflectionFunction($callback))->getParameters(); 403 | 404 | return collect($this->extractChannelKeys($pattern, $channel))->reject(function ($value, $key) { 405 | return is_numeric($key); 406 | })->map(function ($value, $key) use ($callbackParameters) { 407 | return $this->resolveBinding($key, $value, $callbackParameters); 408 | })->values()->all(); 409 | } 410 | 411 | protected function extractChannelKeys($pattern, $channel) 412 | { 413 | preg_match('/^'.preg_replace('/\{(.*?)\}/', '(?<$1>[^\.]+)', $pattern).'/', $channel, $keys); 414 | 415 | return $keys; 416 | } 417 | 418 | public function validAuthenticationResponse($request, $result) 419 | { 420 | if (is_bool($result)) { 421 | return json_encode($result); 422 | } 423 | 424 | return json_encode(['channel_data' => [ 425 | 'user_id' => $request->user()->getKey(), 426 | 'user_info' => $result, 427 | ]]); 428 | } 429 | ``` 430 | 431 | `extractChannelKeys` 用于将 `order.{userid}` 与 `order.23` 中 `userid` 和 `23` 建立 `key`、`value` 关联。如果 `userid` 是 `User` 的主键,`resolveBinding` 还可以为其自动进行路由模型绑定。 -------------------------------------------------------------------------------- /Laravel Config—— 配置文件的加载与源码解析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 本文主要介绍 `laravel` 加载 `config` 配置文件的相关源码。 4 | 5 | # config 配置文件的加载 6 | 7 | config 配置文件由类 `\Illuminate\Foundation\Bootstrap\LoadConfiguration::class` 完成: 8 | 9 | ```php 10 | class LoadConfiguration 11 | { 12 | public function bootstrap(Application $app) 13 | { 14 | $items = []; 15 | 16 | if (file_exists($cached = $app->getCachedConfigPath())) { 17 | $items = require $cached; 18 | 19 | $loadedFromCache = true; 20 | } 21 | 22 | $app->instance('config', $config = new Repository($items)); 23 | 24 | if (! isset($loadedFromCache)) { 25 | $this->loadConfigurationFiles($app, $config); 26 | } 27 | 28 | $app->detectEnvironment(function () use ($config) { 29 | return $config->get('app.env', 'production'); 30 | }); 31 | 32 | date_default_timezone_set($config->get('app.timezone', 'UTC')); 33 | 34 | mb_internal_encoding('UTF-8'); 35 | } 36 | } 37 | ``` 38 | 39 | 可以看到,配置文件的加载步骤: 40 | 41 | - 加载缓存 42 | - 若缓存不存在,则利用函数 `loadConfigurationFiles` 加载配置文件 43 | - 加载环境变量、时间区、编码方式 44 | 45 | 函数 `loadConfigurationFiles` 用于加载配置文件: 46 | 47 | ```php 48 | protected function loadConfigurationFiles(Application $app, RepositoryContract $repository) 49 | { 50 | foreach ($this->getConfigurationFiles($app) as $key => $path) { 51 | $repository->set($key, require $path); 52 | } 53 | } 54 | ``` 55 | 56 | 加载配置文件有两部分:搜索配置文件、加载配置文件的数组变量值 57 | 58 | ## 搜索配置文件 59 | 60 | `getConfigurationFiles` 可以根据配置文件目录搜索所有的 `php` 为后缀的文件,并将其转化为 `files` 数组,其 `key` 为目录名以字符 `.` 为连接的字符串 ,`value` 为文件真实路径: 61 | 62 | ```php 63 | protected function getConfigurationFiles(Application $app) 64 | { 65 | $files = []; 66 | 67 | $configPath = realpath($app->configPath()); 68 | 69 | foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) { 70 | $directory = $this->getNestedDirectory($file, $configPath); 71 | 72 | $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath(); 73 | } 74 | 75 | return $files; 76 | } 77 | 78 | protected function getNestedDirectory(SplFileInfo $file, $configPath) 79 | { 80 | $directory = $file->getPath(); 81 | 82 | if ($nested = trim(str_replace($configPath, '', $directory), DIRECTORY_SEPARATOR)) { 83 | $nested = str_replace(DIRECTORY_SEPARATOR, '.', $nested).'.'; 84 | } 85 | 86 | return $nested; 87 | } 88 | ``` 89 | 90 | ## 加载配置文件数组 91 | 92 | 加载配置文件由类 `Illuminate\Config\Repository\LoadConfiguration` 完成: 93 | 94 | ```php 95 | class Repository 96 | { 97 | public function set($key, $value = null) 98 | { 99 | $keys = is_array($key) ? $key : [$key => $value]; 100 | 101 | foreach ($keys as $key => $value) { 102 | Arr::set($this->items, $key, $value); 103 | } 104 | } 105 | } 106 | ``` 107 | 加载配置文件时间上就是将所有配置文件的数值放入一个巨大的多维数组中,这一部分由类 `Illuminate\Support\Arr` 完成: 108 | 109 | ```php 110 | class Arr 111 | { 112 | public static function set(&$array, $key, $value) 113 | { 114 | if (is_null($key)) { 115 | return $array = $value; 116 | } 117 | 118 | $keys = explode('.', $key); 119 | 120 | while (count($keys) > 1) { 121 | $key = array_shift($keys); 122 | 123 | if (! isset($array[$key]) || ! is_array($array[$key])) { 124 | $array[$key] = []; 125 | } 126 | 127 | $array = &$array[$key]; 128 | } 129 | 130 | $array[array_shift($keys)] = $value; 131 | 132 | return $array; 133 | } 134 | } 135 | ``` 136 | 137 | 例如 `dir1.dir2.app` ,配置文件会生成 `$array[dir1][dir2][app]` 这样的数组。 138 | 139 | # 配置文件数值的获取 140 | 141 | 当我们利用全局函数 `config` 来获取配置值的时候: 142 | 143 | ```php 144 | function config($key = null, $default = null) 145 | { 146 | if (is_null($key)) { 147 | return app('config'); 148 | } 149 | 150 | if (is_array($key)) { 151 | return app('config')->set($key); 152 | } 153 | 154 | return app('config')->get($key, $default); 155 | } 156 | ``` 157 | 158 | 配置文件的获取和加载类似,都是将字符串转为多维数组,然后获取具体数组值: 159 | 160 | ```php 161 | public static function get($array, $key, $default = null) 162 | { 163 | if (! static::accessible($array)) { 164 | return value($default); 165 | } 166 | 167 | if (is_null($key)) { 168 | return $array; 169 | } 170 | 171 | if (static::exists($array, $key)) { 172 | return $array[$key]; 173 | } 174 | 175 | foreach (explode('.', $key) as $segment) { 176 | if (static::accessible($array) && static::exists($array, $segment)) { 177 | $array = $array[$segment]; 178 | } else { 179 | return value($default); 180 | } 181 | } 182 | 183 | return $array; 184 | } 185 | ``` -------------------------------------------------------------------------------- /Laravel Container——IoC 服务容器源码解析(服务器绑定).md: -------------------------------------------------------------------------------- 1 | # 服务容器的绑定 2 | ## bind 绑定 3 | 欢迎关注我的博客:[www.leoyang90.cn](http://www.leoyang90.cn) 4 | 5 | bind 绑定是服务容器最常用的绑定方式,在 [上一篇](http://leoyang90.cn/2017/05/06/Laravel-container/)文章中我们讨论过,bind 的绑定有三种: 6 | > - 绑定自身 7 | > - 绑定闭包 8 | > - 绑定接口 9 | 10 | 今天,我们这篇文章主要从源码上讲解 Ioc 服务容器是如何进行绑定的。 11 | ```php 12 | /** 13 | * Register a binding with the container. 14 | * 15 | * @param string|array $abstract 16 | * @param \Closure|string|null $concrete 17 | * @param bool $shared 18 | * @return void 19 | */ 20 | public function bind($abstract, $concrete = null, $shared = false) 21 | { 22 | // If no concrete type was given, we will simply set the concrete type to the 23 | // abstract type. After that, the concrete type to be registered as shared 24 | // without being forced to state their classes in both of the parameters. 25 | $this->dropStaleInstances($abstract); 26 | 27 | if (is_null($concrete)) { 28 | $concrete = $abstract; 29 | } 30 | 31 | // If the factory is not a Closure, it means it is just a class name which is 32 | // bound into this container to the abstract type and we will just wrap it 33 | // up inside its own Closure to give us more convenience when extending. 34 | if (! $concrete instanceof Closure) { 35 | $concrete = $this->getClosure($abstract, $concrete); 36 | } 37 | 38 | $this->bindings[$abstract] = compact('concrete', 'shared'); 39 | 40 | // If the abstract type was already resolved in this container we'll fire the 41 | // rebound listener so that any objects which have already gotten resolved 42 | // can have their copy of the object updated via the listener callbacks. 43 | if ($this->resolved($abstract)) { 44 | $this->rebound($abstract); 45 | } 46 | } 47 | ``` 48 | 从源码中我们可以看出,服务器的绑定有如下几个步骤: 49 | > 1. 去除原有注册。去除当前绑定接口的原有实现单例对象,和原有的别名,为实现绑定新的实现做准备。 50 | > 2. 加装闭包。如果实现类不是闭包(绑定自身或者绑定接口),那么就创建闭包,以实现 lazy 加载。 51 | > 3. 注册。将闭包函数和单例变量存入 bindings 数组中,以备解析时使用。 52 | > 4. 回调。如果绑定的接口已经被解析过了,将会调用回调函数,对已经解析过的对象进行调整。 53 | 54 | ### 去除原有注册 55 | dropStaleInstances 用于去除当前接口原有的注册和别名,这里负责清除绑定的 aliases 和单例对象的 instances,bindings 后面再做修改: 56 | ```php 57 | protected function dropStaleInstances($abstract) 58 | { 59 | unset($this->instances[$abstract], $this->aliases[$abstract]); 60 | } 61 | ``` 62 | ### 加装闭包 63 | getClosure 的作用是为注册的非闭包实现外加闭包,这样做有两个作用: 64 | 65 | - 延时加载 66 | 67 | 服务容器在 getClosure 中为每个绑定的类都包一层闭包,这样服务容器就只有进行解析的时候闭包才会真正进行运行,实现了 lazy 加载的功能。 68 | - 递归绑定 69 | 70 | 对于服务容器来说,绑定是可以递归的,例如: 71 | ```php 72 | $app->bind(A::class,B::class); 73 | $app->bind(B::class,C::class); 74 | $app->bind(C::class,function(){ 75 | return new C; 76 | }) 77 | ``` 78 | 对于 A 类,我们直接解析 A 可以得到 B 类,但是如果仅仅到此为止,服务容器直接去用反射去创建 B 类的话,那么就很有可能创建失败,因为 B 类很有可能也是接口,B 接口绑定了其他实现类,要知道接口是无法实例化的。 79 | 80 | 因此服务容器需要递归地对 A 进行解析,这个就是 getClosure 的作用,它把所有可能会递归的绑定在闭包中都用 make 函数,这样解析 make(A::class) 的时候得到闭包 make(B::class),make(B::class) 的时候会得到闭包 make(C::class),make(C::class) 终于可以得到真正的实现了。 81 | 82 | 对于自我绑定的情况,因为不存在递归情况,所以在闭包中会使用 build 函数直接创建对象。(如果仍然使用 make,那就无限循环了) 83 | 84 | ```php 85 | protected function getClosure($abstract, $concrete) 86 | { 87 | return function ($container, $parameters = []) use ($abstract, $concrete) { 88 | if ($abstract == $concrete) { 89 | return $container->build($concrete); 90 | } 91 | return $container->makeWith($concrete, $parameters); 92 | }; 93 | } 94 | ``` 95 | ### 注册 96 | 注册就是向 binding 数组中添加注册的接口与它的实现,其中 compact() 函数创建包含变量名和它们的值的数组,创建后的结果为: 97 | ```php 98 | $bindings[$abstract] = [ 99 | 'concrete' => $concrete, 100 | 'shared' => $shared 101 | ] 102 | ``` 103 | ### 回调 104 | 注册之后,还要查看当前注册的接口是否已经被实例化,如果已经被服务容器实例化过,那么就要调用回调函数。(若存在回调函数) 105 | resolved() 函数用于判断当前接口是否曾被解析过,在判断之前,先获取了接口的最终服务名: 106 | ```php 107 | public function resolved($abstract) 108 | { 109 | if ($this->isAlias($abstract)) { 110 | $abstract = $this->getAlias($abstract); 111 | } 112 | 113 | return isset($this->resolved[$abstract]) || 114 | isset($this->instances[$abstract]); 115 | } 116 | 117 | public function isAlias($name) 118 | { 119 | return isset($this->aliases[$name]); 120 | } 121 | ``` 122 | getAlias() 函数利用递归的方法获取别名的最终服务名称: 123 | 124 | ```php 125 | public function getAlias($abstract) 126 | { 127 | if (! isset($this->aliases[$abstract])) { 128 | return $abstract; 129 | } 130 | 131 | if ($this->aliases[$abstract] === $abstract) { 132 | throw new LogicException("[{$abstract}] is aliased to itself."); 133 | } 134 | 135 | return $this->getAlias($this->aliases[$abstract]); 136 | } 137 | ``` 138 | 如果当前接口已经被解析过了,那么就要运行回调函数: 139 | ```php 140 | protected function rebound($abstract) 141 | { 142 | $instance = $this->make($abstract); 143 | 144 | foreach ($this->getReboundCallbacks($abstract) as $callback) { 145 | call_user_func($callback, $this, $instance); 146 | } 147 | } 148 | 149 | protected function getReboundCallbacks($abstract) 150 | { 151 | if (isset($this->reboundCallbacks[$abstract])) { 152 | return $this->reboundCallbacks[$abstract]; 153 | } 154 | 155 | return []; 156 | } 157 | ``` 158 | 这里面的 reboundCallbacks 从哪里来呢?这就是 [Laravel核心——Ioc服务容器](http://www.leoyang90.cn/2017/05/06/Laravel-container/) 文章中提到的 rebinding 159 | ```php 160 | public function rebinding($abstract, Closure $callback) 161 | { 162 | $this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback; 163 | 164 | if ($this->bound($abstract)) { 165 | return $this->make($abstract); 166 | } 167 | } 168 | ``` 169 | 值得注意的是: rebinding 函数不仅绑定了回调函数,同时顺带还对接口abstract进行了解析,因为只有解析过,下次注册才会调用回调函数。 170 | ## singleton 绑定 171 | singleton 绑定仅仅是 bind 绑定的一个 shared 为真的形式: 172 | ```php 173 | public function singleton($abstract, $concrete = null) 174 | { 175 | $this->bind($abstract, $concrete, true); 176 | } 177 | ``` 178 | ## instance 绑定 179 | 不对接口进行解析,直接给接口一个实例作为单例对象。从下面可以看出,主要的工作就是去除接口在abstractAliases 数组和 aliases 数组中的痕迹,防止 make 函数根据别名继续解析下去出现错误。如果当前接口曾经注册过,那么就调用回调函数。 180 | ```php 181 | public function instance($abstract, $instance) 182 | { 183 | $this->removeAbstractAlias($abstract); 184 | 185 | $isBound = $this->bound($abstract); 186 | 187 | unset($this->aliases[$abstract]); 188 | 189 | $this->instances[$abstract] = $instance; 190 | 191 | if ($isBound) { 192 | $this->rebound($abstract); 193 | } 194 | } 195 | 196 | protected function removeAbstractAlias($searched) 197 | { 198 | if (! isset($this->aliases[$searched])) { 199 | return; 200 | } 201 | 202 | foreach ($this->abstractAliases as $abstract => $aliases) { 203 | foreach ($aliases as $index => $alias) { 204 | if ($alias == $searched) { 205 | unset($this->abstractAliases[$abstract][$index]); 206 | } 207 | } 208 | } 209 | } 210 | 211 | public function bound($abstract) 212 | { 213 | return isset($this->bindings[$abstract]) || 214 | isset($this->instances[$abstract]) || 215 | $this->isAlias($abstract); 216 | } 217 | ``` 218 | ## Context 绑定 219 | Context 绑定一般用于依赖注入,当我们利用依赖注入来自动实例化对象时,服务容器其实是利用反射机制来为构造函数实例化它的参数,这个过程中,被实例化的对象就是下面的 concrete,构造函数的参数接口是 abstract,参数接口实际的实现是 implementation。 220 | 例如: 221 | ```php 222 | $this->app->when(PhotoController::class) 223 | ->needs(Filesystem::class) 224 | ->give(function () { 225 | return Storage::disk('local'); 226 | }); 227 | ``` 228 | 这里实例化对象 concrete 就是 PhotoController,构造函数的参数接口 abstract 就是 Filesystem。参数接口实际实现 implementation 是 Storage::disk('local')。 229 | 230 | 这样,每次进行解析构造函数的参数接口的时候,都会去判断当前的 contextual 数组里面 concrete[concrete] [abstract](也就是 concrete[PhotoController::class] [Filesystem::class])对应的上下文绑定,如果有就直接从数组中取出来,如果没有就按照正常方式解析。 231 | 值得注意的是,concrete 和 abstract 都是利用 getAlias 函数,保证最后拿到的不是别名。 232 | ```php 233 | public function when($concrete) 234 | { 235 | return new ContextualBindingBuilder($this, $this->getAlias($concrete)); 236 | } 237 | public function __construct(Container $container, $concrete) 238 | { 239 | $this->concrete = $concrete; 240 | $this->container = $container; 241 | } 242 | public function needs($abstract) 243 | { 244 | $this->needs = $abstract; 245 | 246 | return $this; 247 | } 248 | public function give($implementation) 249 | { 250 | $this->container->addContextualBinding( 251 | $this->concrete, $this->needs, $implementation 252 | ); 253 | } 254 | public function addContextualBinding($concrete, $abstract, $implementation) 255 | { 256 | $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation; 257 | } 258 | ``` 259 | ## tag 绑定 260 | 标签绑定比较简单,绑定过程就是将标签和接口之间建立一个对应数组,在解析的过程中,按照标签把所有接口都解析一遍即可。 261 | ```php 262 | public function tag($abstracts, $tags) 263 | { 264 | $tags = is_array($tags) ? $tags : array_slice(func_get_args(), 1); 265 | 266 | foreach ($tags as $tag) { 267 | if (! isset($this->tags[$tag])) { 268 | $this->tags[$tag] = []; 269 | } 270 | 271 | foreach ((array) $abstracts as $abstract) { 272 | $this->tags[$tag][] = $abstract; 273 | } 274 | } 275 | } 276 | ``` 277 | ## 数组绑定 278 | 利用数组进行绑定的时候 ($app()[A::class] = B::class),服务容器会调用 offsetSet 函数: 279 | ```php 280 | public function offsetSet($key, $value) 281 | { 282 | $this->bind($key, $value instanceof Closure ? $value : function () use ($value) { 283 | return $value; 284 | }); 285 | } 286 | ``` 287 | # extend扩展 288 | extend 扩展分为两种,一种是针对instance注册的对象,这种情况将立即起作用,并更新之前实例化的对象;另一种情况是非 instance 注册的对象,那么闭包函数将会被放入 extenders 数组中,将在下一次实例化对象的时候才起作用: 289 | ```php 290 | public function extend($abstract, Closure $closure) 291 | { 292 | $abstract = $this->getAlias($abstract); 293 | 294 | if (isset($this->instances[$abstract])) { 295 | $this->instances[$abstract] = $closure($this->instances[$abstract], $this); 296 | 297 | $this->rebound($abstract); 298 | } else { 299 | $this->extenders[$abstract][] = $closure; 300 | 301 | if ($this->resolved()) { 302 | $this->rebound($abstract); 303 | } 304 | } 305 | } 306 | ``` 307 | # 服务器事件 308 | 服务器的事件注册依靠 resolving 函数和 afterResolving 函数,这两个函数维护着 globalResolvingCallbacks、resolvingCallbacks、globalAfterResolvingCallbacks、afterResolvingCallbacks 数组,这些数组中存放着事件的回调闭包函数,每当对对象进行解析时就会遍历这些数组,触发事件: 309 | ```php 310 | public function resolving($abstract, Closure $callback = null) 311 | { 312 | if (is_string($abstract)) { 313 | $abstract = $this->getAlias($abstract); 314 | } 315 | 316 | if (is_null($callback) && $abstract instanceof Closure) { 317 | $this->globalResolvingCallbacks[] = $abstract; 318 | } else { 319 | $this->resolvingCallbacks[$abstract][] = $callback; 320 | } 321 | } 322 | 323 | public function afterResolving($abstract, Closure $callback = null) 324 | { 325 | if (is_string($abstract)) { 326 | $abstract = $this->getAlias($abstract); 327 | } 328 | 329 | if ($abstract instanceof Closure && is_null($callback)) { 330 | $this->globalAfterResolvingCallbacks[] = $abstract; 331 | } else { 332 | $this->afterResolvingCallbacks[$abstract][] = $callback; 333 | } 334 | } 335 | ``` 336 | > Written with [StackEdit](https://stackedit.io/). -------------------------------------------------------------------------------- /Laravel Database——数据库的 CRUD 操作.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 当 `connection` 对象构建初始化完成后,我们就可以利用 `DB` 来进行数据库的 `CRUD` ( `Create`、`Retrieve`、`Update`、`Delete`)操作。本篇文章,我们将会讲述 `laravel` 如何与 `pdo` 交互,实现基本数据库服务的原理。 3 | 4 | ## run 5 | 6 | `laravel` 中任何数据库的操作都要经过 `run` 这个函数,这个函数作用在于重新连接数据库、记录数据库日志、数据库异常处理: 7 | 8 | ```php 9 | protected function run($query, $bindings, Closure $callback) 10 | { 11 | $this->reconnectIfMissingConnection(); 12 | 13 | $start = microtime(true); 14 | 15 | try { 16 | $result = $this->runQueryCallback($query, $bindings, $callback); 17 | } catch (QueryException $e) { 18 | $result = $this->handleQueryException( 19 | $e, $query, $bindings, $callback 20 | ); 21 | } 22 | 23 | $this->logQuery( 24 | $query, $bindings, $this->getElapsedTime($start) 25 | ); 26 | 27 | return $result; 28 | } 29 | ``` 30 | 31 | ### 重新连接数据库 reconnect 32 | 33 | 如果当期的 `pdo` 是空,那么就会调用 `reconnector` 重新与数据库进行连接: 34 | 35 | ```php 36 | protected function reconnectIfMissingConnection() 37 | { 38 | if (is_null($this->pdo)) { 39 | $this->reconnect(); 40 | } 41 | } 42 | 43 | public function reconnect() 44 | { 45 | if (is_callable($this->reconnector)) { 46 | return call_user_func($this->reconnector, $this); 47 | } 48 | 49 | throw new LogicException('Lost connection and no reconnector available.'); 50 | } 51 | 52 | ``` 53 | 54 | ### 运行数据库操作 55 | 56 | 数据库的 curd 操作会被包装成为一个闭包函数,作为 `runQueryCallback` 的一个参数,当运行正常时,会返回结果,如果遇到异常的话,会将异常转化为 `QueryException`,并且抛出。 57 | 58 | ```php 59 | protected function runQueryCallback($query, $bindings, Closure $callback) 60 | { 61 | try { 62 | $result = $callback($query, $bindings); 63 | } 64 | 65 | catch (Exception $e) { 66 | throw new QueryException( 67 | $query, $this->prepareBindings($bindings), $e 68 | ); 69 | } 70 | 71 | return $result; 72 | } 73 | 74 | ``` 75 | 76 | ### 数据库异常处理 77 | 78 | 当 `pdo` 查询返回异常的时候,如果当前是事务进行时,那么直接返回异常,让上一层事务来处理。 79 | 80 | 如果是由于与数据库事情连接导致的异常,那么就要重新与数据库进行连接: 81 | 82 | ```php 83 | protected function handleQueryException($e, $query, $bindings, Closure $callback) 84 | { 85 | if ($this->transactions >= 1) { 86 | throw $e; 87 | } 88 | 89 | return $this->tryAgainIfCausedByLostConnection( 90 | $e, $query, $bindings, $callback 91 | ); 92 | } 93 | 94 | ``` 95 | 96 | 与数据库失去连接: 97 | 98 | ```php 99 | protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback) 100 | { 101 | if ($this->causedByLostConnection($e->getPrevious())) { 102 | $this->reconnect(); 103 | 104 | return $this->runQueryCallback($query, $bindings, $callback); 105 | } 106 | 107 | throw $e; 108 | } 109 | 110 | protected function causedByLostConnection(Exception $e) 111 | { 112 | $message = $e->getMessage(); 113 | 114 | return Str::contains($message, [ 115 | 'server has gone away', 116 | 'no connection to the server', 117 | 'Lost connection', 118 | 'is dead or not enabled', 119 | 'Error while sending', 120 | 'decryption failed or bad record mac', 121 | 'server closed the connection unexpectedly', 122 | 'SSL connection has been closed unexpectedly', 123 | 'Error writing data to the connection', 124 | 'Resource deadlock avoided', 125 | 'Transaction() on null', 126 | 'child connection forced to terminate due to client_idle_limit', 127 | ]); 128 | } 129 | ``` 130 | 131 | ### 数据库日志 132 | 133 | ```php 134 | public function logQuery($query, $bindings, $time = null) 135 | { 136 | $this->event(new QueryExecuted($query, $bindings, $time, $this)); 137 | 138 | if ($this->loggingQueries) { 139 | $this->queryLog[] = compact('query', 'bindings', 'time'); 140 | } 141 | } 142 | 143 | ``` 144 | 想要开启或关闭日志功能: 145 | 146 | ```php 147 | public function enableQueryLog() 148 | { 149 | $this->loggingQueries = true; 150 | } 151 | 152 | public function disableQueryLog() 153 | { 154 | $this->loggingQueries = false; 155 | } 156 | 157 | ``` 158 | 159 | ## Select 查询 160 | 161 | ```php 162 | public function select($query, $bindings = [], $useReadPdo = true) 163 | { 164 | return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) { 165 | if ($this->pretending()) { 166 | return []; 167 | } 168 | 169 | $statement = $this->prepared($this->getPdoForSelect($useReadPdo) 170 | ->prepare($query)); 171 | 172 | $this->bindValues($statement, $this->prepareBindings($bindings)); 173 | 174 | $statement->execute(); 175 | 176 | return $statement->fetchAll(); 177 | }); 178 | } 179 | ``` 180 | 181 | 数据库的查询主要有一下几个步骤: 182 | 183 | - 获取 `$this->pdo` 成员变量,若当前未连接数据库,则进行数据库连接,获取 `pdo` 对象。 184 | - 设置 `pdo` 数据 `fetch` 模式 185 | - `pdo` 进行 `sql` 语句预处理,`pdo` 绑定参数 186 | - `sql` 语句执行,并获取数据。 187 | 188 | ### getPdoForSelect 获取 pdo 对象 189 | 190 | ```php 191 | protected function getPdoForSelect($useReadPdo = true) 192 | { 193 | return $useReadPdo ? $this->getReadPdo() : $this->getPdo(); 194 | } 195 | 196 | public function getPdo() 197 | { 198 | if ($this->pdo instanceof Closure) { 199 | return $this->pdo = call_user_func($this->pdo); 200 | } 201 | 202 | return $this->pdo; 203 | } 204 | 205 | public function getReadPdo() 206 | { 207 | if ($this->transactions > 0) { 208 | return $this->getPdo(); 209 | } 210 | 211 | if ($this->getConfig('sticky') && $this->recordsModified) { 212 | return $this->getPdo(); 213 | } 214 | 215 | if ($this->readPdo instanceof Closure) { 216 | return $this->readPdo = call_user_func($this->readPdo); 217 | } 218 | 219 | return $this->readPdo ?: $this->getPdo(); 220 | } 221 | ``` 222 | 223 | `getPdo` 这里逻辑比较简单,值得我们注意的是 `getReadPdo`。为了减缓数据库的压力,我们常常对数据库进行读写分离,也就是只要当写数据库这种操作发生时,才会使用写数据库,否则都会用读数据库。这种措施减少了数据库的压力,但是也带来了一些问题,那就是读写两个数据库在一定时间内会出现数据不一致的情况,原因就是写库的数据未能及时推送给读库,造成读库数据延迟的现象。为了在一定程度上解决这类问题,`laravel` 增添了 `sticky` 选项, 224 | 225 | 从程序中我们可以看出,当我们设置选项 `sticky` 为真,并且的确对数据库进行了写操作后,`getReadPdo` 会强制返回主库的连接,这样就避免了读写分离造成的延迟问题。 226 | 227 | 还有一种情况,当数据库在执行事务期间,所有的读取操作也会被强制连接主库。 228 | 229 | ### prepared 设置数据获取方式 230 | 231 | ```php 232 | protected $fetchMode = PDO::FETCH_OBJ; 233 | protected function prepared(PDOStatement $statement) 234 | { 235 | $statement->setFetchMode($this->fetchMode); 236 | 237 | $this->event(new Events\StatementPrepared( 238 | $this, $statement 239 | )); 240 | 241 | return $statement; 242 | } 243 | ``` 244 | `pdo` 的 `setFetchMode` 函数用于为语句设置默认的获取模式,通常模式有一下几种: 245 | 246 | - PDO::FETCH_ASSOC //从结果集中获取以列名为索引的关联数组。 247 | - PDO::FETCH_NUM //从结果集中获取一个以列在行中的数值偏移量为索引的值数组。 248 | - PDO::FETCH_BOTH //这是默认值,包含上面两种数组。 249 | - PDO::FETCH_OBJ //从结果集当前行的记录中获取其属性对应各个列名的一个对象。 250 | - PDO::FETCH_BOUND //使用fetch()返回TRUE,并将获取的列值赋给在bindParm()方法中指定的相应变量。 251 | - PDO::FETCH_LAZY //创建关联数组和索引数组,以及包含列属性的一个对象,从而可以在这三种接口中任选一种。 252 | 253 | ### pdo 的 prepare 函数 254 | 255 | `prepare` 函数会为 `PDOStatement::execute()` 方法准备要执行的 `SQL` 语句,`SQL` 语句可以包含零个或多个命名(`:name`)或问号(`?`)参数标记,参数在SQL执行时会被替换。 256 | 257 | 不能在 `SQL` 语句中同时包含命名(`:name`)或问号(`?`)参数标记,只能选择其中一种风格。 258 | 259 | 预处理 `SQL` 语句中的参数在使用 `PDOStatement::execute()` 方法时会传递真实的参数。 260 | 261 | 之所以使用 `prepare` 函数,是因为这个函数可以防止 `SQL` 注入,并且可以加快同一查询语句的速度。关于预处理与参数绑定防止 `SQL` 漏洞注入的原理可以参考:[Web安全之SQL注入攻击技巧与防范](http://www.cnblogs.com/csniper/p/5802202.html). 262 | 263 | ### pdo 的 bindValues 函数 264 | 265 | 在调用 `pdo` 的参数绑定函数之前,`laravel` 对参数值进一步进行了优化,把时间类型的对象利用 `grammer` 的设置重新格式化,`false` 也改为0。 266 | 267 | `pdo` 的参数绑定函数 `bindValue`,对于使用命名占位符的预处理语句,应是类似 :name 形式的参数名。对于使用问号占位符的预处理语句,应是以1开始索引的参数位置。 268 | 269 | ```php 270 | public function prepareBindings(array $bindings) 271 | { 272 | $grammar = $this->getQueryGrammar(); 273 | 274 | foreach ($bindings as $key => $value) { 275 | if ($value instanceof DateTimeInterface) { 276 | $bindings[$key] = $value->format($grammar->getDateFormat()); 277 | } elseif ($value === false) { 278 | $bindings[$key] = 0; 279 | } 280 | } 281 | 282 | return $bindings; 283 | } 284 | 285 | public function bindValues($statement, $bindings) 286 | { 287 | foreach ($bindings as $key => $value) { 288 | $statement->bindValue( 289 | is_string($key) ? $key : $key + 1, $value, 290 | is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR 291 | ); 292 | } 293 | } 294 | ``` 295 | 296 | ## insert 297 | 298 | ```php 299 | public function insert($query, $bindings = []) 300 | { 301 | return $this->statement($query, $bindings); 302 | } 303 | 304 | public function statement($query, $bindings = []) 305 | { 306 | return $this->run($query, $bindings, function ($query, $bindings) { 307 | if ($this->pretending()) { 308 | return true; 309 | } 310 | 311 | $statement = $this->getPdo()->prepare($query); 312 | 313 | $this->bindValues($statement, $this->prepareBindings($bindings)); 314 | 315 | $this->recordsHaveBeenModified(); 316 | 317 | return $statement->execute(); 318 | }); 319 | } 320 | 321 | ``` 322 | 这部分的代码与 `select` 非常相似,不同之处有一下几个: 323 | 324 | - 直接获取写库的连接,不会考虑读库 325 | - 由于不需要返回任何数据库数据,因此也不必设置 `fetchMode`。 326 | - `recordsHaveBeenModified` 函数标志当前连接数据库已被写入。 327 | - 不需要调用函数 `fetchAll` 328 | 329 | ```php 330 | public function recordsHaveBeenModified($value = true) 331 | { 332 | if (! $this->recordsModified) { 333 | $this->recordsModified = $value; 334 | } 335 | } 336 | 337 | ``` 338 | 339 | ## update、delete 340 | 341 | `affectingStatement` 这个函数与上面的 `statement` 函数一致,只是最后会返回更新、删除影响的行数。 342 | 343 | ```php 344 | public function update($query, $bindings = []) 345 | { 346 | return $this->affectingStatement($query, $bindings); 347 | } 348 | 349 | public function delete($query, $bindings = []) 350 | { 351 | return $this->affectingStatement($query, $bindings); 352 | } 353 | 354 | public function affectingStatement($query, $bindings = []) 355 | { 356 | return $this->run($query, $bindings, function ($query, $bindings) { 357 | if ($this->pretending()) { 358 | return 0; 359 | } 360 | 361 | $statement = $this->getPdo()->prepare($query); 362 | 363 | $this->bindValues($statement, $this->prepareBindings($bindings)); 364 | 365 | $statement->execute(); 366 | 367 | $this->recordsHaveBeenModified( 368 | ($count = $statement->rowCount()) > 0 369 | ); 370 | 371 | return $count; 372 | }); 373 | } 374 | ``` 375 | 376 | ## transaction 数据库事务 377 | 378 | 为保持数据的一致性,对于重要的数据我们经常使用数据库事务,`transaction` 函数接受一个闭包函数,与一个重复尝试的次数: 379 | 380 | ```php 381 | public function transaction(Closure $callback, $attempts = 1) 382 | { 383 | for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) { 384 | $this->beginTransaction(); 385 | 386 | try { 387 | return tap($callback($this), function ($result) { 388 | $this->commit(); 389 | }); 390 | } 391 | 392 | catch (Exception $e) { 393 | $this->handleTransactionException( 394 | $e, $currentAttempt, $attempts 395 | ); 396 | } catch (Throwable $e) { 397 | $this->rollBack(); 398 | 399 | throw $e; 400 | } 401 | } 402 | } 403 | 404 | ``` 405 | 406 | ### 开始事务 407 | 408 | 数据库事务中非常重要的成员变量是 `$this->transactions`,它标志着当前事务的进程: 409 | 410 | ```php 411 | public function beginTransaction() 412 | { 413 | $this->createTransaction(); 414 | 415 | ++$this->transactions; 416 | 417 | $this->fireConnectionEvent('beganTransaction'); 418 | } 419 | 420 | ``` 421 | 可以看出,当创建事务成功后,就会累加 `$this->transactions`,并且启动 `event`,创建事务: 422 | 423 | ```php 424 | protected function createTransaction() 425 | { 426 | if ($this->transactions == 0) { 427 | try { 428 | $this->getPdo()->beginTransaction(); 429 | } catch (Exception $e) { 430 | $this->handleBeginTransactionException($e); 431 | } 432 | } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) { 433 | $this->createSavepoint(); 434 | } 435 | } 436 | ``` 437 | 如果当前没有任何事务,那么就会调用 `pdo` 来开启事务。 438 | 439 | 如果当前已经在事务保护的范围内,那么就会创建 `SAVEPOINT`,实现数据库嵌套事务: 440 | 441 | ```php 442 | protected function createSavepoint() 443 | { 444 | $this->getPdo()->exec( 445 | $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1)) 446 | ); 447 | } 448 | 449 | public function compileSavepoint($name) 450 | { 451 | return 'SAVEPOINT '.$name; 452 | } 453 | ``` 454 | 455 | 如果创建事务失败,那么就会调用 `handleBeginTransactionException`: 456 | 457 | ```php 458 | protected function handleBeginTransactionException($e) 459 | { 460 | if ($this->causedByLostConnection($e)) { 461 | $this->reconnect(); 462 | 463 | $this->pdo->beginTransaction(); 464 | } else { 465 | throw $e; 466 | } 467 | } 468 | ``` 469 | 470 | 如果创建事务失败是由于与数据库失去连接的话,那么就会重新连接数据库,否则就要抛出异常。 471 | 472 | ### 事务异常 473 | 474 | 事务的异常处理比较复杂,可以先看一看代码: 475 | 476 | ```php 477 | protected function handleTransactionException($e, $currentAttempt, $maxAttempts) 478 | { 479 | if ($this->causedByDeadlock($e) && 480 | $this->transactions > 1) { 481 | --$this->transactions; 482 | 483 | throw $e; 484 | } 485 | 486 | $this->rollBack(); 487 | 488 | if ($this->causedByDeadlock($e) && 489 | $currentAttempt < $maxAttempts) { 490 | return; 491 | } 492 | 493 | throw $e; 494 | } 495 | 496 | protected function causedByDeadlock(Exception $e) 497 | { 498 | $message = $e->getMessage(); 499 | 500 | return Str::contains($message, [ 501 | 'Deadlock found when trying to get lock', 502 | 'deadlock detected', 503 | 'The database file is locked', 504 | 'database is locked', 505 | 'database table is locked', 506 | 'A table in the database is locked', 507 | 'has been chosen as the deadlock victim', 508 | 'Lock wait timeout exceeded; try restarting transaction', 509 | ]); 510 | } 511 | ``` 512 | 513 | 这里可以分为四种情况: 514 | 515 | - 单一事务,非死锁导致的异常 516 | 517 | 单一事务就是说,此时的事务只有一层,没有嵌套事务的存在。数据库的异常也不是死锁导致的,一般是由于 `sql` 语句不正确引起的。这个时候,`handleTransactionException` 会直接回滚事务,并且抛出异常到外层: 518 | 519 | ```php 520 | try { 521 | return tap($callback($this), function ($result) { 522 | $this->commit(); 523 | }); 524 | } 525 | catch (Exception $e) { 526 | $this->handleTransactionException( 527 | $e, $currentAttempt, $attempts 528 | ); 529 | } catch (Throwable $e) { 530 | $this->rollBack(); 531 | 532 | throw $e; 533 | } 534 | ``` 535 | 接到异常之后,程序会再次回滚,但是由于 `$this->transactions` 已经为 0,因此回滚直接返回,并未真正执行,之后就会抛出异常。 536 | 537 | - 单一事务,死锁异常 538 | 539 | 有死锁导致的单一事务异常,一般是由于其他程序同时更改了数据库,这个时候,就要判断当前重复尝试的次数是否大于用户设置的 `maxAttempts`,如果小于就继续尝试,如果大于,那么就会抛出异常。 540 | 541 | - 嵌套事务,非死锁异常 542 | 543 | 如果出现嵌套事务,例如: 544 | 545 | ```php 546 | \DB::transaction(function(){ 547 | ... 548 | //directly or indirectly call another transaction: 549 | \DB::transaction(function() { 550 | ... 551 | ... 552 | }, 2);//attempt twice 553 | }, 2);//attempt twice 554 | 555 | ``` 556 | 如果是非死锁导致的异常,那么就要首先回滚内层的事务,抛出异常到外层事务,再回滚外层事务,抛出异常,让用户来处理。也就是说,对于嵌套事务来说,内部事务异常,一定要回滚整个事务,而不是仅仅回滚内部事务。 557 | 558 | - 嵌套事务,死锁异常 559 | 560 | 嵌套事务的死锁异常,仍然和嵌套事务非死锁异常一样,内部事务异常,一定要回滚整个事务。 561 | 562 | 但是,不同的是,`mysql` 对于嵌套事务的回滚会导致外部事务一并回滚:[InnoDB Error Handling](https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html),因此这时,我们仅仅将 `$this->transactions` 减一,并抛出异常,使得外层事务回滚抛出异常即可。 563 | 564 | ### 回滚事务 565 | 566 | 如果事务内的数据库更新操作失败,那么就要进行回滚: 567 | 568 | ```php 569 | public function rollBack($toLevel = null) 570 | { 571 | $toLevel = is_null($toLevel) 572 | ? $this->transactions - 1 573 | : $toLevel; 574 | 575 | if ($toLevel < 0 || $toLevel >= $this->transactions) { 576 | return; 577 | } 578 | 579 | $this->performRollBack($toLevel); 580 | 581 | $this->transactions = $toLevel; 582 | 583 | $this->fireConnectionEvent('rollingBack'); 584 | } 585 | 586 | ``` 587 | 588 | 回滚的第一件事就是要减少 `$this->transactions` 的值,标志当前事务失败。 589 | 590 | 回滚的时候仍然要判断当前事务的状态,如果当前处于嵌套事务的话,就要进行回滚到 `SAVEPOINT`,如果是单一事务的话,才会真正回滚退出事务: 591 | 592 | ```php 593 | protected function performRollBack($toLevel) 594 | { 595 | if ($toLevel == 0) { 596 | $this->getPdo()->rollBack(); 597 | } elseif ($this->queryGrammar->supportsSavepoints()) { 598 | $this->getPdo()->exec( 599 | $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1)) 600 | ); 601 | } 602 | } 603 | 604 | public function compileSavepointRollBack($name) 605 | { 606 | return 'ROLLBACK TO SAVEPOINT '.$name; 607 | } 608 | ``` 609 | 610 | ## 提交事务 611 | 612 | 提交事务比较简单,仅仅是调用 `pdo` 的 `commit` 即可。需要注意的是对于嵌套事务的事务提交,`commit` 函数仅仅更新了 `$this->transactions`,而并没有真正提交事务,原因是内层事务的提交对于 `mysql` 来说是无效的,只有外部事务的提交才能更新整个事务。 613 | 614 | ```php 615 | public function commit() 616 | { 617 | if ($this->transactions == 1) { 618 | $this->getPdo()->commit(); 619 | } 620 | 621 | $this->transactions = max(0, $this->transactions - 1); 622 | 623 | $this->fireConnectionEvent('committed'); 624 | } 625 | 626 | ``` -------------------------------------------------------------------------------- /Laravel Database——查询构造器与语法编译器源码分析(下).md: -------------------------------------------------------------------------------- 1 | ## insert 语句 2 | 3 | `insert` 语句也是我们经常使用的数据库操作,它的源码如下: 4 | 5 | ```php 6 | public function insert(array $values) 7 | { 8 | if (empty($values)) { 9 | return true; 10 | } 11 | 12 | if (! is_array(reset($values))) { 13 | $values = [$values]; 14 | } 15 | else { 16 | foreach ($values as $key => $value) { 17 | ksort($value); 18 | 19 | $values[$key] = $value; 20 | } 21 | } 22 | 23 | return $this->connection->insert( 24 | $this->grammar->compileInsert($this, $values), 25 | $this->cleanBindings(Arr::flatten($values, 1)) 26 | ); 27 | } 28 | ``` 29 | `laravel` 的 `insert` 是允许批量插入的,方法如下: 30 | 31 | ```php 32 | DB::table('users')->insert([['email' => 'foo', 'name' => 'taylor'], ['email' => 'bar', 'name' => 'dayle']]); 33 | ``` 34 | 一个语句可以向数据库插入两条记录。`sql` 语句为: 35 | 36 | ```php 37 | 38 | insert into users (`email`,`name`) values ('foo', 'taylor'), ('bar', 'dayle'); 39 | ``` 40 | 41 | 因此,`laravel` 在处理 `insert` 的时候,首先会判断当前的参数是单条插入还是批量插入。 42 | 43 | ```php 44 | if (! is_array(reset($values))) { 45 | $values = [$values]; 46 | } 47 | ``` 48 | `reset` 会返回 `values` 的第一个元素。如果是批量插入的话,第一个元素必然也是数组。如果的单条插入的话,第一个元素是列名与列值。因此如果是单条插入的话,会在最外层再套一个数组,统一插入的格式。 49 | 50 | 如果是批量插入的话,首先需要把插入的各个字段进行排序,保证插入时各个记录的列顺序一致。 51 | 52 | 53 | ### compileInsert 54 | 55 | 对 `insert` 的编译也是按照批量插入的标准来进行的: 56 | 57 | ```php 58 | public function compileInsert(Builder $query, array $values) 59 | { 60 | $table = $this->wrapTable($query->from); 61 | 62 | if (! is_array(reset($values))) { 63 | $values = [$values]; 64 | } 65 | 66 | $columns = $this->columnize(array_keys(reset($values))); 67 | 68 | $parameters = collect($values)->map(function ($record) { 69 | return '('.$this->parameterize($record).')'; 70 | })->implode(', '); 71 | 72 | return "insert into $table ($columns) values $parameters"; 73 | } 74 | ``` 75 | 76 | 首先对插入的列名进行 `columnze` 函数处理,之后对每个记录的插入都调用 `parameterize` 函数来对列值进行处理,并用 `()` 包围起来。 77 | 78 | ## update 语句 79 | 80 | ```php 81 | public function update(array $values) 82 | { 83 | $sql = $this->grammar->compileUpdate($this, $values); 84 | 85 | return $this->connection->update($sql, $this->cleanBindings( 86 | $this->grammar->prepareBindingsForUpdate($this->bindings, $values) 87 | )); 88 | } 89 | 90 | ``` 91 | 与插入语句相比,更新语句更加复杂,因为更新语句必然带有 `where` 条件,有时还会有 `join` 条件: 92 | 93 | ```php 94 | public function compileUpdate(Builder $query, $values) 95 | { 96 | $table = $this->wrapTable($query->from); 97 | 98 | $columns = collect($values)->map(function ($value, $key) { 99 | return $this->wrap($key).' = '.$this->parameter($value); 100 | })->implode(', '); 101 | 102 | $joins = ''; 103 | 104 | if (isset($query->joins)) { 105 | $joins = ' '.$this->compileJoins($query, $query->joins); 106 | } 107 | 108 | $wheres = $this->compileWheres($query); 109 | 110 | return trim("update {$table}{$joins} set $columns $wheres"); 111 | } 112 | 113 | ``` 114 | 115 | ## updateOrInsert 语句 116 | 117 | `updateOrInsert` 语句会先根据 `attributes` 条件查询,如果查询失败,就会合并 `attributes` 与 `values` 两个数组,并插入新的记录。如果查询成功,就会利用 `values` 更新数据。 118 | 119 | ```php 120 | public function updateOrInsert(array $attributes, array $values = []) 121 | { 122 | if (! $this->where($attributes)->exists()) { 123 | return $this->insert(array_merge($attributes, $values)); 124 | } 125 | 126 | return (bool) $this->take(1)->update($values); 127 | } 128 | ``` 129 | 130 | ## delete 语句 131 | 132 | 删除语句比较简单,参数仅仅需要 id 即可,`delete` 语句会添加 `id` 的 `where` 条件: 133 | 134 | ```php 135 | public function delete($id = null) 136 | { 137 | if (! is_null($id)) { 138 | $this->where($this->from.'.id', '=', $id); 139 | } 140 | 141 | return $this->connection->delete( 142 | $this->grammar->compileDelete($this), $this->getBindings() 143 | ); 144 | } 145 | 146 | ``` 147 | 删除语句的编译需要先编译 `where` 条件: 148 | 149 | ```php 150 | public function compileDelete(Builder $query) 151 | { 152 | $wheres = is_array($query->wheres) ? $this->compileWheres($query) : ''; 153 | 154 | return trim("delete from {$this->wrapTable($query->from)} $wheres"); 155 | } 156 | ``` 157 | 158 | ## 动态 where 159 | 160 | `laravel` 有一个有趣的功能:动态 where。 161 | 162 | ```php 163 | DB::table('users')->whereFooBarAndBazOrQux('corge', 'waldo', 'fred') 164 | ``` 165 | 这个语句会生成下面的 `sql` 语句: 166 | 167 | ```php 168 | select * from users where foo_bar = 'corge' and baz = 'waldo' or qux = 'fred'; 169 | ``` 170 | 也就是说,动态 `where` 将函数名解析为列名与连接条件,将参数作为搜索的值。 171 | 172 | 我们先看源码: 173 | 174 | ```php 175 | public function dynamicWhere($method, $parameters) 176 | { 177 | 178 | $finder = substr($method, 5); 179 | 180 | $segments = preg_split( 181 | '/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE 182 | ); 183 | 184 | $connector = 'and'; 185 | 186 | $index = 0; 187 | 188 | foreach ($segments as $segment) { 189 | if ($segment !== 'And' && $segment !== 'Or') { 190 | $this->addDynamic($segment, $connector, $parameters, $index); 191 | 192 | $index++; 193 | } 194 | 195 | else { 196 | $connector = $segment; 197 | } 198 | } 199 | 200 | return $this; 201 | } 202 | 203 | protected function addDynamic($segment, $connector, $parameters, $index) 204 | { 205 | $bool = strtolower($connector); 206 | 207 | $this->where(Str::snake($segment), '=', $parameters[$index], $bool); 208 | } 209 | ``` 210 | 211 | - 首先,程序会提取函数名 `whereFooBarAndBazOrQux`,删除前 5 个字符,`FooBarAndBazOrQux`。 212 | 213 | - 正则判断,根据 `And` 或 `Or` 对函数名进行切割:`FooBar`、`And`、`Baz`、`Or`、`Qux`。 214 | 215 | - 添加 `where` 条件,将驼峰命名改为蛇型命名。 216 | -------------------------------------------------------------------------------- /Laravel ENV—— 环境变量的加载与源码解析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | `laravel` 在启动时,会加载项目的 `env` 文件,本文将会详细介绍 `env` 文件的使用与源码的分析。 4 | 5 | # ENV 文件的使用 6 | 7 | ## 多环境 ENV 文件的设置 8 | 9 | 一、在项目写多个 `ENV` 文件,例如三个 `env` 文件: 10 | 11 | - `.env.development`、 12 | - `.env.staging `、 13 | - `.env.production`, 14 | 15 | 这三个文件中分别针对不同环境为某些变量配置了不同的值, 16 | 17 | 二、配置 `APP_ENV` 环境变量值 18 | 19 | 配置环境变量的方法有很多,其中一个方法是在 `nginx` 的配置文件中写下这句代码: 20 | 21 | ```php 22 | fastcgi_param APP_ENV production; 23 | ``` 24 | 25 | 那么 `laravel` 会通过 `env('APP_ENV')` 根据环境变量 `APP_ENV` 来判断当前具体的环境,假如环境变量 `APP_ENV` 为 `production`,那么 `laravel` 将会自动加载 `.env.production` 文件。 26 | 27 | ## 自定义 ENV 文件的路径与文件名 28 | 29 | `laravel` 为用户提供了自定义 `ENV` 文件路径或文件名的函数, 30 | 31 | 例如,若想要自定义 `env` 路径,就可以在 `bootstrap` 文件夹中 `app.php` 文件: 32 | 33 | ```php 34 | $app = new Illuminate\Foundation\Application( 35 | realpath(__DIR__.'/../') 36 | ); 37 | 38 | $app->useEnvironmentPath('/customer/path') 39 | ``` 40 | 若想要自定义 `env` 文件名称,就可以在 `bootstrap` 文件夹中 `app.php` 文件: 41 | 42 | ```php 43 | $app = new Illuminate\Foundation\Application( 44 | realpath(__DIR__.'/../') 45 | ); 46 | 47 | $app->loadEnvironmentFrom('customer.env') 48 | ``` 49 | ## ENV 文件变量设置 50 | 51 | - 在 `env` 文件中,我们可以为变量赋予具体值: 52 | 53 | ```php 54 | CFOO=bar 55 | ``` 56 | 57 | 值得注意的是,这种具体值不允许赋予多个,例如: 58 | 59 | ```php 60 | CFOO=bar baz 61 | ``` 62 | 63 | - 可以为变量赋予字符串引用 64 | 65 | ```php 66 | CQUOTES="a value with a # character" 67 | ``` 68 | 69 | 值得注意的是,这种引用不允许字符串中存在符号 `\`,只能使用转义字符 `\\` 70 | 71 | 而且也不允许内嵌符号 `""`,只能使用转移字符 `\"`,否则取值会意外结束: 72 | 73 | ```php 74 | CQUOTESWITHQUOTE="a value with a # character & a quote \" character inside quotes" # " this is a comment 75 | 76 | $this->assertEquals('a value with a # character & a quote " character inside quotes', getenv('CQUOTESWITHQUOTE')); 77 | ``` 78 | 79 | - 可以在 `env` 文件中添加注释,方法是以 `#` 开始: 80 | 81 | ```php 82 | CQUOTES="a value with a # character" # this is a comment 83 | ``` 84 | 85 | - 可以使用 `export` 来为变量赋值: 86 | 87 | ```php 88 | export EFOO="bar" 89 | ``` 90 | - 可以在 `env` 文件中使用变量为变量赋值: 91 | 92 | ```php 93 | NVAR1="Hello" 94 | NVAR2="World!" 95 | NVAR3="{$NVAR1} {$NVAR2}" 96 | NVAR4="${NVAR1} ${NVAR2}" 97 | NVAR5="$NVAR1 {NVAR2}" 98 | 99 | $this->assertEquals('{$NVAR1} {$NVAR2}', $_ENV['NVAR3']); // not resolved 100 | $this->assertEquals('Hello World!', $_ENV['NVAR4']); 101 | $this->assertEquals('$NVAR1 {NVAR2}', $_ENV['NVAR5']); // not resolved 102 | ``` 103 | 104 | # ENV 加载源码分析 105 | 106 | ## laravel 加载 ENV 107 | 108 | `ENV` 的加载功能由类 `\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class` 完成,它的启动函数为: 109 | 110 | ```php 111 | public function bootstrap(Application $app) 112 | { 113 | if ($app->configurationIsCached()) { 114 | return; 115 | } 116 | 117 | $this->checkForSpecificEnvironmentFile($app); 118 | 119 | try { 120 | (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); 121 | } catch (InvalidPathException $e) { 122 | // 123 | } 124 | } 125 | ``` 126 | 127 | 如果我们在环境变量中设置了 `APP_ENV` 变量,那么就会调用函数 `checkForSpecificEnvironmentFile` 来根据环境加载不同的 `env` 文件: 128 | 129 | ```php 130 | protected function checkForSpecificEnvironmentFile($app) 131 | { 132 | if (php_sapi_name() == 'cli' && with($input = new ArgvInput)->hasParameterOption('--env')) { 133 | $this->setEnvironmentFilePath( 134 | $app, $app->environmentFile().'.'.$input->getParameterOption('--env') 135 | ); 136 | } 137 | 138 | if (! env('APP_ENV')) { 139 | return; 140 | } 141 | 142 | $this->setEnvironmentFilePath( 143 | $app, $app->environmentFile().'.'.env('APP_ENV') 144 | ); 145 | } 146 | 147 | protected function setEnvironmentFilePath($app, $file) 148 | { 149 | if (file_exists($app->environmentPath().'/'.$file)) { 150 | $app->loadEnvironmentFrom($file); 151 | } 152 | } 153 | ``` 154 | 155 | ## vlucas/phpdotenv 源码解读 156 | 157 | `laravel` 中对 `env` 文件的读取是采用 `vlucas/phpdotenv` 的开源项目: 158 | 159 | ```php 160 | class Dotenv 161 | { 162 | public function __construct($path, $file = '.env') 163 | { 164 | $this->filePath = $this->getFilePath($path, $file); 165 | $this->loader = new Loader($this->filePath, true); 166 | } 167 | 168 | public function load() 169 | { 170 | return $this->loadData(); 171 | } 172 | 173 | protected function loadData($overload = false) 174 | { 175 | $this->loader = new Loader($this->filePath, !$overload); 176 | 177 | return $this->loader->load(); 178 | } 179 | } 180 | ``` 181 | `env` 文件变量的读取依赖类 `/Dotenv/Loader`: 182 | 183 | ```php 184 | class Loader 185 | { 186 | public function load() 187 | { 188 | $this->ensureFileIsReadable(); 189 | 190 | $filePath = $this->filePath; 191 | $lines = $this->readLinesFromFile($filePath); 192 | foreach ($lines as $line) { 193 | if (!$this->isComment($line) && $this->looksLikeSetter($line)) { 194 | $this->setEnvironmentVariable($line); 195 | } 196 | } 197 | 198 | return $lines; 199 | } 200 | } 201 | ``` 202 | 203 | 我们可以看到,`env` 文件的读取的流程: 204 | 205 | - 判断 `env` 文件是否可读 206 | - 读取整个 `env` 文件,并将文件按行存储 207 | - 循环读取每一行,略过注释 208 | - 进行环境变量赋值 209 | 210 | ```php 211 | protected function ensureFileIsReadable() 212 | { 213 | if (!is_readable($this->filePath) || !is_file($this->filePath)) { 214 | throw new InvalidPathException(sprintf('Unable to read the environment file at %s.', $this->filePath)); 215 | } 216 | } 217 | 218 | protected function readLinesFromFile($filePath) 219 | { 220 | // Read file into an array of lines with auto-detected line endings 221 | $autodetect = ini_get('auto_detect_line_endings'); 222 | ini_set('auto_detect_line_endings', '1'); 223 | $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 224 | ini_set('auto_detect_line_endings', $autodetect); 225 | 226 | return $lines; 227 | } 228 | 229 | protected function isComment($line) 230 | { 231 | return strpos(ltrim($line), '#') === 0; 232 | } 233 | 234 | protected function looksLikeSetter($line) 235 | { 236 | return strpos($line, '=') !== false; 237 | } 238 | ``` 239 | 240 | 环境变量赋值是 `env` 文件加载的核心,主要由 `setEnvironmentVariable` 函数: 241 | 242 | ```php 243 | public function setEnvironmentVariable($name, $value = null) 244 | { 245 | list($name, $value) = $this->normaliseEnvironmentVariable($name, $value); 246 | 247 | if ($this->immutable && $this->getEnvironmentVariable($name) !== null) { 248 | return; 249 | } 250 | 251 | if (function_exists('apache_getenv') && function_exists('apache_setenv') && apache_getenv($name)) { 252 | apache_setenv($name, $value); 253 | } 254 | 255 | if (function_exists('putenv')) { 256 | putenv("$name=$value"); 257 | } 258 | 259 | $_ENV[$name] = $value; 260 | $_SERVER[$name] = $value; 261 | } 262 | ``` 263 | 264 | `normaliseEnvironmentVariable` 函数用来加载各种类型的环境变量: 265 | 266 | ```php 267 | protected function normaliseEnvironmentVariable($name, $value) 268 | { 269 | list($name, $value) = $this->splitCompoundStringIntoParts($name, $value); 270 | list($name, $value) = $this->sanitiseVariableName($name, $value); 271 | list($name, $value) = $this->sanitiseVariableValue($name, $value); 272 | 273 | $value = $this->resolveNestedVariables($value); 274 | 275 | return array($name, $value); 276 | } 277 | ``` 278 | `splitCompoundStringIntoParts` 用于将赋值语句转化为环境变量名 `name` 和环境变量值 `value`。 279 | 280 | ```php 281 | protected function splitCompoundStringIntoParts($name, $value) 282 | { 283 | if (strpos($name, '=') !== false) { 284 | list($name, $value) = array_map('trim', explode('=', $name, 2)); 285 | } 286 | 287 | return array($name, $value); 288 | } 289 | ``` 290 | `sanitiseVariableName` 用于格式化环境变量名: 291 | 292 | ```php 293 | protected function sanitiseVariableName($name, $value) 294 | { 295 | $name = trim(str_replace(array('export ', '\'', '"'), '', $name)); 296 | 297 | return array($name, $value); 298 | } 299 | ``` 300 | `sanitiseVariableValue` 用于格式化环境变量值: 301 | 302 | ```php 303 | protected function sanitiseVariableValue($name, $value) 304 | { 305 | $value = trim($value); 306 | if (!$value) { 307 | return array($name, $value); 308 | } 309 | 310 | if ($this->beginsWithAQuote($value)) { // value starts with a quote 311 | $quote = $value[0]; 312 | $regexPattern = sprintf( 313 | '/^ 314 | %1$s # match a quote at the start of the value 315 | ( # capturing sub-pattern used 316 | (?: # we do not need to capture this 317 | [^%1$s\\\\] # any character other than a quote or backslash 318 | |\\\\\\\\ # or two backslashes together 319 | |\\\\%1$s # or an escaped quote e.g \" 320 | )* # as many characters that match the previous rules 321 | ) # end of the capturing sub-pattern 322 | %1$s # and the closing quote 323 | .*$ # and discard any string after the closing quote 324 | /mx', 325 | $quote 326 | ); 327 | $value = preg_replace($regexPattern, '$1', $value); 328 | $value = str_replace("\\$quote", $quote, $value); 329 | $value = str_replace('\\\\', '\\', $value); 330 | } else { 331 | $parts = explode(' #', $value, 2); 332 | $value = trim($parts[0]); 333 | 334 | // Unquoted values cannot contain whitespace 335 | if (preg_match('/\s+/', $value) > 0) { 336 | throw new InvalidFileException('Dotenv values containing spaces must be surrounded by quotes.'); 337 | } 338 | } 339 | 340 | return array($name, trim($value)); 341 | } 342 | ``` 343 | 344 | 这段代码是加载 `env` 文件最复杂的部分,我们详细来说: 345 | 346 | - 若环境变量值是具体值,那么仅仅需要分割注释 `#` 部分,并判断是否存在空格符即可。 347 | 348 | - 若环境变量值由引用构成,那么就需要进行正则匹配,具体的正则表达式为: 349 | 350 | ```php 351 | /^"((?:[^"\\]|\\\\|\\"))".*$/mx 352 | ``` 353 | 这个正则表达式的意思是: 354 | 355 | - 提取 `“”` 双引号内部的字符串,抛弃双引号之后的字符串 356 | - 若双引号内部还有双引号,那么以最前面的双引号为提取内容,例如 "dfd("dfd")fdf",我们只能提取出来最前面的部分 "dfd(" 357 | - 对于内嵌的引用可以使用 `\"` ,例如 "dfd\"dfd\"fdf",我们就可以提取出来 "dfd\"dfd\"fdf"。 358 | - 不允许引用中含有 `\`,但可以使用转义字符 `\\` 359 | -------------------------------------------------------------------------------- /Laravel Event——事件系统的启动与运行源码分析.md: -------------------------------------------------------------------------------- 1 | # Laravel Event——事件系统的启动与运行源码分析 2 | 3 | ## 前言 4 | 5 | `Laravel` 的事件系统是一个简单的观察者模式,主要目的是用于代码的解耦,可以防止不同功能的代码耦合在一起。`laravel` 中事件系统由两部分构成,一个是事件的名称,事件的名称可以是个字符串,例如 `event.email`,也可以是一个事件类,例如 `App\Events\OrderShipped`;另一个是事件的 `listener`,可以是一个闭包,还可以是监听类,例如 `App\Listeners\SendShipmentNotification`。 6 | 7 | ## 事件服务的注册 8 | 9 | 事件服务的注册分为两部分,一个是 `Application` 启动时所调用的 `registerBaseServiceProviders` 函数: 10 | 11 | ```php 12 | protected function registerBaseServiceProviders() 13 | { 14 | $this->register(new EventServiceProvider($this)); 15 | 16 | $this->register(new LogServiceProvider($this)); 17 | 18 | $this->register(new RoutingServiceProvider($this)); 19 | } 20 | ``` 21 | 22 | 其中的 `EventServiceProvider` 是 `/Illuminate/Events/EventServiceProvider`: 23 | 24 | ```php 25 | public function register() 26 | { 27 | $this->app->singleton('events', function ($app) { 28 | return (new Dispatcher($app))->setQueueResolver(function () use ($app) { 29 | return $app->make(QueueFactoryContract::class); 30 | }); 31 | }); 32 | } 33 | 34 | ``` 35 | 这部分为 `Ioc` 容器注册了 `events` 实例,`Dispatcher` 就是 `events` 真正的实现类。`QueueResolver` 是队列化事件的实现。 36 | 37 | 另一个注册是普通注册类 `/app/Providers/EventServiceProvider` : 38 | 39 | ```php 40 | class EventServiceProvider extends ServiceProvider 41 | { 42 | protected $listen = [ 43 | 'App\Events\SomeEvent' => [ 44 | 'App\Listeners\EventListener', 45 | ], 46 | ]; 47 | 48 | public function boot() 49 | { 50 | parent::boot(); 51 | // 52 | } 53 | } 54 | 55 | ``` 56 | 57 | 这个注册类的主要作用是事件系统的启动,这个类继承自 `/Illuminate/Foundation/Support/Providers/EventServiceProvider`: 58 | 59 | ```php 60 | class EventServiceProvider extends ServiceProvider 61 | { 62 | protected $listen = []; 63 | 64 | protected $subscribe = []; 65 | 66 | public function boot() 67 | { 68 | foreach ($this->listens() as $event => $listeners) { 69 | foreach ($listeners as $listener) { 70 | Event::listen($event, $listener); 71 | } 72 | } 73 | 74 | foreach ($this->subscribe as $subscriber) { 75 | Event::subscribe($subscriber); 76 | } 77 | } 78 | } 79 | 80 | ``` 81 | 可以看到,事件系统的启动主要是进行事件系统的监听与订阅。 82 | 83 | ## 事件系统的监听 listen 84 | 85 | 所谓的事件监听,就是将事件名与闭包函数,或者事件类与监听类之间建立关联。 86 | 87 | ```php 88 | public function listen($events, $listener) 89 | { 90 | foreach ((array) $events as $event) { 91 | if (Str::contains($event, '*')) { 92 | $this->setupWildcardListen($event, $listener); 93 | } else { 94 | $this->listeners[$event][] = $this->makeListener($listener); 95 | } 96 | } 97 | } 98 | 99 | protected function setupWildcardListen($event, $listener) 100 | { 101 | $this->wildcards[$event][] = $this->makeListener($listener, true); 102 | } 103 | ``` 104 | 对于有通配符的事件名,会统一放入 `wildcards` 数组中,`makeListener` 是创建事件的关键: 105 | 106 | ```php 107 | public function makeListener($listener, $wildcard = false) 108 | { 109 | if (is_string($listener)) { 110 | return $this->createClassListener($listener, $wildcard); 111 | } 112 | 113 | return function ($event, $payload) use ($listener, $wildcard) { 114 | if ($wildcard) { 115 | return $listener($event, $payload); 116 | } else { 117 | return $listener(...array_values($payload)); 118 | } 119 | }; 120 | } 121 | ``` 122 | 创建监听者的时候,会判断监听对象是监听类还是闭包函数。 123 | 124 | 对于闭包监听来说,`makeListener` 会再包上一层闭包函数,根据是否含有通配符来确定具体的参数。 125 | 126 | 对于监听类来说,会继续 `createClassListener`: 127 | 128 | ```php 129 | public function createClassListener($listener, $wildcard = false) 130 | { 131 | return function ($event, $payload) use ($listener, $wildcard) { 132 | if ($wildcard) { 133 | return call_user_func($this->createClassCallable($listener), $event, $payload); 134 | } else { 135 | return call_user_func_array( 136 | $this->createClassCallable($listener), $payload 137 | ); 138 | } 139 | }; 140 | } 141 | 142 | protected function createClassCallable($listener) 143 | { 144 | list($class, $method) = $this->parseClassCallable($listener); 145 | 146 | if ($this->handlerShouldBeQueued($class)) { 147 | return $this->createQueuedHandlerCallable($class, $method); 148 | } else { 149 | return [$this->container->make($class), $method]; 150 | } 151 | } 152 | ``` 153 | 154 | 对于监听类来说,程序首先会判断监听类对应的函数: 155 | 156 | ```php 157 | protected function parseClassCallable($listener) 158 | { 159 | return Str::parseCallback($listener, 'handle'); 160 | } 161 | ``` 162 | 163 | 如果未指定监听类的对应函数,那么会默认 `handle` 函数。 164 | 165 | 如果当前监听类是队列的话,会将任务推送给队列。 166 | 167 | ## 触发事件 168 | 169 | 事件的触发可以利用事件名,或者事件类的实例: 170 | 171 | ```php 172 | public function dispatch($event, $payload = [], $halt = false) 173 | { 174 | list($event, $payload) = $this->parseEventAndPayload( 175 | $event, $payload 176 | ); 177 | 178 | if ($this->shouldBroadcast($payload)) { 179 | $this->broadcastEvent($payload[0]); 180 | } 181 | 182 | $responses = []; 183 | 184 | foreach ($this->getListeners($event) as $listener) { 185 | $response = $listener($event, $payload); 186 | 187 | if (! is_null($response) && $halt) { 188 | return $response; 189 | } 190 | 191 | if ($response === false) { 192 | break; 193 | } 194 | 195 | $responses[] = $response; 196 | } 197 | 198 | return $halt ? null : $responses; 199 | } 200 | 201 | ``` 202 | `parseEventAndPayload` 函数利用传入参数是事件名还是事件类实例来确定监听类函数的参数: 203 | 204 | ```php 205 | protected function parseEventAndPayload($event, $payload) 206 | { 207 | if (is_object($event)) { 208 | list($payload, $event) = [[$event], get_class($event)]; 209 | } 210 | 211 | return [$event, array_wrap($payload)]; 212 | } 213 | ``` 214 | 如果是事件类的实例,那么监听函数的参数就是事件类自身;如果是事件类名,那么监听函数的参数就是触发事件时传入的参数。 215 | 216 | 获得事件与参数后,就要获取监听类: 217 | 218 | ```php 219 | public function getListeners($eventName) 220 | { 221 | $listeners = isset($this->listeners[$eventName]) ? $this->listeners[$eventName] : []; 222 | 223 | $listeners = array_merge( 224 | $listeners, $this->getWildcardListeners($eventName) 225 | ); 226 | 227 | return class_exists($eventName, false) 228 | ? $this->addInterfaceListeners($eventName, $listeners) 229 | : $listeners; 230 | } 231 | 232 | ``` 233 | 234 | 寻找监听类的时候,也要从通配符监听器中寻找: 235 | 236 | ```php 237 | protected function getWildcardListeners($eventName) 238 | { 239 | $wildcards = []; 240 | 241 | foreach ($this->wildcards as $key => $listeners) { 242 | if (Str::is($key, $eventName)) { 243 | $wildcards = array_merge($wildcards, $listeners); 244 | } 245 | } 246 | 247 | return $wildcards; 248 | } 249 | ``` 250 | 251 | 如果监听类继承自其他类,那么父类也会一并当做监听类返回。 252 | 253 | 获得了监听类之后,就要调用监听类相应的函数。 254 | 255 | 触发事件时有一个参数 `halt`,这个参数如果是 `true` 的时候,只要有一个监听类返回了结果,那么就会立刻返回。例如: 256 | 257 | ```php 258 | public function testHaltingEventExecution() 259 | { 260 | unset($_SERVER['__event.test']); 261 | $d = new Dispatcher; 262 | $d->listen('foo', function ($foo) { 263 | $this->assertTrue(true); 264 | 265 | return 'here'; 266 | }); 267 | $d->listen('foo', function ($foo) { 268 | throw new Exception('should not be called'); 269 | }); 270 | $d->until('foo', ['bar']); 271 | } 272 | 273 | ``` 274 | 275 | 多个监听类在运行的时候,只要有一个返回了 `false`,那么就会中断事件。 276 | 277 | ### push 函数 278 | 279 | `push` 函数可以将触发事件的参数事先设置好,这样触发的时候只要写入事件名即可,例如: 280 | 281 | ```php 282 | public function testQueuedEventsAreFired() 283 | { 284 | unset($_SERVER['__event.test']); 285 | $d = new Dispatcher; 286 | $d->push('update', ['name' => 'taylor']); 287 | $d->listen('update', function ($name) { 288 | $_SERVER['__event.test'] = $name; 289 | }); 290 | 291 | $this->assertFalse(isset($_SERVER['__event.test'])); 292 | $d->flush('update'); 293 | $this->assertEquals('taylor', $_SERVER['__event.test']); 294 | } 295 | ``` 296 | 297 | 原理也很简单: 298 | 299 | ```php 300 | public function push($event, $payload = []) 301 | { 302 | $this->listen($event.'_pushed', function () use ($event, $payload) { 303 | $this->dispatch($event, $payload); 304 | }); 305 | } 306 | 307 | public function flush($event) 308 | { 309 | $this->dispatch($event.'_pushed'); 310 | } 311 | ``` 312 | 313 | ## 数据库 Eloquent 的事件 314 | 315 | 数据库模型的事件的注册除了以上的方法还有另外两种,具体详情可以看:[Laravel 模型事件实现原理](https://laravel-china.org/articles/5465/event-realization-principle-of-laravel-model) ; 316 | 317 | ### 事件注册 318 | 319 | - 静态方法定义 320 | 321 | ```php 322 | class EventServiceProvider extends ServiceProvider 323 | { 324 | public function boot() 325 | { 326 | parent::boot(); 327 | 328 | User::saved(function(User$user) { 329 | 330 | }); 331 | 332 | User::saved('UserSavedListener@saved'); 333 | } 334 | } 335 | 336 | ``` 337 | 338 | - 观察者 339 | 340 | ```php 341 | class UserObserver 342 | { 343 | public function created(User $user) 344 | { 345 | // 346 | } 347 | 348 | public function saved(User $user) 349 | { 350 | // 351 | } 352 | } 353 | 354 | ``` 355 | 356 | 然后在某个服务提供者的boot方法中注册观察者: 357 | 358 | ```php 359 | class AppServiceProvider extends ServiceProvider 360 | { 361 | public function boot() 362 | { 363 | User::observe(UserObserver::class); 364 | } 365 | 366 | public function register() 367 | { 368 | // 369 | } 370 | } 371 | 372 | ``` 373 | 374 | 这两种方法都是向事件系统注册事件名 `eloquent.{$event}: {static::class}`: 375 | 376 | - 静态方法 377 | 378 | ```php 379 | public static function saved($callback) 380 | { 381 | static::registerModelEvent('saved', $callback); 382 | } 383 | 384 | protected static function registerModelEvent($event, $callback) 385 | { 386 | if (isset(static::$dispatcher)) { 387 | $name = static::class; 388 | 389 | static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback); 390 | } 391 | } 392 | ``` 393 | 394 | - 观察者 395 | 396 | ```php 397 | public static function observe($class) 398 | { 399 | $instance = new static; 400 | 401 | $className = is_string($class) ? $class : get_class($class); 402 | 403 | foreach ($instance->getObservableEvents() as $event) { 404 | if (method_exists($class, $event)) { 405 | static::registerModelEvent($event, $className.'@'.$event); 406 | } 407 | } 408 | } 409 | 410 | public function getObservableEvents() 411 | { 412 | return array_merge( 413 | [ 414 | 'creating', 'created', 'updating', 'updated', 415 | 'deleting', 'deleted', 'saving', 'saved', 416 | 'restoring', 'restored', 417 | ], 418 | $this->observables 419 | ); 420 | } 421 | ``` 422 | 423 | ### 事件触发 424 | 425 | 模型事件的触发需要调用 `fireModelEvent` 函数: 426 | 427 | ```php 428 | protected function fireModelEvent($event, $halt = true) 429 | { 430 | if (! isset(static::$dispatcher)) { 431 | return true; 432 | } 433 | 434 | $method = $halt ? 'until' : 'fire'; 435 | 436 | $result = $this->fireCustomModelEvent($event, $method); 437 | 438 | return ! is_null($result) ? $result : static::$dispatcher->{$method}( 439 | "eloquent.{$event}: ".static::class, $this 440 | ); 441 | } 442 | 443 | ``` 444 | 445 | `fireCustomModelEvent` 是我们本文中着重讲的事件类与监听类的触发: 446 | 447 | ```php 448 | protected function fireCustomModelEvent($event, $method) 449 | { 450 | if (! isset($this->events[$event])) { 451 | return; 452 | } 453 | 454 | $result = static::$dispatcher->$method(new $this->events[$event]($this)); 455 | 456 | if (! is_null($result)) { 457 | return $result; 458 | } 459 | } 460 | ``` 461 | 462 | 如果没有对应的事件后,会继续利用事件名进行触发。 463 | 464 | `until` 是我们上一节讲的如果任意事件返回正确结果,就会直接返回,不会继续进行下一个事件。 -------------------------------------------------------------------------------- /Laravel Exceptions——异常与错误处理.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 对于一个优秀的框架来说,正确的异常处理可以防止暴露自身接口给用户,可以提供快速追溯问题的提示给开发人员。本文会详细的介绍 `laravel` 异常处理的源码。 4 | 5 | # PHP 异常处理 6 | 7 | 本章节参考 [PHP错误异常处理详解](http://blog.csdn.net/hguisu/article/details/7464977)。 8 | 9 | 异常处理(又称为错误处理)功能提供了处理程序运行时出现的错误或异常情况的方法。 10 |    11 | 异常处理通常是防止未知错误产生所采取的处理措施。异常处理的好处是你不用再绞尽脑汁去考虑各种错误,这为处理某一类错误提供了一个很有效的方法,使编程效率大大提高。当异常被触发时,通常会发生: 12 | 13 | - 当前代码状态被保存 14 | - 代码执行被切换到预定义的异常处理器函数 15 | - 根据情况,处理器也许会从保存的代码状态重新开始执行代码,终止脚本执行,或从代码中另外的位置继续执行脚本 16 | 17 | PHP 5 提供了一种新的面向对象的错误处理方法。可以使用检测(try)、抛出(throw)和捕获(catch)异常。即使用try检测有没有抛出(throw)异常,若有异常抛出(throw),使用catch捕获异常。 18 | 19 | 一个 try 至少要有一个与之对应的 catch。定义多个 catch 可以捕获不同的对象。php 会按这些 catch 被定义的顺序执行,直到完成最后一个为止。而在这些 catch 内,又可以抛出新的异常。 20 | 21 | ## 异常的抛出 22 | 23 | 当一个异常被抛出时,其后的代码将不会继续执行,PHP 会尝试查找匹配的 `catch` 代码块。如果一个异常没有被捕获,而且又没用使用`set_exception_handler()` 作相应的处理的话,那么 `PHP` 将会产生一个严重的错误,并且输出未能捕获异常 `(Uncaught Exception ... )` 的提示信息。 24 | 25 | 抛出异常,但不去捕获它: 26 | 27 | ```php 28 | ini_set('display_errors', 'On'); 29 | error_reporting(E_ALL & ~ E_WARNING); 30 | $error = 'Always throw this error'; 31 | throw new Exception($error); 32 | // 继续执行 33 | echo 'Hello World'; 34 | ``` 35 | 上面的代码会获得类似这样的一个致命错误: 36 | 37 | ```php 38 | Fatal error: Uncaught exception 'Exception' with message 'Always throw this error' in E:\sngrep\index.php on line 5 39 | Exception: Always throw this error in E:\sngrep\index.php on line 5 40 | Call Stack: 41 | 0.0005 330680 1. {main}() E:\sngrep\index.php:0 42 | ``` 43 | 44 | ## Try, throw 和 catch 45 | 46 | 要避免上面这个致命错误,可以使用try catch捕获掉。 47 | 48 | 处理处理程序应当包括: 49 | 50 | 51 | - Try - 使用异常的函数应该位于 "try" 代码块内。如果没有触发异常,则代码将照常继续执行。但是如果异常被触发,会抛出一个异常。 52 | - Throw - 这里规定如何触发异常。每一个 "throw" 必须对应至少一个 "catch" 53 | - Catch - "catch" 代码块会捕获异常,并创建一个包含异常信息的对象 54 | 55 | 抛出异常并捕获掉,可以继续执行后面的代码: 56 | 57 | ```php 58 | try { 59 | $error = 'Always throw this error'; 60 | throw new Exception($error); 61 | 62 | // 从这里开始,tra 代码块内的代码将不会被执行 63 | echo 'Never executed'; 64 | 65 | } catch (Exception $e) { 66 | echo 'Caught exception: ', $e->getMessage(),'
'; 67 | } 68 | 69 | // 继续执行 70 | echo 'Hello World'; 71 | ``` 72 | 73 | ## 顶层异常处理器 set_exception_handler 74 | 75 | 在我们实际开发中,异常捕捉仅仅靠 `try {} catch ()` 是远远不够的。`set_exception_handler()` 函数可设置处理所有未捕获异常的用户定义函数。 76 | 77 | ```php 78 | function myException($exception) 79 | { 80 | echo "Exception: " , $exception->getMessage(); 81 | } 82 | 83 | set_exception_handler('myException'); 84 | throw new Exception('Uncaught Exception occurred'); 85 | ``` 86 | 87 | ## 扩展 PHP 内置的异常处理类 88 | 89 | 用户可以用自定义的异常处理类来扩展 PHP 内置的异常处理类。以下的代码说明了在内置的异常处理类中,哪些属性和方法在子类中是可访问和可继承的。 90 | 91 | ```php 92 | class Exception 93 | { 94 | protected $message = 'Unknown exception'; // 异常信息 95 | protected $code = 0; // 用户自定义异常代码 96 | protected $file; // 发生异常的文件名 97 | protected $line; // 发生异常的代码行号 98 | 99 | function __construct($message = null, $code = 0); 100 | 101 | final function getMessage(); // 返回异常信息 102 | final function getCode(); // 返回异常代码 103 | final function getFile(); // 返回发生异常的文件名 104 | final function getLine(); // 返回发生异常的代码行号 105 | final function getTrace(); // backtrace() 数组 106 | final function getTraceAsString(); // 已格成化成字符串的 getTrace() 信息 107 | 108 | /* 可重载的方法 */ 109 | function __toString(); // 可输出的字符串 110 | } 111 | ``` 112 | 113 | 如果使用自定义的类来扩展内置异常处理类,并且要重新定义构造函数的话,建议同时调用 `parent::__construct()` 来检查所有的变量是否已被赋值。当对象要输出字符串的时候,可以重载 `__toString()` 并自定义输出的样式。 114 | 115 | ```php 116 | class MyException extends Exception 117 | { 118 | // 重定义构造器使 message 变为必须被指定的属性 119 | public function __construct($message, $code = 0) { 120 | // 自定义的代码 121 | 122 | // 确保所有变量都被正确赋值 123 | parent::__construct($message, $code); 124 | } 125 | 126 | // 自定义字符串输出的样式 */ 127 | public function __toString() { 128 | return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; 129 | } 130 | 131 | public function customFunction() { 132 | echo "A Custom function for this type of exception\n"; 133 | } 134 | } 135 | ``` 136 | 137 | `MyException` 类是作为旧的 exception 类的一个扩展来创建的。这样它就继承了旧类的所有属性和方法,我们可以使用 `exception` 类的方法,比如 `getLine()` 、 `getFile()` 以及 `getMessage()`。 138 | 139 | # PHP 错误处理 140 | 141 | ## PHP 的错误级别 142 | 143 | | 值 | 常量 | 说明 | 144 | | :--: | :--------|:-----| 145 | | 1 | E_ERROR | 致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行。| 146 | | 2 | E_WARNING | 运行时警告 (非致命错误)。仅给出提示信息,但是脚本不会终止运行。 147 | | 4 | E_PARSE | 编译时语法解析错误。解析错误仅仅由分析器产生。 148 | | 8 | E_NOTICE | 运行时通知。表示脚本遇到可能会表现为错误的情况,但是在可以正常运行的脚本里面也可能会有类似的通知。 149 | | 16 | E\_CORE\_ERROR | 在PHP初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由PHP引擎核心产生的。 150 | | 32 | E\_CORE\_WARNING | PHP初始化启动过程中发生的警告 (非致命错误) 。类似 E_WARNING,但是是由PHP引擎核心产生的。 151 | | 64 | E\_COMPILE\_ERROR | 致命编译时错误。类似E_ERROR, 但是是由Zend脚本引擎产生的。 152 | | 128 | E\_COMPILE\_WARNING | 编译时警告 (非致命错误)。类似 E_WARNING,但是是由Zend脚本引擎产生的。 153 | | 256 | E\_USER\_ERROR | 用户产生的错误信息。类似 E_ERROR, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 154 | | 512 | E\_USER\_WARNING | 用户产生的警告信息。类似 E_WARNING, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 155 | | 1024 | E\_USER\_NOTICE | 用户产生的通知信息。类似 E_NOTICE, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 156 | | 2048 | E_STRICT | 启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。 157 | | 4096 | E\_RECOVERABLE\_ERROR | 可被捕捉的致命错误。 它表示发生了一个可能非常危险的错误,但是还没有导致PHP引擎处于不稳定的状态。 如果该错误没有被用户自定义句柄捕获 (参见 set_error_handler()),将成为一个 E_ERROR 从而脚本会终止运行。 158 | | 8192 | E_DEPRECATED | 运行时通知。启用后将会对在未来版本中可能无法正常工作的代码给出警告。 159 | | 16384 | E\_USER\_DEPRECATED | 用户产少的警告信息。 类似 E_DEPRECATED, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 160 | | 30719 | E_ALL | 用户产少的警告信息。 类似 E_DEPRECATED, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 161 | 162 | ## 错误的抛出 163 | 164 | 除了系统在运行 php 代码抛出的意外错误。我们还可以利用 `rigger_error ` 产生一个自定义的用户级别的 `error/warning/notice` 错误信息: 165 | 166 | ```php 167 | if ($divisor == 0) { 168 | trigger_error("Cannot divide by zero", E_USER_ERROR); 169 | } 170 | ``` 171 | ## 顶级错误处理器 172 | 173 | 顶级错误处理器 `set_error_handler` 一般用于捕捉 `E_NOTICE` 、`E_USER_ERROR`、`E_USER_WARNING`、`E_USER_NOTICE` 级别的错误,不能捕捉 `E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_CORE_WARNING`, `E_COMPILE_ERROR` 和`E_COMPILE_WARNING`。 174 | 175 | ## `register_shutdown_function` 176 | 177 | `register_shutdown_function()` 函数可实现当程序执行完成后执行的函数,其功能为可实现程序执行完成的后续操作。程序在运行的时候可能存在执行超时,或强制关闭等情况,但这种情况下默认的提示是非常不友好的,如果使用 `register_shutdown_function()` 函数捕获异常,就能提供更加友好的错误展示方式,同时可以实现一些功能的后续操作,如执行完成后的临时数据清理,包括临时文件等。 178 | 179 | 可以这样理解调用条件: 180 | 181 | - 当页面被用户强制停止时 182 | - 当程序代码运行超时时 183 | - 当PHP代码执行完成时,代码执行存在异常和错误、警告 184 | 185 | 我们前面说过,`set_error_handler` 能够捕捉的错误类型有限,很多致命错误例如解析错误等都无法捕捉,但是这类致命错误发生时,PHP 会调用 `register_shutdown_function` 所注册的函数,如果结合函数 `error_get_last `,就会获取错误发生的信息。 186 | 187 | # Laravel 异常处理 188 | 189 | `laravel` 的异常处理由类 `\Illuminate\Foundation\Bootstrap\HandleExceptions::class` 完成: 190 | 191 | ```php 192 | class HandleExceptions 193 | { 194 | public function bootstrap(Application $app) 195 | { 196 | $this->app = $app; 197 | 198 | error_reporting(-1); 199 | 200 | set_error_handler([$this, 'handleError']); 201 | 202 | set_exception_handler([$this, 'handleException']); 203 | 204 | register_shutdown_function([$this, 'handleShutdown']); 205 | 206 | if (! $app->environment('testing')) { 207 | ini_set('display_errors', 'Off'); 208 | } 209 | } 210 | } 211 | ``` 212 | 213 | ## 异常转化 214 | 215 | `laravel` 的异常处理均由函数 `handleException` 负责。 216 | 217 | `PHP7` 实现了一个全局的 `throwable` 接口,原来的 `Exception` 和部分 `Error` 都实现了这个接口, 以接口的方式定义了异常的继承结构。于是,`PHP7` 中更多的 `Error` 变为可捕获的 `Exception` 返回给开发者,如果不进行捕获则为 `Error` ,如果捕获就变为一个可在程序内处理的 `Exception`。这些可被捕获的 `Error` 通常都是不会对程序造成致命伤害的 `Error`,例如函数不存在。 218 | 219 | `PHP7` 中,基于 `/Error exception`,派生了5个新的engine exception:`ArithmeticError` / `AssertionError` / `DivisionByZeroError` / `ParseError` / `TypeError`。在 `PHP7` 里,无论是老的 `/Exception` 还是新的 `/Error` ,它们都实现了一个共同的interface: `/Throwable`。 220 | 221 | 因此,遇到非 `Exception` 类型的异常,首先就要将其转化为 `FatalThrowableError` 类型: 222 | 223 | ```php 224 | public function handleException($e) 225 | { 226 | if (! $e instanceof Exception) { 227 | $e = new FatalThrowableError($e); 228 | } 229 | 230 | $this->getExceptionHandler()->report($e); 231 | 232 | if ($this->app->runningInConsole()) { 233 | $this->renderForConsole($e); 234 | } else { 235 | $this->renderHttpResponse($e); 236 | } 237 | } 238 | ``` 239 | 240 | `FatalThrowableError` 是 `Symfony` 继承 `\ErrorException` 的错误异常类: 241 | 242 | ```php 243 | class FatalThrowableError extends FatalErrorException 244 | { 245 | public function __construct(\Throwable $e) 246 | { 247 | if ($e instanceof \ParseError) { 248 | $message = 'Parse error: '.$e->getMessage(); 249 | $severity = E_PARSE; 250 | } elseif ($e instanceof \TypeError) { 251 | $message = 'Type error: '.$e->getMessage(); 252 | $severity = E_RECOVERABLE_ERROR; 253 | } else { 254 | $message = $e->getMessage(); 255 | $severity = E_ERROR; 256 | } 257 | 258 | \ErrorException::__construct( 259 | $message, 260 | $e->getCode(), 261 | $severity, 262 | $e->getFile(), 263 | $e->getLine() 264 | ); 265 | 266 | $this->setTrace($e->getTrace()); 267 | } 268 | } 269 | ``` 270 | 271 | ## 异常 Log 272 | 273 | 当遇到异常情况的时候,`laravel` 首要做的事情就是记录 `log`,这个就是 `report` 函数的作用。 274 | 275 | ```php 276 | protected function getExceptionHandler() 277 | { 278 | return $this->app->make(ExceptionHandler::class); 279 | } 280 | ``` 281 | 282 | `laravel` 在 `Ioc` 容器中默认的异常处理类是 `Illuminate\Foundation\Exceptions\Handler`: 283 | 284 | ```php 285 | class Handler implements ExceptionHandlerContract 286 | { 287 | public function report(Exception $e) 288 | { 289 | if ($this->shouldntReport($e)) { 290 | return; 291 | } 292 | 293 | try { 294 | $logger = $this->container->make(LoggerInterface::class); 295 | } catch (Exception $ex) { 296 | throw $e; // throw the original exception 297 | } 298 | 299 | $logger->error($e); 300 | } 301 | 302 | protected function shouldntReport(Exception $e) 303 | { 304 | $dontReport = array_merge($this->dontReport, [HttpResponseException::class]); 305 | 306 | return ! is_null(collect($dontReport)->first(function ($type) use ($e) { 307 | return $e instanceof $type; 308 | })); 309 | } 310 | } 311 | ``` 312 | 313 | ## 异常页面展示 314 | 315 | 记录 `log` 后,就要将异常转化为页面向开发者展示异常的信息,以便查看问题的来源: 316 | 317 | ```php 318 | protected function renderHttpResponse(Exception $e) 319 | { 320 | $this->getExceptionHandler()->render($this->app['request'], $e)->send(); 321 | } 322 | 323 | class Handler implements ExceptionHandlerContract 324 | { 325 | public function render($request, Exception $e) 326 | { 327 | $e = $this->prepareException($e); 328 | 329 | if ($e instanceof HttpResponseException) { 330 | return $e->getResponse(); 331 | } elseif ($e instanceof AuthenticationException) { 332 | return $this->unauthenticated($request, $e); 333 | } elseif ($e instanceof ValidationException) { 334 | return $this->convertValidationExceptionToResponse($e, $request); 335 | } 336 | 337 | return $this->prepareResponse($request, $e); 338 | } 339 | } 340 | ``` 341 | 对于不同的异常,`laravel` 有不同的处理,大致有 `HttpException`、`HttpResponseException`、`AuthorizationException`、`ModelNotFoundException`、`AuthenticationException`、`ValidationException`。由于特定的不同异常带有自身的不同需求,本文不会特别介绍。本文继续介绍最普通的异常 `HttpException` 的处理: 342 | 343 | ```php 344 | protected function prepareResponse($request, Exception $e) 345 | { 346 | if ($this->isHttpException($e)) { 347 | return $this->toIlluminateResponse($this->renderHttpException($e), $e); 348 | } else { 349 | return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e); 350 | } 351 | } 352 | 353 | protected function renderHttpException(HttpException $e) 354 | { 355 | $status = $e->getStatusCode(); 356 | 357 | view()->replaceNamespace('errors', [ 358 | resource_path('views/errors'), 359 | __DIR__.'/views', 360 | ]); 361 | 362 | if (view()->exists("errors::{$status}")) { 363 | return response()->view("errors::{$status}", ['exception' => $e], $status, $e->getHeaders()); 364 | } else { 365 | return $this->convertExceptionToResponse($e); 366 | } 367 | } 368 | ``` 369 | 370 | 对于 `HttpException` 来说,会根据其错误的状态码,选取不同的错误页面模板,若不存在相关的模板,则会通过 `SymfonyResponse` 来构造异常展示页面: 371 | 372 | ```php 373 | protected function convertExceptionToResponse(Exception $e) 374 | { 375 | $e = FlattenException::create($e); 376 | 377 | $handler = new SymfonyExceptionHandler(config('app.debug')); 378 | 379 | return SymfonyResponse::create($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders()); 380 | } 381 | 382 | protected function toIlluminateResponse($response, Exception $e) 383 | { 384 | if ($response instanceof SymfonyRedirectResponse) { 385 | $response = new RedirectResponse($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all()); 386 | } else { 387 | $response = new Response($response->getContent(), $response->getStatusCode(), $response->headers->all()); 388 | } 389 | 390 | return $response->withException($e); 391 | } 392 | 393 | ``` 394 | 395 | # laravel 错误处理 396 | 397 | ```php 398 | public function handleError($level, $message, $file = '', $line = 0, $context = []) 399 | { 400 | if (error_reporting() & $level) { 401 | throw new ErrorException($message, 0, $level, $file, $line); 402 | } 403 | } 404 | 405 | public function handleShutdown() 406 | { 407 | if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) { 408 | $this->handleException($this->fatalExceptionFromError($error, 0)); 409 | } 410 | } 411 | 412 | protected function fatalExceptionFromError(array $error, $traceOffset = null) 413 | { 414 | return new FatalErrorException( 415 | $error['message'], $error['type'], 0, $error['file'], $error['line'], $traceOffset 416 | ); 417 | } 418 | 419 | protected function isFatal($type) 420 | { 421 | return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]); 422 | } 423 | ``` 424 | 425 | 对于不致命的错误,例如 `notice`级别的错误,`handleError` 即可截取, `laravel` 将错误转化为了异常,交给了 `handleException` 去处理。 426 | 427 | 对于致命错误,例如 `E_PARSE` 解析错误,`handleShutdown` 将会启动,并且判断当前脚本结束是否是由于致命错误,如果是致命错误,将会将其转化为 `FatalErrorException`, 交给了 `handleException` 作为异常去处理。 -------------------------------------------------------------------------------- /Laravel Facade——Facade 门面源码分析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 这篇文章我们开始讲laravel框架中的门面Facade,什么是门面呢?官方文档: 3 | 4 | >   Facades(读音:/fəˈsäd/ )为应用程序的服务容器中可用的类提供了一个「静态」接口。Laravel 自带了很多 facades ,几乎可以用来访问到 Laravel 中所有的服务。Laravel facades 实际上是服务容器中那些底层类的「静态代理」,相比于传统的静态方法, facades 在提供了简洁且丰富的语法同时,还带来了更好的可测试性和扩展性。 5 | 6 |   什么意思呢?首先,我们要知道laravel框架的核心就是个Ioc容器即[服务容器](http://d.laravel-china.org/docs/5.4/container),功能类似于一个工厂模式,是个高级版的工厂。laravel的其他功能例如路由、缓存、日志、数据库其实都是类似于插件或者零件一样,叫做[服务](http://d.laravel-china.org/docs/5.4/providers)。Ioc容器主要的作用就是生产各种零件,就是提供各个服务。在laravel中,如果我们想要用某个服务,该怎么办呢?最简单的办法就是调用服务容器的make函数,或者利用依赖注入,或者就是今天要讲的门面Facade。门面相对于其他方法来说,最大的特点就是简洁,例如我们经常使用的Router,如果利用服务容器的make: 7 | 8 | ```php 9 | App::make('router')->get('/', function () { 10 | return view('welcome'); 11 | }); 12 | ``` 13 | 如果利用门面: 14 | 15 | ```php 16 | Route::get('/', function () { 17 | return view('welcome'); 18 | }); 19 | ``` 20 | 可以看出代码更加简洁。其实,下面我们就会介绍门面最后调用的函数也是服务容器的make函数。 21 | 22 | # Facade的原理 23 |   我们以Route为例,来讲解一下门面Facade的原理与实现。我们先来看Route的门面类: 24 | 25 | ```php 26 | class Route extends Facade 27 | { 28 | protected static function getFacadeAccessor() 29 | { 30 | return 'router'; 31 | } 32 | } 33 | ``` 34 | 35 | 36 |   很简单吧?其实每个门面类也就是重定义一下getFacadeAccessor函数就行了,这个函数返回服务的唯一名称:router。需要注意的是要确保这个名称可以用服务容器的make函数创建成功(App::make('router')),原因我们马上就会讲到。 37 |   那么当我们写出Route::get()这样的语句时,到底发生了什么呢?奥秘就在基类Facade中。 38 | 39 | ```php 40 | public static function __callStatic($method, $args) 41 | { 42 | $instance = static::getFacadeRoot(); 43 | 44 | if (! $instance) { 45 | throw new RuntimeException('A facade root has not been set.'); 46 | } 47 | 48 | return $instance->$method(...$args); 49 | } 50 | ``` 51 |   当运行Route::get()时,发现门面Route没有静态get()函数,PHP就会调用这个魔术函数__callStatic。我们看到这个魔术函数做了两件事:获得对象实例,利用对象调用get()函数。首先先看看如何获得对象实例的: 52 | 53 | ```php 54 | public static function getFacadeRoot() 55 | { 56 | return static::resolveFacadeInstance(static::getFacadeAccessor()); 57 | } 58 | protected static function getFacadeAccessor() 59 | { 60 | throw new RuntimeException('Facade does not implement getFacadeAccessor method.'); 61 | } 62 | protected static function resolveFacadeInstance($name) 63 | { 64 | if (is_object($name)) { 65 | return $name; 66 | } 67 | 68 | if (isset(static::$resolvedInstance[$name])) { 69 | return static::$resolvedInstance[$name]; 70 | } 71 | 72 | return static::$resolvedInstance[$name] = static::$app[$name]; 73 | } 74 | ``` 75 |   我们看到基类getFacadeRoot()调用了getFacadeAccessor(),也就是我们的服务重载的函数,如果调用了基类的getFacadeAccessor,就会抛出异常。在我们的例子里getFacadeAccessor()返回了“router”,接下来getFacadeRoot()又调用了resolveFacadeInstance()。在这个函数里重点就是 76 | 77 | ```php 78 | return static::$resolvedInstance[$name] = static::$app[$name]; 79 | ``` 80 | 我们看到,在这里利用了\$app也就是服务容器创建了“router”,创建成功后放入$resolvedInstance作为缓存,以便以后快速加载。 81 |   好了,Facade的原理到这里就讲完了,但是到这里我们有个疑惑,为什么代码中写Route就可以调用Illuminate\Support\Facades\Route呢?这个就是别名的用途了,很多门面都有自己的别名,这样我们就不必在代码里面写use Illuminate\Support\Facades\Route,而是可以直接用Route了。 82 | 83 | # 别名Aliases 84 |   为什么我们可以在laravel中全局用Route,而不需要使用use Illuminate\Support\Facades\Route?其实奥秘在于一个PHP函数:[class_alias](http://www.php.net/manual/zh/function.class-alias.php),它可以为任何类创建别名。laravel在启动的时候为各个门面类调用了class_alias函数,因此不必直接用类名,直接用别名即可。在config文件夹的app文件里面存放着门面与类名的映射: 85 | 86 | ```php 87 | 'aliases' => [ 88 | 89 | 'App' => Illuminate\Support\Facades\App::class, 90 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, 91 | 'Auth' => Illuminate\Support\Facades\Auth::class, 92 | ... 93 | ] 94 | 95 | ``` 96 |   下面我们来看看laravel是如何为门面类创建别名的。 97 | 98 | ## 启动别名Aliases服务 99 | 100 |   说到laravel的启动,我们离不开index.php: 101 | 102 | ```php 103 | require __DIR__.'/../bootstrap/autoload.php'; 104 | 105 | $app = require_once __DIR__.'/../bootstrap/app.php'; 106 | 107 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 108 | 109 | $response = $kernel->handle( 110 | $request = Illuminate\Http\Request::capture() 111 | ); 112 | ... 113 | ``` 114 |   第一句就是我们前面博客说的composer的自动加载,接下来第二句获取laravel核心的Ioc容器,第三句“制造”出Http请求的内核,第四句是我们这里的关键,这句牵扯很大,laravel里面所有功能服务的注册加载,乃至Http请求的构造与传递都是这一句的功劳。 115 | 116 | ```php 117 | $request = Illuminate\Http\Request::capture() 118 | ``` 119 | 120 |   这句是laravel通过全局$\_SERVER数组构造一个Http请求的语句,接下来会调用Http的内核函数handle: 121 | 122 | ```php 123 | public function handle($request) 124 | { 125 | try { 126 | $request->enableHttpMethodParameterOverride(); 127 | 128 | $response = $this->sendRequestThroughRouter($request); 129 | } catch (Exception $e) { 130 | $this->reportException($e); 131 | 132 | $response = $this->renderException($request, $e); 133 | } catch (Throwable $e) { 134 | $this->reportException($e = new FatalThrowableError($e)); 135 | 136 | $response = $this->renderException($request, $e); 137 | } 138 | 139 | event(new Events\RequestHandled($request, $response)); 140 | 141 | return $response; 142 | } 143 | ``` 144 | 145 |   在handle函数方法中enableHttpMethodParameterOverride函数是允许在表单中使用delete、put等类型的请求。我们接着看sendRequestThroughRouter: 146 | 147 | ```php 148 | protected function sendRequestThroughRouter($request) 149 | { 150 | $this->app->instance('request', $request); 151 | 152 | Facade::clearResolvedInstance('request'); 153 | 154 | $this->bootstrap(); 155 | 156 | return (new Pipeline($this->app)) 157 | ->send($request) 158 | ->through($this->app->shouldSkipMiddleware() ? [] : 159 | $this->middleware) 160 | ->then($this->dispatchToRouter()); 161 | } 162 | ``` 163 |   前两句是在laravel的Ioc容器设置request请求的对象实例,Facade中清楚request的缓存实例。bootstrap: 164 | 165 | ```php 166 | public function bootstrap() 167 | { 168 | if (! $this->app->hasBeenBootstrapped()) { 169 | $this->app->bootstrapWith($this->bootstrappers()); 170 | } 171 | } 172 | 173 | protected $bootstrappers = [ 174 | \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, 175 | \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, 176 | \Illuminate\Foundation\Bootstrap\HandleExceptions::class, 177 | \Illuminate\Foundation\Bootstrap\RegisterFacades::class, 178 | \Illuminate\Foundation\Bootstrap\RegisterProviders::class, 179 | \Illuminate\Foundation\Bootstrap\BootProviders::class, 180 | ]; 181 | ``` 182 |   $bootstrappers是Http内核里专门用于启动的组件,bootstrap函数中调用Ioc容器的bootstrapWith函数来创建这些组件并利用组件进行启动服务。app->bootstrapWith: 183 | 184 | ```php 185 | public function bootstrapWith(array $bootstrappers) 186 | { 187 | $this->hasBeenBootstrapped = true; 188 | 189 | foreach ($bootstrappers as $bootstrapper) { 190 | $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]); 191 | 192 | $this->make($bootstrapper)->bootstrap($this); 193 | 194 | $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]); 195 | } 196 | } 197 | ``` 198 |   可以看到bootstrapWith函数也就是利用Ioc容器创建各个启动服务的实例后,回调启动自己的函数bootstrap,在这里我们只看我们Facade的启动组件 199 | 200 | ```php 201 | \Illuminate\Foundation\Bootstrap\RegisterFacades::class 202 | ``` 203 | RegisterFacades的bootstrap函数: 204 | 205 | ```php 206 | class RegisterFacades 207 | { 208 | public function bootstrap(Application $app) 209 | { 210 | Facade::clearResolvedInstances(); 211 | 212 | Facade::setFacadeApplication($app); 213 | 214 | AliasLoader::getInstance($app->make('config')->get('app.aliases', [])) 215 | ->register(); 216 | } 217 | } 218 | ``` 219 |   可以看出来,bootstrap做了一下几件事: 220 | > 1. 清除了Facade中的缓存 221 | > 2. 设置Facade的Ioc容器 222 | > 3. 获得我们前面讲的config文件夹里面app文件aliases别名映射数组 223 | > 4. 使用aliases实例化初始化AliasLoader 224 | > 5. 调用AliasLoader->register() 225 | 226 | ```php 227 | public function register() 228 | { 229 | if (! $this->registered) { 230 | $this->prependToLoaderStack(); 231 | 232 | $this->registered = true; 233 | } 234 | } 235 | 236 | protected function prependToLoaderStack() 237 | { 238 | spl_autoload_register([$this, 'load'], true, true); 239 | } 240 | ``` 241 |   我们可以看出,别名服务的启动关键就是这个spl_autoload_register,这个函数我们应该很熟悉了,在自动加载中这个函数用于解析命名空间,在这里用于解析别名的真正类名。 242 | 243 | ## 别名Aliases服务 244 | 245 |   我们首先来看看被注册到spl_autoload_register的函数,load: 246 | 247 | ```php 248 | public function load($alias) 249 | { 250 | if (static::$facadeNamespace && strpos($alias, 251 | static::$facadeNamespace) === 0) { 252 | $this->loadFacade($alias); 253 | 254 | return true; 255 | } 256 | 257 | if (isset($this->aliases[$alias])) { 258 | return class_alias($this->aliases[$alias], $alias); 259 | } 260 | } 261 | ``` 262 |   这个函数的下面很好理解,就是class_alias利用别名映射数组将别名映射到真正的门面类中去,但是上面这个是什么呢?实际上,这个是laravel5.4版本新出的功能叫做实时门面服务。 263 | 264 | ## 实时门面服务 265 | 266 |   其实门面功能已经很简单了,我们只需要定义一个类继承Facade即可,但是laravel5.4打算更近一步——自动生成门面子类,这就是实时门面。 267 |   实时门面怎么用?看下面的例子: 268 | 269 | ```php 270 | namespace App\Services; 271 | 272 | class PaymentGateway 273 | { 274 | protected $tax; 275 | 276 | public function __construct(TaxCalculator $tax) 277 | { 278 | $this->tax = $tax; 279 | } 280 | } 281 | ``` 282 | 这是一个自定义的类,如果我们想要为这个类定义一个门面,在laravel5.4我们可以这么做: 283 | 284 | ```php 285 | use Facades\ { 286 | App\Services\PaymentGateway 287 | }; 288 | 289 | Route::get('/pay/{amount}', function ($amount) { 290 | PaymentGateway::pay($amount); 291 | }); 292 | ``` 293 |   当然如果你愿意,你还可以在alias数组为门面添加一个别名映射"PaymentGateway" => "use Facades\App\Services\PaymentGateway",这样就不用写这么长的名字了。 294 |   那么这么做的原理是什么呢?我们接着看源码: 295 | 296 | ```php 297 | protected static $facadeNamespace = 'Facades\\'; 298 | if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) { 299 | $this->loadFacade($alias); 300 | 301 | return true; 302 | } 303 | ``` 304 |   如果命名空间是以Facades\\开头的,那么就会调用实时门面的功能,调用loadFacade函数: 305 | 306 | ```php 307 | protected function loadFacade($alias) 308 | { 309 | tap($this->ensureFacadeExists($alias), function ($path) { 310 | require $path; 311 | }); 312 | } 313 | ``` 314 |   [tap](https://segmentfault.com/a/1190000008447747)是laravel的全局帮助函数,ensureFacadeExists函数负责自动生成门面类,loadFacade负责加载门面类: 315 | 316 | ```php 317 | protected function ensureFacadeExists($alias) 318 | { 319 | if (file_exists($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) { 320 | return $path; 321 | } 322 | 323 | file_put_contents($path, $this->formatFacadeStub( 324 | $alias, file_get_contents(__DIR__.'/stubs/facade.stub') 325 | )); 326 | 327 | return $path; 328 | } 329 | ``` 330 |   可以看出来,laravel框架生成的门面类会放到stroge/framework/cache/文件夹下,名字以facade开头,以命名空间的哈希结尾。如果存在这个文件就会返回,否则就要利用file_put_contents生成这个文件,formatFacadeStub: 331 | 332 | ```php 333 | protected function formatFacadeStub($alias, $stub) 334 | { 335 | $replacements = [ 336 | str_replace('/', '\\', dirname(str_replace('\\', '/', $alias))), 337 | class_basename($alias), 338 | substr($alias, strlen(static::$facadeNamespace)), 339 | ]; 340 | 341 | return str_replace( 342 | ['DummyNamespace', 'DummyClass', 'DummyTarget'], $replacements, $stub 343 | ); 344 | } 345 | ``` 346 | 简单的说,对于Facades\App\Services\PaymentGateway,$replacements第一项是门面命名空间,将Facades\App\Services\PaymentGateway转为Facades/App/Services/PaymentGateway,取前面Facades/App/Services/,再转为命名空间Facades\App\Services\;第二项是门面类名,PaymentGateway;第三项是门面类的服务对象,App\Services\PaymentGateway,用这些来替换门面的模板文件: 347 | 348 | ```php 349 | resource('prefix/foos', 'FooController'); 35 | 36 | $this->assertEquals('prefix/foos/{foo}', $routes[3]->uri()); 37 | ``` 38 | 39 | ## 双参数 `RESTFul` 路由 40 | 41 | `laravel` 允许定义拥有两个参数的 `RESTFul` 路由: 42 | 43 | ```php 44 | $router->resource('foos.bars', 'FooController'); 45 | 46 | $this->assertEquals('foos/{foo}/bars/{bar}', $routes[3]->uri()); 47 | ``` 48 | 49 | ## 参数自定义命名 50 | 51 | 一般来说,`RESTFul` 路由的参数命名规则是路由单数,符号 `-` 转为 `_`,例如下面例子中 `bars`,和 `foo-baz`。 52 | 53 | ```php 54 | $router->resource('foos', 'FooController'); 55 | $this->assertEquals('foos/{foo}', $routes[3]->uri()); 56 | 57 | $router->resource('foo-bar.foo-baz', 'FooController', ['only' => ['show']]); 58 | $this->assertEquals('foo-bar/{foo_bar}/foo-baz/{foo_baz}', $routes[0]->uri()); 59 | ``` 60 | 61 | 我们可以利用 `parameters ` 强制这种单数模式: 62 | 63 | ```php 64 | $router->resource('foos', 'FooController', ['parameters' => 'singular']); 65 | $this->assertEquals('foos/{foo}', $routes[3]->uri()); 66 | ``` 67 | 我们也可以利用 `singularParameters` 来强制: 68 | 69 | ```php 70 | ResourceRegistrar::singularParameters(true); 71 | 72 | $router->resource('foos', 'FooController', ['parameters' => 'singular']); 73 | $this->assertEquals('foos/{foo}', $routes[3]->uri()); 74 | ``` 75 | 我们还可以不使用单数,利用 `parameters ` 用自己自定义的名字来定义参数: 76 | 77 | ```php 78 | $router->resource('bars.foos.bazs', 'FooController', ['parameters' => ['foos' => 'oof', 'bazs' => 'b']]); 79 | 80 | $this->assertEquals('bars/{bar}/foos/{oof}/bazs/{b}', $routes[3]->uri()); 81 | ``` 82 | 83 | 同时,我们仍然可以利用 `setParameters` 函数来自定义参数命名: 84 | 85 | ```php 86 | ResourceRegistrar::setParameters(['foos' => 'oof', 'bazs' => 'b']); 87 | 88 | $router->resource('bars.foos.bazs', 'FooController'); 89 | $this->assertEquals('bars/{bar}/foos/{oof}/bazs/{b}', $routes[3]->uri()); 90 | 91 | ``` 92 | 93 | ## `RESTFul` 路由动词控制 94 | 95 | `laravel` 为 `RESTFul` 路由生成了两个带有动词的路由: `create` 、 `edit`,分别用于加载订单的创建页面与编辑页面,这两个动词 `laravel` 是允许修改的: 96 | 97 | ```php 98 | ResourceRegistrar::verbs([ 99 | 'create' => 'ajouter', 100 | 'edit' => 'modifier', 101 | ]); 102 | 103 | $router->resource('foo', 'FooController'); 104 | $routes = $router->getRoutes(); 105 | 106 | $this->assertEquals('foo/ajouter', $routes->getByName('foo.create')->uri()); 107 | $this->assertEquals('foo/{foo}/modifier', $routes->getByName('foo.edit')->uri()); 108 | ``` 109 | 110 | ## 控制器方法约束 111 | 112 | 一般情况下,我们都会一次性想要上面所生成的七个路由,然而,有时候,我们只需要其中几个,或者不想要其中几个。这时候就可以利用 `only` 或者 `except`: 113 | 114 | ```php 115 | $router = $this->getRouter(); 116 | $router->resource('foo', 'FooController', ['only' => ['show', 'destroy']]); 117 | $routes = $router->getRoutes(); 118 | 119 | $this->assertCount(2, $routes); 120 | ``` 121 | 122 | ```php 123 | $router = $this->getRouter(); 124 | $router->resource('foo', 'FooController', ['except' => ['show', 'destroy']]); 125 | $routes = $router->getRoutes(); 126 | 127 | $this->assertCount(5, $routes); 128 | ``` 129 | 130 | ## `RESTFul` 路由名称自定义 131 | 132 | `RESTFul` 路由的每个路由都要自己默认的路由名称,`laravel` 允许我们对路由名称进行修改: 133 | 134 | 我们可以用 `as` 来为路由名称添加前缀: 135 | 136 | ```php 137 | $router->resource('foo-bars', 'FooController', ['only' => ['show'], 'as' => 'prefix']); 138 | 139 | $this->assertEquals('prefix.foo-bars.show', $routes[0]->getName()); 140 | ``` 141 | 当有多个路由参数的时候,路由参数默认添加到了路由名称中: 142 | 143 | ```php 144 | $router->resource('prefix/foo.bar', 'FooController'); 145 | 146 | $this->assertTrue($router->getRoutes()->hasNamedRoute('foo.bar.index')); 147 | ``` 148 | 可以利用 `names` 为单个路由来命名: 149 | 150 | ```php 151 | $router->resource('foo', 'FooController', ['names' => [ 152 | 'index' => 'foo', 153 | 'show' => 'bar', 154 | ]]); 155 | 156 | $this->assertTrue($router->getRoutes()->hasNamedRoute('foo')); 157 | $this->assertTrue($router->getRoutes()->hasNamedRoute('bar')); 158 | ``` 159 | 还可以利用 `names` 为所有路由来命名: 160 | 161 | ```php 162 | $router->resource('foo', 'FooController', ['names' => 'bar']); 163 | 164 | $this->assertTrue($router->getRoutes()->hasNamedRoute('bar.index')); 165 | ``` 166 | 167 | # `RESTFul` 路由源码分析 168 | 169 | `RESTFul` 路由的创建工作由类 `ResourceRegistrar` 负责,这个类为默认为用户创建七个路由,函数方法 `register ` 是创建路由的主函数: 170 | 171 | ```php 172 | class ResourceRegistrar 173 | { 174 | public function register($name, $controller, array $options = []) 175 | { 176 | if (isset($options['parameters']) && ! isset($this->parameters)) { 177 | $this->parameters = $options['parameters']; 178 | } 179 | 180 | if (Str::contains($name, '/')) { 181 | $this->prefixedResource($name, $controller, $options); 182 | 183 | return; 184 | } 185 | 186 | $base = $this->getResourceWildcard(last(explode('.', $name))); 187 | 188 | $defaults = $this->resourceDefaults; 189 | 190 | foreach ($this->getResourceMethods($defaults, $options) as $m) { 191 | $this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options); 192 | } 193 | } 194 | } 195 | ``` 196 | 197 | 这个函数主要流程分为三段: 198 | 199 | - 判断是否由前缀 200 | - 获取路由的基础参数 201 | - 添加路由 202 | 203 | ## 拥有前缀的 `RESTFul` 路由 204 | 205 | 如果我们为 `RESTFul` 路由添加了前缀,那么 `laravel` 将会以 `group` 的形式添加路由: 206 | 207 | ```php 208 | protected function prefixedResource($name, $controller, array $options) 209 | { 210 | list($name, $prefix) = $this->getResourcePrefix($name); 211 | 212 | $callback = function ($me) use ($name, $controller, $options) { 213 | $me->resource($name, $controller, $options); 214 | }; 215 | 216 | return $this->router->group(compact('prefix'), $callback); 217 | } 218 | 219 | protected function getResourcePrefix($name) 220 | { 221 | $segments = explode('/', $name); 222 | 223 | $prefix = implode('/', array_slice($segments, 0, -1)); 224 | 225 | return [end($segments), $prefix]; 226 | } 227 | ``` 228 | 229 | ## 获取基础 `RESTFul` 路由参数 230 | 231 | 在添加各种路由之前,我们需要先获取路由的基础参数,也就是当存在多参数情况下,最后的参数。获取参数后,如果用户有自定义命名,则获取自定义命名: 232 | 233 | ```php 234 | public function getResourceWildcard($value) 235 | { 236 | if (isset($this->parameters[$value])) { 237 | $value = $this->parameters[$value]; 238 | } elseif (isset(static::$parameterMap[$value])) { 239 | $value = static::$parameterMap[$value]; 240 | } elseif ($this->parameters === 'singular' || static::$singularParameters) { 241 | $value = Str::singular($value); 242 | } 243 | 244 | return str_replace('-', '_', $value); 245 | } 246 | ``` 247 | 248 | ## 添加各种路由 249 | 250 | 添加路由主要有三个步骤: 251 | 252 | - 计算路由 `uri` 253 | - 获取路由属性 254 | - 创建路由 255 | 256 | ```php 257 | protected function addResourceIndex($name, $base, $controller, $options) 258 | { 259 | $uri = $this->getResourceUri($name); 260 | 261 | $action = $this->getResourceAction($name, $controller, 'index', $options); 262 | 263 | return $this->router->get($uri, $action); 264 | } 265 | ``` 266 | 267 | 当计算路由 `uri` 时,由于存在多参数的情况,需要循环计算路由参数: 268 | 269 | ```php 270 | public function getResourceUri($resource) 271 | { 272 | if (! Str::contains($resource, '.')) { 273 | return $resource; 274 | } 275 | 276 | $segments = explode('.', $resource); 277 | 278 | $uri = $this->getNestedResourceUri($segments); 279 | 280 | return str_replace('/{'.$this->getResourceWildcard(end($segments)).'}', '', $uri); 281 | } 282 | 283 | protected function getNestedResourceUri(array $segments) 284 | { 285 | return implode('/', array_map(function ($s) { 286 | return $s.'/{'.$this->getResourceWildcard($s).'}'; 287 | }, $segments)); 288 | } 289 | ``` 290 | 当计算路由的属性时,最重要的是获取路由的名字,路由的名字可以是默认,也可以是用户利用 `names` 或者 `as` 属性来自定义: 291 | 292 | ```php 293 | protected function getResourceAction($resource, $controller, $method, $options) 294 | { 295 | $name = $this->getResourceRouteName($resource, $method, $options); 296 | 297 | $action = ['as' => $name, 'uses' => $controller.'@'.$method]; 298 | 299 | if (isset($options['middleware'])) { 300 | $action['middleware'] = $options['middleware']; 301 | } 302 | 303 | return $action; 304 | } 305 | 306 | protected function getResourceRouteName($resource, $method, $options) 307 | { 308 | $name = $resource; 309 | 310 | if (isset($options['names'])) { 311 | if (is_string($options['names'])) { 312 | $name = $options['names']; 313 | } elseif (isset($options['names'][$method])) { 314 | return $options['names'][$method]; 315 | } 316 | } 317 | 318 | $prefix = isset($options['as']) ? $options['as'].'.' : ''; 319 | 320 | return trim(sprintf('%s%s.%s', $prefix, $name, $method), '.'); 321 | } 322 | ``` 323 | 324 | 值得注意的是,如果单独为某一个方法命名,那么直接回返回命名,而不会受 `as` 和方法名 'method' 的影响。 -------------------------------------------------------------------------------- /Laravel HTTP——Pipeline中间件处理源码分析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 当所有的路由都加载完毕后,就会根据请求的 `url` 来将请求分发到对应的路由上去。然而,在分发到路由之前还要经过各种中间件的计算。`laravel` 利用装饰者模式来实现中间件的功能。 4 | 5 | # 从原始装饰者模式到闭包装饰者 6 | 7 | 装饰者模式是设计模式的一种,主要进行对象的多次处理与过滤,是在开放-关闭原则下实现动态添加或减少功能的一种方式。下面先看一个装饰者模式的例子: 8 | 9 | 总共有两种咖啡:Decaf、Espresso,另有两种调味品:Mocha、Whip(3种设计的主要差别在于抽象方式不同) 10 | 11 | 装饰模式分为3个部分: 12 | 13 | 1,抽象组件 -- 对应Coffee类 14 | 15 | 2,具体组件 -- 对应具体的咖啡,如:Decaf,Espresso 16 | 17 | 3,装饰者 -- 对应调味品,如:Mocha,Whip 18 | 19 | ## 原始装饰者模式 20 | 21 | ```php 22 | public interface Coffee 23 | { 24 | public double cost(); 25 | } 26 | 27 | public class Espresso implements Coffee 28 | { 29 | public double cost() 30 | { 31 | return 2.5; 32 | } 33 | } 34 | 35 | public class Dressing implements Coffee 36 | { 37 | private Coffee coffee; 38 | 39 | public Dressing(Coffee coffee) 40 | { 41 | this.coffee = coffee; 42 | } 43 | 44 | public double cost() 45 | { 46 | return coffee.cost(); 47 | } 48 | } 49 | 50 | public class Whip extends Dressing { 51 | public Whip(Coffee coffee) 52 | { 53 | super(coffee); 54 | } 55 | 56 | public double cost() 57 | { 58 | return super.cost() + 0.1; 59 | } 60 | } 61 | 62 | public class Mocha extends Dressing 63 | { 64 | public Mocha(Coffee coffee) 65 | { 66 | super(coffee); 67 | } 68 | 69 | public double cost() 70 | { 71 | return super.cost() + 0.5; 72 | } 73 | } 74 | ``` 75 | 76 | 当我们使用装饰者模式的时候: 77 | 78 | ```php 79 | public class Test { 80 | public static void main(String[] args) { 81 | Coffee coffee = new Espresso(); 82 | coffee = new Mocha(coffee); 83 | coffee = new Mocha(coffee); 84 | coffee = new Whip(coffee); 85 | //3.6(2.5 + 0.5 + 0.5 + 0.1) 86 | System.out.println(coffee.cost()); 87 | } 88 | } 89 | ``` 90 | 91 | 我们可以看出来,装饰者模式就是利用装饰者类来对具体类不断的进行多层次的处理,首先我们创建了 `Espresso` 类,然后第一次利用 `Mocha` 装饰者对 `Espresso` 咖啡加了摩卡,第二次重复加了摩卡,第三次利用装饰者 `Whip` 对 `Espresso` 咖啡加了奶油。每次加入新的调料,装饰者都会对价格 `cost` 做一些处理(+0.1、+0.5)。 92 | 93 | ## 无构造函数的装饰者 94 | 95 | 我们对这个装饰者进行一些改造: 96 | 97 | ```php 98 | public class Espresso 99 | { 100 | double cost; 101 | 102 | public double cost() 103 | { 104 | $this-> cost = 2.5; 105 | } 106 | } 107 | 108 | public class Dressing 109 | { 110 | public double cost(Espresso $espresso) 111 | { 112 | return ($espresso); 113 | } 114 | } 115 | 116 | public class Whip extends Dressing 117 | { 118 | public double cost(Espresso $espresso) 119 | { 120 | $espresso->cost = espresso->cost() + 0.1; 121 | 122 | return ($espresso); 123 | } 124 | } 125 | 126 | public class Mocha extends Dressing 127 | { 128 | public double cost(Espresso $espresso) 129 | { 130 | $espresso->cost = espresso->cost() + 0.5; 131 | 132 | return ($espresso); 133 | } 134 | } 135 | ``` 136 | 137 | ```php 138 | public class Test { 139 | public static void main(String[] args) { 140 | Coffee $coffee = new Espresso(); 141 | 142 | $coffee = (new Mocha())->cost($coffee); 143 | $coffee = (new Mocha())->cost($coffee); 144 | $coffee = (new Whip())->cost($coffee); 145 | 146 | //3.6(2.5 + 0.5 + 0.5 + 0.1) 147 | System.out.println(coffee.cost()); 148 | } 149 | } 150 | ``` 151 | 152 | 改造后,装饰者类通过函数 `cost` 来注入具体类 `caffee`,而不是通过构造函数,这样做有助于自动化进行装饰处理。我们改造后发现,想要对具体类通过装饰类进行处理,需要不断的调用 `cost` 函数,如果有10个装饰操作,就要手动写10个语句,因此我们继续进行改造: 153 | 154 | ## 闭包装饰者模式 155 | 156 | ```php 157 | public class Espresso 158 | { 159 | double cost; 160 | 161 | public double cost() 162 | { 163 | $this-> cost = 2.5; 164 | } 165 | } 166 | 167 | public class Dressing 168 | { 169 | public double cost(Espresso $espresso, Closure $closure) 170 | { 171 | return ($espresso); 172 | } 173 | } 174 | 175 | public class Whip extends Dressing 176 | { 177 | public double cost(Espresso $espresso, Closure $closure) 178 | { 179 | $espresso->cost = espresso->cost() + 0.1; 180 | 181 | return $closure($espresso); 182 | } 183 | } 184 | 185 | public class Mocha extends Dressing 186 | { 187 | public double cost(Espresso $espresso, Closure $closure) 188 | { 189 | $espresso->cost = espresso->cost() + 0.5; 190 | 191 | return $closure($espresso); 192 | } 193 | } 194 | ``` 195 | 196 | ```php 197 | public class Test { 198 | public static void main(String[] args) { 199 | Coffee $coffee = new Espresso(); 200 | 201 | $fun = function($coffee,$fuc,$dressing) { 202 | $dressing->cost($coffee, $fuc); 203 | } 204 | 205 | 206 | $fuc0 = function($coffee) { 207 | return $coffee; 208 | }; 209 | 210 | $fuc1 = function($coffee) use ($fuc0, $dressing = (new Mocha(),$fun)) { 211 | return $fun($coffee, $fuc0, $dressing); 212 | } 213 | 214 | $fuc2 = function($coffee) use ($fuc1, $dressing = (new Mocha(),$fun)) { 215 | return $fuc($coffee, $fun1, $dressing); 216 | } 217 | 218 | $fuc3 = function($coffee) use ($fuc2, $dressing = (new Whip(),$fun)) { 219 | return $fuc($coffee, $fun2, $dressing); 220 | } 221 | 222 | $coffee = $fun3($coffee); 223 | 224 | //3.6(2.5 + 0.5 + 0.5 + 0.1) 225 | System.out.println(coffee.cost()); 226 | } 227 | } 228 | ``` 229 | 230 | 在这次改造中,我们使用了闭包函数,这样做的目的在于,我们只需要最后一句 `$fun3($coffee)`,就可以启动整个装饰链条。 231 | 232 | ## 闭包装饰者的抽象化 233 | 234 | 然而这种改造还不够深入,因为我们还可以把 `$fuc1`、`$fuc2`、`$fuc3` 继续抽象化为一个闭包函数,这个闭包函数仅仅是参数 `$fuc`、`$dressing` 每次不同,`$coffee` 相同,因此改造如下: 235 | 236 | ```php 237 | public class Test { 238 | public static void main(String[] args) { 239 | Coffee $coffee = new Espresso(); 240 | 241 | $fun = function($coffee) use ($fuc,$dressing) { 242 | $dressing->cost($coffee, $fuc); 243 | } 244 | 245 | $fuc = function($fuc,$dressing) use ($fun) { 246 | return $fun; 247 | }; 248 | 249 | 250 | $fuc0 = function($coffee) { 251 | return $coffee; 252 | }; 253 | 254 | $fuc1 = $fuc($fuc0, (new Mocha()); 255 | 256 | $fuc2 = $fuc($fuc1, (new Mocha()); 257 | 258 | $fuc3 = $fuc($fuc2, (new Whip()); 259 | 260 | $coffee = $fun3($coffee); 261 | 262 | //3.6(2.5 + 0.5 + 0.5 + 0.1) 263 | System.out.println(coffee.cost()); 264 | } 265 | } 266 | ``` 267 | 268 | 这次,我们把之前的闭包分为两个部分,`$fun` 负责具体类的参数传递,`$fuc`负责装饰者和闭包函数的参数传递。在最后一句 `$fun3`,只需要传递一个具体类,就可以启动整个装饰链条。 269 | 270 | ## 闭包装饰者的自动化 271 | 272 | 到这里,我们还有一件事没有完成,那就是 `$fuc1`、`$fuc2`、`$fuc3` 这些闭包的构建还是手动的,我们需要将这个过程改为自动的: 273 | 274 | ```php 275 | public class Test { 276 | public static void main(String[] args) { 277 | Coffee $coffee = new Espresso(); 278 | 279 | $fun = function($coffee) use ($fuc,$dressing) { 280 | $dressing->cost($coffee, $fuc); 281 | } 282 | 283 | $fuc = function($fuc,$dressing) use ($fun) { 284 | return $fun; 285 | }; 286 | 287 | 288 | $fuc0 = function($coffee) { 289 | return $coffee; 290 | }; 291 | 292 | $fucn = array_reduce( 293 | [(new Mocha(),(new Mocha(),(new Whip()], $fuc, $fuc0 294 | ); 295 | 296 | $coffee = $fucn($coffee); 297 | 298 | //3.6(2.5 + 0.5 + 0.5 + 0.1) 299 | System.out.println(coffee.cost()); 300 | } 301 | } 302 | ``` 303 | 304 | # laravel的闭包装饰者——Pipeline 305 | 306 | 上一章我们说到了路由的注册启动与加载过程,这个过程由 `bootstrap()` 完成。当所有的路由加载完毕后,就要进行各种中间件的处理了: 307 | 308 | ```php 309 | protected function sendRequestThroughRouter($request) 310 | { 311 | $this->app->instance('request', $request); 312 | 313 | Facade::clearResolvedInstance('request'); 314 | 315 | $this->bootstrap(); 316 | 317 | return (new Pipeline($this->app)) 318 | ->send($request) 319 | ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) 320 | ->then($this->dispatchToRouter()); 321 | } 322 | 323 | public function shouldSkipMiddleware() 324 | { 325 | return $this->bound('middleware.disable') && 326 | $this->make('middleware.disable') === true; 327 | } 328 | ``` 329 | 330 | `laravel` 的中间件处理由 `Pipeline` 来完成,它是一个闭包装饰者模式,其中 331 | 332 | * `request` 是具体类,相当于我们上面的 `caffee` 类; 333 | * `middleware` 中间件是装饰者类,相当于上面的 `dressing` 类; 334 | 335 | 我们先看看这个类内部的代码: 336 | 337 | ```php 338 | class Pipeline implements PipelineContract 339 | { 340 | public function __construct(Container $container = null) 341 | { 342 | $this->container = $container; 343 | } 344 | 345 | public function send($passable) 346 | { 347 | $this->passable = $passable; 348 | 349 | return $this; 350 | } 351 | 352 | public function through($pipes) 353 | { 354 | $this->pipes = is_array($pipes) ? $pipes : func_get_args(); 355 | 356 | return $this; 357 | } 358 | 359 | public function then(Closure $destination) 360 | { 361 | $pipeline = array_reduce( 362 | array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination) 363 | ); 364 | 365 | return $pipeline($this->passable); 366 | } 367 | 368 | protected function prepareDestination(Closure $destination) 369 | { 370 | return function ($passable) use ($destination) { 371 | return $destination($passable); 372 | }; 373 | } 374 | 375 | protected function carry() 376 | { 377 | return function ($stack, $pipe) { 378 | return function ($passable) use ($stack, $pipe) { 379 | if ($pipe instanceof Closure) { 380 | return $pipe($passable, $stack); 381 | } elseif (! is_object($pipe)) { 382 | list($name, $parameters) = $this->parsePipeString($pipe); 383 | 384 | $pipe = $this->getContainer()->make($name); 385 | 386 | $parameters = array_merge([$passable, $stack], $parameters); 387 | } else { 388 | $parameters = [$passable, $stack]; 389 | } 390 | 391 | return $pipe->{$this->method}(...$parameters); 392 | }; 393 | }; 394 | } 395 | } 396 | ``` 397 | 398 | `pipeline` 的构造和我们上面所讲的闭包装饰者相同,我们着重来看 `carry()` 函数的代码: 399 | 400 | ```php 401 | function ($stack, $pipe) { 402 | ... 403 | } 404 | ``` 405 | 406 | 最外层的闭包相当于上个章节的 `$fuc`, 407 | 408 | ```php 409 | function ($passable) use ($stack, $pipe) { 410 | ... 411 | } 412 | ``` 413 | 414 | 里面的这一层比闭包型党与上个章节的 `$fun`, 415 | 416 | `prepareDestination` 这个函数相当于上面的 `$fuc0`, 417 | 418 | ```php 419 | if ($pipe instanceof Closure) { 420 | return $pipe($passable, $stack); 421 | } elseif (! is_object($pipe)) { 422 | list($name, $parameters) = $this->parsePipeString($pipe); 423 | 424 | $pipe = $this->getContainer()->make($name); 425 | 426 | $parameters = array_merge([$passable, $stack], $parameters); 427 | } else { 428 | $parameters = [$passable, $stack]; 429 | } 430 | 431 | return $pipe->{$this->method}(...$parameters); 432 | ``` 433 | 434 | 这一部分相当于上个章节的 `$dressing->cost($coffee, $fuc);`,这部分主要解析中间件 `handle()` 函数的参数: 435 | 436 | ```php 437 | public function via($method) 438 | { 439 | $this->method = $method; 440 | 441 | return $this; 442 | } 443 | 444 | protected function parsePipeString($pipe) 445 | { 446 | list($name, $parameters) = array_pad(explode(':', $pipe, 2), 2, []); 447 | 448 | if (is_string($parameters)) { 449 | $parameters = explode(',', $parameters); 450 | } 451 | 452 | return [$name, $parameters]; 453 | } 454 | ``` 455 | 456 | 这样,`laravel` 就实现了中间件对 `request` 的层层处理。 457 | 458 | -------------------------------------------------------------------------------- /Laravel HTTP——SubstituteBindings 参数绑定中间件的使用与源码解析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 当路由与请求进行正则匹配后,各个路由的参数就获得了它们各自的数值。然而,有些路由参数变量,我们还想要把它转化为特定的对象,这时候就需要中间件的帮助。 `SubstituteBindings` 中间件就是一个将路由参数转化为特定对象的组件,它默认可以将特定名称的路由参数转化数据库模型对象,可以转化已绑定的路由参数为把绑定的对象。 4 | 5 | # `SubstituteBindings` 中间件的使用 6 | 7 | ## 数据库模型隐性转化 8 | 9 | 首先我们定义了一个带有路由参数的路由: 10 | 11 | ```php 12 | Route::put('user/{userid}', 'UserController@update'); 13 | ``` 14 | 15 | 然后我们在路由的控制器方法中或者路由闭包函数中定义一个数据库模型类型的参数,这个参数名与路由参数相同: 16 | 17 | ```php 18 | class UserController extends Controller 19 | { 20 | public function update(UserModel $userid) 21 | { 22 | $userid->name = 'taylor'; 23 | $userid->update(); 24 | } 25 | } 26 | ``` 27 | 28 | 这时,路由的参数会被中间件隐性地转化为 `UserModel`,且模型变量 `$userid` 的主键值为参数变量 `{userid}` 正则匹配后的数值。 29 | 30 | 综合测试样例: 31 | 32 | ```php 33 | public function testImplicitBindingsWithOptionalParameter() 34 | { 35 | unset($_SERVER['__test.controller_callAction_parameters']); 36 | $router->get(($str = str_random()).'/{user}/{defaultNull?}/{team?}', [ 37 | 'middleware' => SubstituteBindings::class, 38 | 'uses' => 'Illuminate\Tests\Routing\RouteTestAnotherControllerWithParameterStub@withModels', 39 | ]); 40 | 41 | $router->dispatch(Request::create($str.'/1', 'GET')); 42 | 43 | $values = array_values($_SERVER['__test.controller_callAction_parameters']); 44 | 45 | $this->assertEquals(1, $values[0]->value); 46 | } 47 | 48 | 49 | class RouteTestAnotherControllerWithParameterStub extends Controller 50 | { 51 | public function callAction($method, $parameters) 52 | { 53 | $_SERVER['__test.controller_callAction_parameters'] = $parameters; 54 | } 55 | 56 | public function withModels(RoutingTestUserModel $user) 57 | { 58 | } 59 | } 60 | 61 | class RoutingTestUserModel extends Model 62 | { 63 | public function getRouteKeyName() 64 | { 65 | return 'id'; 66 | } 67 | 68 | public function where($key, $value) 69 | { 70 | $this->value = $value; 71 | 72 | return $this; 73 | } 74 | 75 | public function first() 76 | { 77 | return $this; 78 | } 79 | 80 | public function firstOrFail() 81 | { 82 | return $this; 83 | } 84 | } 85 | 86 | ``` 87 | 88 | ## 路由显示绑定 89 | 90 | 除了隐示地转化路由参数外,我们还可以给路由参数显示提供绑定。显示绑定有 `bind`、`model` 两种方法。 91 | 92 | - 通过 `bind` 为参数绑定闭包函数: 93 | 94 | ```php 95 | public function testRouteBinding() 96 | { 97 | $router = $this->getRouter(); 98 | $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) { 99 | return $name; 100 | }]); 101 | $router->bind('bar', function ($value) { 102 | return strtoupper($value); 103 | }); 104 | $this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); 105 | } 106 | ``` 107 | 108 | - 通过 `bind` 为参数绑定类方法,可以指定 `classname@method`,也可以直接使用类名,默认会调用类的 `bind` 函数: 109 | 110 | ```php 111 | public function testRouteClassBinding() 112 | { 113 | $router = $this->getRouter(); 114 | $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) { 115 | return $name; 116 | }]); 117 | $router->bind('bar', 'Illuminate\Tests\Routing\RouteBindingStub'); 118 | $this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); 119 | } 120 | 121 | public function testRouteClassMethodBinding() 122 | { 123 | $router = $this->getRouter(); 124 | $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) { 125 | return $name; 126 | }]); 127 | $router->bind('bar', 'Illuminate\Tests\Routing\RouteBindingStub@find'); 128 | $this->assertEquals('dragon', $router->dispatch(Request::create('foo/Dragon', 'GET'))->getContent()); 129 | } 130 | 131 | class RouteBindingStub 132 | { 133 | public function bind($value, $route) 134 | { 135 | return strtoupper($value); 136 | } 137 | 138 | public function find($value, $route) 139 | { 140 | return strtolower($value); 141 | } 142 | } 143 | ``` 144 | 145 | - 通过 `model` 为参数绑定数据库模型,路由的参数就不需要和控制器方法中的变量名相同,`laravel` 会利用路由参数的值去调用 `where` 方法查找对应记录: 146 | 147 | ```php 148 | if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) { 149 | return $model; 150 | } 151 | ``` 152 | 测试样例如下: 153 | 154 | ```php 155 | public function testModelBinding() 156 | { 157 | $router = $this->getRouter(); 158 | $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) { 159 | return $name; 160 | }]); 161 | $router->model('bar', 'Illuminate\Tests\Routing\RouteModelBindingStub'); 162 | $this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); 163 | } 164 | 165 | class RouteModelBindingStub 166 | { 167 | public function getRouteKeyName() 168 | { 169 | return 'id'; 170 | } 171 | 172 | public function where($key, $value) 173 | { 174 | $this->value = $value; 175 | 176 | return $this; 177 | } 178 | 179 | public function first() 180 | { 181 | return strtoupper($this->value); 182 | } 183 | } 184 | ``` 185 | 186 | - 若绑定的 `model` 并没有找到对应路由参数的记录,可以在 `model` 中定义一个闭包函数,路由参数会调用闭包函数: 187 | 188 | ```php 189 | public function testModelBindingWithCustomNullReturn() 190 | { 191 | $router = $this->getRouter(); 192 | $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) { 193 | return $name; 194 | }]); 195 | $router->model('bar', 'Illuminate\Tests\Routing\RouteModelBindingNullStub', function () { 196 | return 'missing'; 197 | }); 198 | $this->assertEquals('missing', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); 199 | } 200 | 201 | public function testModelBindingWithBindingClosure() 202 | { 203 | $router = $this->getRouter(); 204 | $router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) { 205 | return $name; 206 | }]); 207 | $router->model('bar', 'Illuminate\Tests\Routing\RouteModelBindingNullStub', function ($value) { 208 | return (new RouteModelBindingClosureStub())->findAlternate($value); 209 | }); 210 | $this->assertEquals('tayloralt', $router->dispatch(Request::create('foo/TAYLOR', 'GET'))->getContent()); 211 | } 212 | 213 | class RouteModelBindingNullStub 214 | { 215 | public function getRouteKeyName() 216 | { 217 | return 'id'; 218 | } 219 | 220 | public function where($key, $value) 221 | { 222 | return $this; 223 | } 224 | 225 | public function first() 226 | { 227 | } 228 | } 229 | 230 | class RouteModelBindingClosureStub 231 | { 232 | public function findAlternate($value) 233 | { 234 | return strtolower($value).'alt'; 235 | } 236 | } 237 | 238 | ``` 239 | 240 | # `SubstituteBindings` 中间件的源码解析 241 | 242 | ```php 243 | class SubstituteBindings 244 | { 245 | public function handle($request, Closure $next) 246 | { 247 | $this->router->substituteBindings($route = $request->route()); 248 | 249 | $this->router->substituteImplicitBindings($route); 250 | 251 | return $next($request); 252 | } 253 | } 254 | ``` 255 | 从代码来看,`substituteBindings` 用于显示的参数转化,`substituteImplicitBindings` 用于隐性的参数转化。 256 | 257 | ## 隐性参数转化源码解析 258 | 259 | 进行隐性参数转化,其步骤为: 260 | 261 | - 扫描控制器方法或者闭包函数所有的参数,提取出数据库模型类型对象 262 | - 根据模型类型对象的 `name`,找出与模型对象命名相同的路由参数 263 | - 根据模型类型对象的 `classname`,构建数据库模型类型对象,根据路由参数的数值在数据库中执行 `sql` 语句查询 264 | 265 | ```php 266 | public function substituteImplicitBindings($route) 267 | { 268 | ImplicitRouteBinding::resolveForRoute($this->container, $route); 269 | } 270 | 271 | class ImplicitRouteBinding 272 | { 273 | public static function resolveForRoute($container, $route) 274 | { 275 | $parameters = $route->parameters(); 276 | 277 | foreach ($route->signatureParameters(Model::class) as $parameter) { 278 | $class = $parameter->getClass(); 279 | 280 | if (array_key_exists($parameter->name, $parameters) && 281 | ! $route->parameter($parameter->name) instanceof Model) { 282 | $method = $parameter->isDefaultValueAvailable() ? 'first' : 'firstOrFail'; 283 | 284 | $model = $container->make($class->name); 285 | 286 | $route->setParameter( 287 | $parameter->name, $model->where( 288 | $model->getRouteKeyName(), $parameters[$parameter->name] 289 | )->{$method}() 290 | ); 291 | } 292 | } 293 | } 294 | } 295 | ``` 296 | 297 | 值得注意的是,显示参数转化的优先级要高于隐性转化,如果当前参数已经被 `model` 函数显示转化,那么该参数并不会进行隐性转化,也就是上面语句 `! $route->parameter($parameter->name) instanceof Model` 的作用。 298 | 299 | 其中扫描控制器方法参数的功能主要利用反射机制: 300 | 301 | ```php 302 | public function signatureParameters($subClass = null) 303 | { 304 | return RouteSignatureParameters::fromAction($this->action, $subClass); 305 | } 306 | 307 | class RouteSignatureParameters 308 | { 309 | public static function fromAction(array $action, $subClass = null) 310 | { 311 | $parameters = is_string($action['uses']) 312 | ? static::fromClassMethodString($action['uses']) 313 | : (new ReflectionFunction($action['uses']))->getParameters(); 314 | 315 | return is_null($subClass) ? $parameters : array_filter($parameters, function ($p) use ($subClass) { 316 | return $p->getClass() && $p->getClass()->isSubclassOf($subClass); 317 | }); 318 | } 319 | 320 | protected static function fromClassMethodString($uses) 321 | { 322 | list($class, $method) = Str::parseCallback($uses); 323 | 324 | return (new ReflectionMethod($class, $method))->getParameters(); 325 | } 326 | } 327 | ``` 328 | 329 | ## bind 显示参数绑定 330 | 331 | 路由的 `bind` 功能由专门的 `binders` 数组负责,这个数组中保存着所有的需要显示转化的路由参数与他们的转化闭包函数: 332 | 333 | ```php 334 | public function bind($key, $binder) 335 | { 336 | $this->binders[str_replace('-', '_', $key)] = RouteBinding::forCallback( 337 | $this->container, $binder 338 | ); 339 | } 340 | 341 | class RouteBinding 342 | { 343 | public static function forCallback($container, $binder) 344 | { 345 | if (is_string($binder)) { 346 | return static::createClassBinding($container, $binder); 347 | } 348 | 349 | return $binder; 350 | } 351 | 352 | protected static function createClassBinding($container, $binding) 353 | { 354 | return function ($value, $route) use ($container, $binding) { 355 | list($class, $method) = Str::parseCallback($binding, 'bind'); 356 | 357 | $callable = [$container->make($class), $method]; 358 | 359 | return call_user_func($callable, $value, $route); 360 | }; 361 | } 362 | } 363 | ``` 364 | 365 | 可以看出,`bind` 函数可以绑定闭包、`classname@method`、`classname`,如果仅仅绑定了一个类名,那么程序默认调用类中 `bind` 方法。 366 | 367 | ## model 显示参数绑定 368 | 369 | `model` 调用 `bind` 函数,赋给 `bind` 函数一个提前包装好的闭包函数: 370 | 371 | ```php 372 | public function model($key, $class, Closure $callback = null) 373 | { 374 | $this->bind($key, RouteBinding::forModel($this->container, $class, $callback)); 375 | } 376 | 377 | class RouteBinding 378 | { 379 | public static function forModel($container, $class, $callback = null) 380 | { 381 | return function ($value) use ($container, $class, $callback) { 382 | if (is_null($value)) { 383 | return; 384 | } 385 | 386 | $instance = $container->make($class); 387 | 388 | if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) { 389 | return $model; 390 | } 391 | 392 | if ($callback instanceof Closure) { 393 | return call_user_func($callback, $value); 394 | } 395 | 396 | throw (new ModelNotFoundException)->setModel($class); 397 | }; 398 | } 399 | } 400 | ``` 401 | 402 | 可以看出,这个闭包函数与隐性转化很相似,都是首先创建数据库模型对象,再利用路由参数值来查询数据库,返回对象。 `model` 还可以提供默认的闭包函数,以供查询不到数据库时调用。 403 | 404 | ## 显示路由参数转化 405 | 406 | 当运行中间件 `SubstituteBindings` 时,就会将先前绑定的各个闭包函数执行,并对路由参数进行转化: 407 | 408 | ```php 409 | public function substituteBindings($route) 410 | { 411 | foreach ($route->parameters() as $key => $value) { 412 | if (isset($this->binders[$key])) { 413 | $route->setParameter($key, $this->performBinding($key, $value, $route)); 414 | } 415 | } 416 | 417 | return $route; 418 | } 419 | 420 | protected function performBinding($key, $value, $route) 421 | { 422 | return call_user_func($this->binders[$key], $value, $route); 423 | } 424 | 425 | public function setParameter($name, $value) 426 | { 427 | $this->parameters(); 428 | 429 | $this->parameters[$name] = $value; 430 | } 431 | ``` -------------------------------------------------------------------------------- /Laravel HTTP——控制器方法的参数构建与运行.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 经过前面一系列中间件的工作,现在请求终于要达到了正确的控制器方法了。本篇文章主要讲 `laravel` 如何调用控制器方法,并且为控制器方法依赖注入构建参数的过程。 4 | 5 | # 路由控制器的调用 6 | 7 | 我们前面已经解析过中间件的搜集与排序、pipeline 的原理,接下来就要进行路由的 `run` 运行函数: 8 | 9 | ```php 10 | protected function runRouteWithinStack(Route $route, Request $request) 11 | { 12 | $shouldSkipMiddleware = $this->container->bound('middleware.disable') && 13 | $this->container->make('middleware.disable') === true; 14 | 15 | $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); 16 | 17 | return (new Pipeline($this->container)) 18 | ->send($request) 19 | ->through($middleware) 20 | ->then(function ($request) use ($route) { 21 | return $this->prepareResponse( 22 | $request, $route->run() 23 | ); 24 | }); 25 | } 26 | ``` 27 | 28 | 路由的 `run` 函数主要负责路由控制器方法与路由闭包函数的运行: 29 | 30 | ```php 31 | public function run() 32 | { 33 | $this->container = $this->container ?: new Container; 34 | 35 | try { 36 | if ($this->isControllerAction()) { 37 | return $this->runController(); 38 | } 39 | 40 | return $this->runCallable(); 41 | } catch (HttpResponseException $e) { 42 | return $e->getResponse(); 43 | } 44 | } 45 | ``` 46 | 47 | 路由的运行主要靠 `ControllerDispatcher` 这个类: 48 | 49 | ```php 50 | class Route 51 | { 52 | protected function isControllerAction() 53 | { 54 | return is_string($this->action['uses']); 55 | } 56 | 57 | protected function runController() 58 | { 59 | return (new ControllerDispatcher($this->container))->dispatch( 60 | $this, $this->getController(), $this->getControllerMethod() 61 | ); 62 | } 63 | } 64 | 65 | class ControllerDispatcher 66 | { 67 | use RouteDependencyResolverTrait; 68 | 69 | public function dispatch(Route $route, $controller, $method) 70 | { 71 | $parameters = $this->resolveClassMethodDependencies( 72 | $route->parametersWithoutNulls(), $controller, $method 73 | ); 74 | 75 | if (method_exists($controller, 'callAction')) { 76 | return $controller->callAction($method, $parameters); 77 | } 78 | 79 | return $controller->{$method}(...array_values($parameters)); 80 | } 81 | } 82 | ``` 83 | 上面可以很清晰地看出,控制器的运行分为两步:解析函数参数、调用callAction 84 | 85 | ## 解析控制器方法参数 86 | 87 | 解析参数的功能主要由 `ControllerDispatcher` 类的 `RouteDependencyResolverTrait` 这一 `trait` 负责: 88 | 89 | ```php 90 | trait RouteDependencyResolverTrait 91 | { 92 | protected function resolveClassMethodDependencies(array $parameters, $instance, $method) 93 | { 94 | if (! method_exists($instance, $method)) { 95 | return $parameters; 96 | } 97 | 98 | return $this->resolveMethodDependencies( 99 | $parameters, new ReflectionMethod($instance, $method) 100 | ); 101 | } 102 | 103 | public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector) 104 | { 105 | $instanceCount = 0; 106 | 107 | $values = array_values($parameters); 108 | 109 | foreach ($reflector->getParameters() as $key => $parameter) { 110 | $instance = $this->transformDependency( 111 | $parameter, $parameters 112 | ); 113 | 114 | if (! is_null($instance)) { 115 | $instanceCount++; 116 | 117 | $this->spliceIntoParameters($parameters, $key, $instance); 118 | } elseif (! isset($values[$key - $instanceCount]) && 119 | $parameter->isDefaultValueAvailable()) { 120 | $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue()); 121 | } 122 | } 123 | 124 | return $parameters; 125 | } 126 | } 127 | ``` 128 | 129 | 控制器方法函数参数构造难点在于,参数来源有三种: 130 | 131 | - 路由参数赋值 132 | - Ioc 容器自动注入 133 | - 函数自带默认值 134 | 135 | 在 Ioc 容器自动注入的时候,要保证路由的现有参数中没有相应的类,防止依赖注入覆盖路由绑定的参数: 136 | 137 | ```php 138 | protected function transformDependency(ReflectionParameter $parameter, $parameters) 139 | { 140 | $class = $parameter->getClass(); 141 | 142 | if ($class && ! $this->alreadyInParameters($class->name, $parameters)) { 143 | return $this->container->make($class->name); 144 | } 145 | } 146 | 147 | protected function alreadyInParameters($class, array $parameters) 148 | { 149 | return ! is_null(Arr::first($parameters, function ($value) use ($class) { 150 | return $value instanceof $class; 151 | })); 152 | } 153 | ``` 154 | 由 Ioc 容器构造出的参数需要插入到原有的路由参数数组中: 155 | 156 | ```php 157 | if (! is_null($instance)) { 158 | $instanceCount++; 159 | 160 | $this->spliceIntoParameters($parameters, $key, $instance); 161 | } 162 | 163 | protected function spliceIntoParameters(array &$parameters, $offset, $value) 164 | { 165 | array_splice( 166 | $parameters, $offset, 0, [$value] 167 | ); 168 | } 169 | ``` 170 | 171 | 当路由的参数数组与 Ioc 容器构造的参数数量不足以覆盖控制器参数个数时,就要去判断控制器是否具有默认参数: 172 | 173 | ```php 174 | elseif (! isset($values[$key - $instanceCount]) && 175 | $parameter->isDefaultValueAvailable()) { 176 | $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue()); 177 | } 178 | ``` 179 | 180 | ## 调用控制器方法 callAction 181 | 182 | 所有的控制器并非是直接调用相应方法的,而是通过 `callAction` 函数再分配,如果实在没有相应方法还会调用魔术方法 `__call()`: 183 | 184 | ```php 185 | public function callAction($method, $parameters) 186 | { 187 | return call_user_func_array([$this, $method], $parameters); 188 | } 189 | 190 | public function __call($method, $parameters) 191 | { 192 | throw new BadMethodCallException("Method [{$method}] does not exist."); 193 | } 194 | ``` 195 | 196 | # 路由闭包函数的调用 197 | 198 | 路由闭包函数的调用与控制器方法一样,仍然需要依赖注入,参数构造: 199 | 200 | ```php 201 | protected function runCallable() 202 | { 203 | $callable = $this->action['uses']; 204 | 205 | return $callable(...array_values($this->resolveMethodDependencies( 206 | $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses']) 207 | ))); 208 | } 209 | ``` -------------------------------------------------------------------------------- /Laravel HTTP——路由中间件源码分析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 当进行了路由匹配与路由参数绑定后,接下来就要进行路由闭包或者控制器的运行,在此之前,本文先介绍中间件的相关源码。 3 | 4 | # 中间件的搜集 5 | 由于定义的中间件方式很灵活,所以在运行控制器或者路由闭包之前,我们需要先将在各个地方注册的所有中间件都搜集到一起,然后集中排序。 6 | 7 | ```php 8 | public function dispatchToRoute(Request $request) 9 | { 10 | $route = $this->findRoute($request); 11 | 12 | $request->setRouteResolver(function () use ($route) { 13 | return $route; 14 | }); 15 | 16 | $this->events->dispatch(new Events\RouteMatched($route, $request)); 17 | 18 | $response = $this->runRouteWithinStack($route, $request); 19 | 20 | return $this->prepareResponse($request, $response); 21 | } 22 | 23 | protected function runRouteWithinStack(Route $route, Request $request) 24 | { 25 | $shouldSkipMiddleware = $this->container->bound('middleware.disable') && 26 | $this->container->make('middleware.disable') === true; 27 | 28 | $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); 29 | 30 | return (new Pipeline($this->container)) 31 | ->send($request) 32 | ->through($middleware) 33 | ->then(function ($request) use ($route) { 34 | return $this->prepareResponse( 35 | $request, $route->run() 36 | ); 37 | }); 38 | } 39 | 40 | public function gatherRouteMiddleware(Route $route) 41 | { 42 | $middleware = collect($route->gatherMiddleware())->map(function ($name) { 43 | return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups); 44 | })->flatten(); 45 | 46 | return $this->sortMiddleware($middleware); 47 | } 48 | ``` 49 | 50 | 路由的中间件大致有两个大的来源: 51 | 52 | - 在路由的定义过程中,利用关键字 `middleware` 为路由添加中间件,这种中间件都是在文件 `App\Http\Kernel` 中`$middlewareGroups` 、`$routeMiddleware` 这两个数组定义的中间件别名。 53 | - 在路由控制器的构造函数中,添加中间件,可以在这里定义一个闭包作为中间件,也可以利用中间件别名。 54 | 55 | ```php 56 | public function gatherMiddleware() 57 | { 58 | if (! is_null($this->computedMiddleware)) { 59 | return $this->computedMiddleware; 60 | } 61 | 62 | $this->computedMiddleware = []; 63 | 64 | return $this->computedMiddleware = array_unique(array_merge( 65 | $this->middleware(), $this->controllerMiddleware() 66 | ), SORT_REGULAR); 67 | } 68 | ``` 69 | 70 | ### 路由定义的中间件是从 `action` 数组中取出来的: 71 | 72 | ```php 73 | public function middleware($middleware = null) 74 | { 75 | if (is_null($middleware)) { 76 | return (array) Arr::get($this->action, 'middleware', []); 77 | } 78 | 79 | if (is_string($middleware)) { 80 | $middleware = func_get_args(); 81 | } 82 | 83 | $this->action['middleware'] = array_merge( 84 | (array) Arr::get($this->action, 'middleware', []), $middleware 85 | ); 86 | 87 | return $this; 88 | } 89 | ``` 90 | ### 控制器定义的中间件: 91 | 92 | ```php 93 | public function controllerMiddleware() 94 | { 95 | if (! $this->isControllerAction()) { 96 | return []; 97 | } 98 | 99 | return ControllerDispatcher::getMiddleware( 100 | $this->getController(), $this->getControllerMethod() 101 | ); 102 | } 103 | 104 | public function getController() 105 | { 106 | $class = $this->parseControllerCallback()[0]; 107 | 108 | if (! $this->controller) { 109 | $this->controller = $this->container->make($class); 110 | } 111 | 112 | return $this->controller; 113 | } 114 | 115 | protected function getControllerMethod() 116 | { 117 | return $this->parseControllerCallback()[1]; 118 | } 119 | 120 | protected function parseControllerCallback() 121 | { 122 | return Str::parseCallback($this->action['uses']); 123 | } 124 | 125 | public static function parseCallback($callback, $default = null) 126 | { 127 | return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; 128 | } 129 | ``` 130 | 131 | 当前的路由如果使用控制器的时候,就要解析属性 `use`,解析出控制器的类名与类方法。接下来就需要 `ControllerDispatcher` 类。 132 | 133 | 在讲解 `ControllerDispatcher` 类之前,我们需要先了解一下控制器中间件: 134 | 135 | ```php 136 | abstract class Controller 137 | { 138 | public function middleware($middleware, array $options = []) 139 | { 140 | foreach ((array) $middleware as $m) { 141 | $this->middleware[] = [ 142 | 'middleware' => $m, 143 | 'options' => &$options, 144 | ]; 145 | } 146 | 147 | return new ControllerMiddlewareOptions($options); 148 | } 149 | } 150 | 151 | class ControllerMiddlewareOptions 152 | { 153 | protected $options; 154 | 155 | public function __construct(array &$options) 156 | { 157 | $this->options = &$options; 158 | } 159 | 160 | public function only($methods) 161 | { 162 | $this->options['only'] = is_array($methods) ? $methods : func_get_args(); 163 | 164 | return $this; 165 | } 166 | 167 | public function except($methods) 168 | { 169 | $this->options['except'] = is_array($methods) ? $methods : func_get_args(); 170 | 171 | return $this; 172 | } 173 | } 174 | ``` 175 | 在为控制器定义中间的是,可以为中间件利用 `only` 指定在当前控制器中调用该中间件的特定控制器方法,也可以利用 `except`指定在当前控制器禁止调用中间件的方法。这些信息都保存在控制器的变量 `middleware` 的 `options` 中。 176 | 177 | 在搜集控制器的中间件时,就要利用中间件的这些信息: 178 | 179 | ```php 180 | class ControllerDispatcher 181 | { 182 | public static function getMiddleware($controller, $method) 183 | { 184 | if (! method_exists($controller, 'getMiddleware')) { 185 | return []; 186 | } 187 | 188 | return collect($controller->getMiddleware())->reject(function ($data) use ($method) { 189 | return static::methodExcludedByOptions($method, $data['options']); 190 | })->pluck('middleware')->all(); 191 | } 192 | 193 | protected static function methodExcludedByOptions($method, array $options) 194 | { 195 | return (isset($options['only']) && ! in_array($method, (array) $options['only'])) || 196 | (! empty($options['except']) && in_array($method, (array) $options['except'])); 197 | } 198 | } 199 | ``` 200 | 201 | 在 `ControllerDispatcher` 类中,利用了 `reject` 函数对每一个中间件都进行了控制器方法的判断,排除了不支持该控制器方法的中间件。`pluck` 函数获取了控制器 `$this->middleware[]` 数组中 `middleware` 的所有元素。 202 | 203 | # 中间件的解析 204 | 中间件解析主要的工作是将路由中中间件的别名转化为中间件全程,主要流程为: 205 | 206 | ```php 207 | class MiddlewareNameResolver 208 | { 209 | public static function resolve($name, $map, $middlewareGroups) 210 | { 211 | if ($name instanceof Closure) { 212 | return $name; 213 | } elseif (isset($map[$name]) && $map[$name] instanceof Closure) { 214 | return $map[$name]; 215 | 216 | } elseif (isset($middlewareGroups[$name])) { 217 | return static::parseMiddlewareGroup( 218 | $name, $map, $middlewareGroups 219 | ); 220 | 221 | } else { 222 | list($name, $parameters) = array_pad(explode(':', $name, 2), 2, null); 223 | 224 | return (isset($map[$name]) ? $map[$name] : $name). 225 | (! is_null($parameters) ? ':'.$parameters : ''); 226 | } 227 | } 228 | } 229 | ``` 230 | 231 | 可以看出,解析的中间件对象有三种:闭包、中间件别名、中间件组。 232 | 233 | - 对于闭包来说,`resolve` 直接返回闭包; 234 | - 对于中间件别名来说,例如 `auth` ,会从 `App\Http\Kernel` 文件 `$routeMiddleware` 数组中寻找中间件全名 `\Illuminate\Auth\Middleware\Authenticate::class` 235 | - 对于具有参数的中间件别名来说,例如 `throttle:60,1`,会将别名转化为全名 `\Illuminate\Routing\Middleware\ThrottleRequests::60,1` 236 | - 对于中间件组来说,会调用 `parseMiddlewareGroup` 函数。 237 | 238 | ```php 239 | protected static function parseMiddlewareGroup($name, $map, $middlewareGroups) 240 | { 241 | $results = []; 242 | 243 | foreach ($middlewareGroups[$name] as $middleware) { 244 | if (isset($middlewareGroups[$middleware])) { 245 | $results = array_merge($results, static::parseMiddlewareGroup( 246 | $middleware, $map, $middlewareGroups 247 | )); 248 | 249 | continue; 250 | } 251 | 252 | list($middleware, $parameters) = array_pad( 253 | explode(':', $middleware, 2), 2, null 254 | ); 255 | 256 | if (isset($map[$middleware])) { 257 | $middleware = $map[$middleware]; 258 | } 259 | 260 | $results[] = $middleware.($parameters ? ':'.$parameters : ''); 261 | } 262 | 263 | return $results; 264 | } 265 | ``` 266 | 可以看出,对于中间件组来说,就要从 `App\Http\Kernel` 文件 `$$middlewareGroups` 数组中寻找组内的多个中间件,例如中间件组 `api` : 267 | 268 | ```php 269 | 'api' => [ 270 | 'throttle:60,1', 271 | 'bindings', 272 | ] 273 | ``` 274 | 解析出的中间件可能存在参数,别名转化为全名后函数返回。值得注意的是,中间件组内不一定都是别名,也有可能是中间件组的组名,例如: 275 | 276 | ```php 277 | 'api' => [ 278 | 'throttle:60,1', 279 | 'web', 280 | ] 281 | 282 | 'web' => [ 283 | \App\Http\Middleware\EncryptCookies::class, 284 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 285 | ], 286 | ``` 287 | 这时,就需要迭代解析。 288 | 289 | # 中间件的排序 290 | 291 | ```php 292 | public function gatherRouteMiddleware(Route $route) 293 | { 294 | $middleware = collect($route->gatherMiddleware())->map(function ($name) { 295 | return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups); 296 | })->flatten(); 297 | 298 | return $this->sortMiddleware($middleware); 299 | } 300 | ``` 301 | 302 | 将所有中间件搜集并解析完毕后,接下来就要对中间件的调用顺序做一些调整,以确保中间件功能正常。 303 | 304 | ```php 305 | protected $middlewarePriority = [ 306 | \Illuminate\Session\Middleware\StartSession::class, 307 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 308 | \Illuminate\Auth\Middleware\Authenticate::class, 309 | \Illuminate\Session\Middleware\AuthenticateSession::class, 310 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 311 | \Illuminate\Auth\Middleware\Authorize::class, 312 | ]; 313 | ``` 314 | 数组 `middlewarePriority` 中保存着必须有一定顺序的中间件,例如 `StartSession` 中间件就必须运行在 `ShareErrorsFromSession` 之前。因此一旦路由中有这两个中间件,那么就要确保两者的顺序一致。 315 | 316 | 中间件的排序由函数 `sortMiddleware` 负责: 317 | 318 | ```php 319 | class SortedMiddleware extends Collection 320 | { 321 | public function __construct(array $priorityMap, $middlewares) 322 | { 323 | if ($middlewares instanceof Collection) { 324 | $middlewares = $middlewares->all(); 325 | } 326 | 327 | $this->items = $this->sortMiddleware($priorityMap, $middlewares); 328 | } 329 | 330 | protected function sortMiddleware($priorityMap, $middlewares) 331 | { 332 | $lastIndex = 0; 333 | 334 | foreach ($middlewares as $index => $middleware) { 335 | if (! is_string($middleware)) { 336 | continue; 337 | } 338 | 339 | $stripped = head(explode(':', $middleware)); 340 | 341 | if (in_array($stripped, $priorityMap)) { 342 | $priorityIndex = array_search($stripped, $priorityMap); 343 | 344 | if (isset($lastPriorityIndex) && $priorityIndex < $lastPriorityIndex) { 345 | return $this->sortMiddleware( 346 | $priorityMap, array_values( 347 | $this->moveMiddleware($middlewares, $index, $lastIndex) 348 | ) 349 | ); 350 | } else { 351 | $lastIndex = $index; 352 | $lastPriorityIndex = $priorityIndex; 353 | } 354 | } 355 | } 356 | 357 | return array_values(array_unique($middlewares, SORT_REGULAR)); 358 | } 359 | 360 | protected function moveMiddleware($middlewares, $from, $to) 361 | { 362 | array_splice($middlewares, $to, 0, $middlewares[$from]); 363 | 364 | unset($middlewares[$from + 1]); 365 | 366 | return $middlewares; 367 | } 368 | } 369 | ``` 370 | 371 | 函数的方法很简单,检测当前中间件数组,查看是否存在中间件是数组 `middlewarePriority` 内元素。如果发现了两个中间件不符合顺序,那么就要调换中间件顺序,然后进行迭代。 -------------------------------------------------------------------------------- /Laravel HTTP——路由的匹配与参数绑定.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 上一篇文章我们说到路由的正则编译,正则编译的目的就是和请求的 `url` 来匹配,只有匹配上的路由才是我们真正想要的,此外也会通过正则匹配来获取路由的参数。 3 | 4 | 5 | # 路由的匹配 6 | 7 | 路由进行正则编译后,就要与请求 `request` 来进行正则匹配,并且进行一些验证,例如 `UriValidator`、`MethodValidator`、`SchemeValidator`、`HostValidator`。 8 | 9 | ```php 10 | class RouteCollection implements Countable, IteratorAggregate 11 | { 12 | public function match(Request $request) 13 | { 14 | $routes = $this->get($request->getMethod()); 15 | 16 | $route = $this->matchAgainstRoutes($routes, $request); 17 | 18 | if (! is_null($route)) { 19 | return $route->bind($request); 20 | } 21 | 22 | $others = $this->checkForAlternateVerbs($request); 23 | 24 | if (count($others) > 0) { 25 | return $this->getRouteForMethods($request, $others); 26 | } 27 | 28 | throw new NotFoundHttpException; 29 | } 30 | 31 | protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true) 32 | { 33 | return Arr::first($routes, function ($value) use ($request, $includingMethod) { 34 | return $value->matches($request, $includingMethod); 35 | }); 36 | } 37 | } 38 | 39 | class Route 40 | { 41 | public function matches(Request $request, $includingMethod = true) 42 | { 43 | $this->compileRoute(); 44 | 45 | foreach ($this->getValidators() as $validator) { 46 | if (! $includingMethod && $validator instanceof MethodValidator) { 47 | continue; 48 | } 49 | 50 | if (! $validator->matches($this, $request)) { 51 | return false; 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | public static function getValidators() 59 | { 60 | if (isset(static::$validators)) { 61 | return static::$validators; 62 | } 63 | 64 | return static::$validators = [ 65 | new UriValidator, new MethodValidator, 66 | new SchemeValidator, new HostValidator, 67 | ]; 68 | } 69 | } 70 | ``` 71 | 72 | ## UriValidator uri 验证 73 | 74 | `UriValidator` 验证主要是目的是查看路由正则与请求是否匹配: 75 | 76 | ```php 77 | class UriValidator implements ValidatorInterface 78 | { 79 | public function matches(Route $route, Request $request) 80 | { 81 | $path = $request->path() == '/' ? '/' : '/'.$request->path(); 82 | 83 | return preg_match($route->getCompiled()->getRegex(), rawurldecode($path)); 84 | } 85 | } 86 | ``` 87 | 88 | 值得注意的是,在匹配路径之前,程序使用了 `rawurldecode` 来对请求进行解码。 89 | 90 | ## MethodValidator 验证 91 | 92 | 请求方法验证: 93 | 94 | ```php 95 | class MethodValidator implements ValidatorInterface 96 | { 97 | public function matches(Route $route, Request $request) 98 | { 99 | return in_array($request->getMethod(), $route->methods()); 100 | } 101 | } 102 | ``` 103 | 104 | ## SchemeValidator 验证 105 | 106 | 路由 `scheme` 协议验证: 107 | 108 | ```php 109 | class SchemeValidator implements ValidatorInterface 110 | { 111 | public function matches(Route $route, Request $request) 112 | { 113 | if ($route->httpOnly()) { 114 | return ! $request->secure(); 115 | } elseif ($route->secure()) { 116 | return $request->secure(); 117 | } 118 | 119 | return true; 120 | } 121 | } 122 | 123 | public function httpOnly() 124 | { 125 | return in_array('http', $this->action, true); 126 | } 127 | 128 | public function secure() 129 | { 130 | return in_array('https', $this->action, true); 131 | } 132 | ``` 133 | 134 | ## HostValidator 验证 135 | 主域验证: 136 | 137 | ```php 138 | class HostValidator implements ValidatorInterface 139 | { 140 | public function matches(Route $route, Request $request) 141 | { 142 | if (is_null($route->getCompiled()->getHostRegex())) { 143 | return true; 144 | } 145 | 146 | return preg_match($route->getCompiled()->getHostRegex(), $request->getHost()); 147 | } 148 | } 149 | ``` 150 | 151 | 也就是说,如果路由中并不设置 `host` 属性,那么这个验证并不进行。 152 | 153 | # 路由的参数绑定 154 | 155 | 一旦某个路由符合请求的 `uri` 四项认证,就将会被返回,接下来就要对路由的参数进行绑定与赋值: 156 | 157 | ```php 158 | class RouteCollection implements Countable, IteratorAggregate 159 | { 160 | public function bind(Request $request) 161 | { 162 | $this->compileRoute(); 163 | 164 | $this->parameters = (new RouteParameterBinder($this)) 165 | ->parameters($request); 166 | 167 | return $this; 168 | } 169 | } 170 | ``` 171 | `bind` 函数负责路由参数与请求 `url` 的绑定工作: 172 | 173 | ```php 174 | class RouteParameterBinder 175 | { 176 | public function parameters($request) 177 | { 178 | $parameters = $this->bindPathParameters($request); 179 | 180 | if (! is_null($this->route->compiled->getHostRegex())) { 181 | $parameters = $this->bindHostParameters( 182 | $request, $parameters 183 | ); 184 | } 185 | 186 | return $this->replaceDefaults($parameters); 187 | } 188 | } 189 | ``` 190 | 可以看出,路由参数绑定分为主域参数绑定与路径参数绑定,我们先看路径参数绑定: 191 | 192 | ## 路径参数绑定 193 | 194 | ```php 195 | class RouteParameterBinder 196 | { 197 | protected function bindPathParameters($request) 198 | { 199 | preg_match($this->route->compiled->getRegex(), '/'.$request->decodedPath(), $matches); 200 | 201 | return $this->matchToKeys(array_slice($matches, 1)); 202 | } 203 | } 204 | ``` 205 | 206 | 例如,`{foo}/{baz?}.{ext?}` 进行正则编译后结果: 207 | 208 | ```php 209 | #^/(?P[^/]++)(?:/(?P[^/\.]++)(?:\.(?P[^/]++))?)?$#s 210 | ``` 211 | 212 | 其与 `request` 匹配后的结果为: 213 | 214 | ```php 215 | $matches = array ( 216 | 0 = "/foo/baz.ext", 217 | 1 = "foo", 218 | foo = "foo", 219 | 2 = "baz", 220 | baz = "baz", 221 | 3 = "ext", 222 | ext = "ext", 223 | ) 224 | ``` 225 | 226 | `array_slice($matches, 1)` 取出了 `$matches` 数组 1 之后的结果,然后调用了 `matchToKeys` 函数, 227 | 228 | ```php 229 | protected function matchToKeys(array $matches) 230 | { 231 | if (empty($parameterNames = $this->route->parameterNames())) { 232 | return []; 233 | } 234 | 235 | $parameters = array_intersect_key($matches, array_flip($parameterNames)); 236 | 237 | return array_filter($parameters, function ($value) { 238 | return is_string($value) && strlen($value) > 0; 239 | }); 240 | } 241 | ``` 242 | 243 | 该函数中利用正则获取了路由的所有参数: 244 | 245 | ```php 246 | class Route 247 | { 248 | public function parameterNames() 249 | { 250 | if (isset($this->parameterNames)) { 251 | return $this->parameterNames; 252 | } 253 | 254 | return $this->parameterNames = $this->compileParameterNames(); 255 | } 256 | 257 | 258 | protected function compileParameterNames() 259 | { 260 | preg_match_all('/\{(.*?)\}/', $this->domain().$this->uri, $matches); 261 | 262 | return array_map(function ($m) { 263 | return trim($m, '?'); 264 | }, $matches[1]); 265 | } 266 | } 267 | ``` 268 | 可以看出,获取路由参数的正则表达式采用了勉强模式,意图提取出所有的路由参数。否则,对于路由 `{foo}/{baz?}.{ext?}`,贪婪型正则表达式 `/\{(.*)\}/` 将会匹配整个字符串,而不是各个参数分组。 269 | 270 | 提取出的参数结果为: 271 | 272 | ```php 273 | $matches = array ( 274 | 0 = array ( 275 | 0 = "{foo}". 276 | 1 = "{baz?}", 277 | 2 = "{ext?}", 278 | ) 279 | 1 = array ( 280 | 0 = "foo". 281 | 1 = "baz?", 282 | 2 = "ext?", 283 | ) 284 | ) 285 | ``` 286 | 得出的结果将会去除 `$matches[1]`,并且将会删除结果中最后的 `?`。 287 | 288 | 之后,在 `matchToKeys` 函数中, 289 | 290 | ```php 291 | $parameters = array_intersect_key($matches, array_flip($parameterNames)); 292 | ``` 293 | 获取了匹配结果与路由所有参数的交集: 294 | 295 | ```php 296 | $parameters = array ( 297 | foo = "foo", 298 | baz = "baz", 299 | ext = "ext", 300 | ) 301 | ``` 302 | 303 | 304 | ## 主域参数绑定 305 | 306 | ```php 307 | protected function bindHostParameters($request, $parameters) 308 | { 309 | preg_match($this->route->compiled->getHostRegex(), $request->getHost(), $matches); 310 | 311 | return array_merge($this->matchToKeys(array_slice($matches, 1)), $parameters); 312 | } 313 | ``` 314 | 315 | 步骤与路由参数绑定一致。 316 | 317 | ## 替换默认值 318 | 319 | 进行参数绑定后,有一些可选参数并没有在 `request` 中匹配到,这时候就要用可选参数的默认值添加到变量 `parameters` 中: 320 | 321 | ```php 322 | protected function replaceDefaults(array $parameters) 323 | { 324 | foreach ($parameters as $key => $value) { 325 | $parameters[$key] = isset($value) ? $value : Arr::get($this->route->defaults, $key); 326 | } 327 | 328 | foreach ($this->route->defaults as $key => $value) { 329 | if (! isset($parameters[$key])) { 330 | $parameters[$key] = $value; 331 | } 332 | } 333 | 334 | return $parameters; 335 | } 336 | ``` 337 | 338 | # 匹配异常处理 339 | 340 | 如果 `url` 匹配失败,没有找到任何路由与请求相互匹配,就会切换 `method` 方法,以求任意路由来匹配: 341 | 342 | ```php 343 | protected function checkForAlternateVerbs($request) 344 | { 345 | $methods = array_diff(Router::$verbs, [$request->getMethod()]); 346 | 347 | $others = []; 348 | 349 | foreach ($methods as $method) { 350 | if (! is_null($this->matchAgainstRoutes($this->get($method), $request, false))) { 351 | $others[] = $method; 352 | } 353 | } 354 | 355 | return $others; 356 | } 357 | ``` 358 | 359 | 如果使用其他方法匹配成功,就要判断当前方法是否是 `options`,如果是则直接返回,否则报出异常: 360 | 361 | ```php 362 | protected function getRouteForMethods($request, array $methods) 363 | { 364 | if ($request->method() == 'OPTIONS') { 365 | return (new Route('OPTIONS', $request->path(), function () use ($methods) { 366 | return new Response('', 200, ['Allow' => implode(',', $methods)]); 367 | }))->bind($request); 368 | } 369 | 370 | $this->methodNotAllowed($methods); 371 | } 372 | 373 | protected function methodNotAllowed(array $others) 374 | { 375 | throw new MethodNotAllowedHttpException($others); 376 | } 377 | ``` -------------------------------------------------------------------------------- /Laravel HTTP——重定向的使用与源码分析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | `laravel` 为我们提供便携的重定向功能,可以由门面 `Redirect`,或者全局函数 `redirect()` 来启用,本篇文章将会介绍重定向功能的具体细节及源码分析。 3 | 4 | # URI 重定向 5 | 6 | 重定向功能是由类 `UrlGenerator` 所实现,这个类需要 `request` 来进行初始化: 7 | 8 | ```php 9 | $url = new UrlGenerator( 10 | $routes = new RouteCollection, 11 | $request = Request::create('http://www.foo.com/') 12 | ); 13 | 14 | ``` 15 | 16 | ## 重定向到 uri 17 | 18 | - 当我们想要重定向到某个地址时,可以使用 `to` 函数: 19 | 20 | ```php 21 | $this->assertEquals('http://www.foo.com/foo/bar', $url->to('foo/bar')); 22 | ``` 23 | 24 | - 当我们想要添加额外的路径,可以将数组赋给第二个参数: 25 | 26 | ```php 27 | $this->assertEquals('https://www.foo.com/foo/bar/baz/boom', $url->to('foo/bar', ['baz', 'boom'], true)); 28 | $this->assertEquals('https://www.foo.com/foo/bar/baz?foo=bar', $url->to('foo/bar?foo=bar', ['baz'], true)); 29 | 30 | ``` 31 | ## 强制 https 32 | 33 | 如果我们想要重定向到 `https` ,我们可以设置第三个参数为 `true` : 34 | 35 | ```php 36 | $this->assertEquals('https://www.foo.com/foo/bar', $url->to('foo/bar', [], true)); 37 | ``` 38 | 39 | 或者使用 `forceScheme` 函数: 40 | 41 | ```php 42 | $url->forceScheme('https'); 43 | 44 | $this->assertEquals('https://www.foo.com/foo/bar', $url->to('foo/bar'); 45 | ``` 46 | 47 | ## 强制域名 48 | 49 | ```php 50 | $url->forceRootUrl('https://www.bar.com'); 51 | 52 | $this->assertEquals('https://www.bar.com/foo/bar', $url->to('foo/bar'); 53 | ``` 54 | 55 | ## 路径自定义 56 | 57 | ```php 58 | $url->formatPathUsing(function ($path) { 59 | return '/something'.$path; 60 | }); 61 | 62 | $this->assertEquals('http://www.foo.com/something/foo/bar', $url->to('foo/bar')); 63 | ``` 64 | 65 | # 路由重定向 66 | 67 | 重定向另一个非常重要的功能是重定向到路由所在的地址中去: 68 | 69 | ```php 70 | $route = new Route(['GET'], '/named-route', ['as' => 'plain']); 71 | $routes->add($route); 72 | 73 | $this->assertEquals('http:/www.bar.com/named-route', $url->route('plain')); 74 | ``` 75 | 76 | ## 非域名路径 77 | 78 | `laravel` 路由重定向可以选择重定向后的地址是否仍然带有域名,这个特性由第三个参数决定: 79 | 80 | ```php 81 | $route = new Route(['GET'], '/named-route', ['as' => 'plain']); 82 | $routes->add($route); 83 | 84 | $this->assertEquals('/named-route', $url->route('plain', [], false)); 85 | ``` 86 | 87 | ## 重定向端口号 88 | 89 | 路由重定向可以允许带有 `request` 自己的端口: 90 | 91 | ```php 92 | $url = new UrlGenerator( 93 | $routes = new RouteCollection, 94 | $request = Request::create('http://www.foo.com:8080/') 95 | ); 96 | 97 | $route = new Route(['GET'], 'foo/bar/{baz}', ['as' => 'bar', 'domain' => 'sub.{foo}.com']); 98 | $routes->add($route); 99 | 100 | $this->assertEquals('http://sub.taylor.com:8080/foo/bar/otwell', $url->route('bar', ['taylor', 'otwell'])); 101 | ``` 102 | 103 | ## 重定向路径参数绑定 104 | 105 | 如果路由中含有参数,可以将需要的参数赋给 `route` 第二个参数: 106 | 107 | ```php 108 | $route = new Route(['GET'], 'foo/bar/{baz}', ['as' => 'foobar']); 109 | $routes->add($route); 110 | 111 | $this->assertEquals('http://www.foo.com/foo/bar/taylor', $url->route('foobar', 'taylor')); 112 | ``` 113 | 114 | 也可以根据参数的命名来指定参数绑定: 115 | 116 | ```php 117 | $route = new Route(['GET'], 'foo/bar/{baz}/breeze/{boom}', ['as' => 'bar']); 118 | $routes->add($route); 119 | 120 | $this->assertEquals('http://www.foo.com/foo/bar/otwell/breeze/taylor', $url->route('bar', ['boom' => 'taylor', 'baz' => 'otwell'])); 121 | ``` 122 | 123 | 还可以利用 `defaults` 函数为重定向提供默认的参数来绑定: 124 | 125 | ```php 126 | $url->defaults(['locale' => 'en']); 127 | $route = new Route(['GET'], 'foo', ['as' => 'defaults', 'domain' => '{locale}.example.com', function () { 128 | }]); 129 | $routes->add($route); 130 | 131 | $this->assertEquals('http://en.example.com/foo', $url->route('defaults')); 132 | ``` 133 | 134 | ## 重定向路由 querystring 添加 135 | 136 | 当在 `route` 函数中赋给的参数多于路径参数的时候,多余的参数会被添加到 `querystring` 中: 137 | 138 | ```php 139 | $route = new Route(['GET'], 'foo/bar/{baz}/breeze/{boom}', ['as' => 'bar']); 140 | $routes->add($route); 141 | 142 | $this->assertEquals('http://www.foo.com/foo/bar/taylor/breeze/otwell?fly=wall', $url->route('bar', ['taylor', 'otwell', 'fly' => 'wall'])); 143 | ``` 144 | 145 | ## fragment 重定向 146 | 147 | ```php 148 | $route = new Route(['GET'], 'foo/bar#derp', ['as' => 'fragment']); 149 | $routes->add($route); 150 | 151 | $this->assertEquals('/foo/bar?baz=%C3%A5%CE%B1%D1%84#derp', $url->route('fragment', ['baz' => 'åαф'], false)); 152 | ``` 153 | 154 | ## 路由 action 重定向 155 | 156 | 我们不仅可以通过路由的别名来重定向,还可以利用路由的控制器方法来重定向: 157 | 158 | ```php 159 | $route = new Route(['GET'], 'foo/bam', ['controller' => 'foo@bar']); 160 | $routes->add($route); 161 | 162 | $this->assertEquals('http://www.foo.com/foo/bam', $url->action('foo@bar')); 163 | ``` 164 | 可以设定重定向控制器的默认命名空间: 165 | 166 | ```php 167 | $url->setRootControllerNamespace('namespace'); 168 | 169 | $route = new Route(['GET'], 'foo/bar', ['controller' => 'namespace\foo@bar']); 170 | $routes->add($route); 171 | 172 | $route = new Route(['GET'], 'something/else', ['controller' => 'something\foo@bar']); 173 | $routes->add($route); 174 | 175 | $this->assertEquals('http://www.foo.com/foo/bar', $url->action('foo@bar')); 176 | $this->assertEquals('http://www.foo.com/something/else', $url->action('\something\foo@bar')); 177 | ``` 178 | 179 | ## UrlRoutable 参数绑定 180 | 181 | 可以为重定向传入 `UrlRoutable` 类型的参数,重定向会通过类方法 `getRouteKey` 来获取对象的某个属性,进而绑定到路由的参数中去。 182 | 183 | ```php 184 | public function testRoutableInterfaceRoutingWithSingleParameter() 185 | { 186 | $url = new UrlGenerator( 187 | $routes = new RouteCollection, 188 | $request = Request::create('http://www.foo.com/') 189 | ); 190 | 191 | $route = new Route(['GET'], 'foo/{bar}', ['as' => 'routable']); 192 | $routes->add($route); 193 | 194 | $model = new RoutableInterfaceStub; 195 | $model->key = 'routable'; 196 | 197 | $this->assertEquals('/foo/routable', $url->route('routable', $model, false)); 198 | } 199 | 200 | class RoutableInterfaceStub implements UrlRoutable 201 | { 202 | public $key; 203 | 204 | public function getRouteKey() 205 | { 206 | return $this->{$this->getRouteKeyName()}; 207 | } 208 | 209 | public function getRouteKeyName() 210 | { 211 | return 'key'; 212 | } 213 | } 214 | 215 | ``` 216 | 217 | # URI 重定向源码分析 218 | 219 | 在说重定向的源码之前,我们先了解一下一般的 `uri` 基本组成: 220 | 221 | `scheme://domain:port/path?queryString` 222 | 223 | 也就是说,一般 `uri` 由五部分构成。重定向实际上就是按照各种传入的参数以及属性的设置来重新生成上面的五部分: 224 | 225 | ```php 226 | public function to($path, $extra = [], $secure = null) 227 | { 228 | if ($this->isValidUrl($path)) { 229 | return $path; 230 | } 231 | 232 | $tail = implode('/', array_map( 233 | 'rawurlencode', (array) $this->formatParameters($extra)) 234 | ); 235 | 236 | $root = $this->formatRoot($this->formatScheme($secure)); 237 | 238 | list($path, $query) = $this->extractQueryString($path); 239 | 240 | return $this->format( 241 | $root, '/'.trim($path.'/'.$tail, '/') 242 | ).$query; 243 | } 244 | ``` 245 | 246 | ## 重定向 scheme 247 | 248 | 重定向的 `scheme` 由函数 `formatScheme` 生成: 249 | 250 | ```php 251 | public function formatScheme($secure) 252 | { 253 | if (! is_null($secure)) { 254 | return $secure ? 'https://' : 'http://'; 255 | } 256 | 257 | if (is_null($this->cachedSchema)) { 258 | $this->cachedSchema = $this->forceScheme ?: $this->request->getScheme().'://'; 259 | } 260 | 261 | return $this->cachedSchema; 262 | } 263 | 264 | public function forceScheme($schema) 265 | { 266 | $this->cachedSchema = null; 267 | 268 | $this->forceScheme = $schema.'://'; 269 | } 270 | ``` 271 | 272 | 可以看出来, `scheme` 的生成存在优先级: 273 | 274 | - 由 `to` 传入的 `secure` 参数 275 | - 由 `forceScheme` 设置的 `schema` 参数 276 | - `request` 自带的 `scheme` 277 | 278 | ## 重定向 domain 279 | 280 | 重定向的 `domain` 由函数 `formatRoot` 生成: 281 | 282 | ```php 283 | public function formatRoot($scheme, $root = null) 284 | { 285 | if (is_null($root)) { 286 | if (is_null($this->cachedRoot)) { 287 | $this->cachedRoot = $this->forcedRoot ?: $this->request->root(); 288 | } 289 | 290 | $root = $this->cachedRoot; 291 | } 292 | 293 | $start = Str::startsWith($root, 'http://') ? 'http://' : 'https://'; 294 | 295 | return preg_replace('~'.$start.'~', $scheme, $root, 1); 296 | } 297 | 298 | public function forceRootUrl($root) 299 | { 300 | $this->forcedRoot = rtrim($root, '/'); 301 | 302 | $this->cachedRoot = null; 303 | } 304 | ``` 305 | 306 | 与 `scheme` 类似,`root` 的生成也存在优先级: 307 | 308 | - 由 `to` 传入的 `root` 参数 309 | - 由 `forceRootUrl` 设置的 `root` 参数 310 | - `request` 自带的 `root` 311 | 312 | ## 重定向 path 313 | 314 | 重定向的 `path` 由三部分构成,一部分是 `request` 自带的 `path`,一部分是函数 `to` 原有的 `path` ,另一部分是函数 `to` 传入的参数: 315 | 316 | ```php 317 | public function formatParameters($parameters) 318 | { 319 | $parameters = array_wrap($parameters); 320 | 321 | foreach ($parameters as $key => $parameter) { 322 | if ($parameter instanceof UrlRoutable) { 323 | $parameters[$key] = $parameter->getRouteKey(); 324 | } 325 | } 326 | 327 | return $parameters; 328 | } 329 | 330 | protected function extractQueryString($path) 331 | { 332 | if (($queryPosition = strpos($path, '?')) !== false) { 333 | return [ 334 | substr($path, 0, $queryPosition), 335 | substr($path, $queryPosition), 336 | ]; 337 | } 338 | 339 | return [$path, '']; 340 | } 341 | ``` 342 | 343 | # 路由重定向源码分析 344 | 345 | 相对于 `uri` 的重定向来说,路由重定向的 `scheme`、`root` 、`path`、`queryString` 都要以路由自身的属性为第一优先级,此外还要利用额外参数来绑定路由的 `uri` 参数: 346 | 347 | ```php 348 | public function route($name, $parameters = [], $absolute = true) 349 | { 350 | if (! is_null($route = $this->routes->getByName($name))) { 351 | return $this->toRoute($route, $parameters, $absolute); 352 | } 353 | 354 | throw new InvalidArgumentException("Route [{$name}] not defined."); 355 | } 356 | 357 | public function to($route, $parameters = [], $absolute = false) 358 | { 359 | $domain = $this->getRouteDomain($route, $parameters); 360 | 361 | $uri = $this->addQueryString($this->url->format( 362 | $root = $this->replaceRootParameters($route, $domain, $parameters), 363 | $this->replaceRouteParameters($route->uri(), $parameters) 364 | ), $parameters); 365 | 366 | if (preg_match('/\{.*?\}/', $uri)) { 367 | throw UrlGenerationException::forMissingParameters($route); 368 | } 369 | 370 | $uri = strtr(rawurlencode($uri), $this->dontEncode); 371 | 372 | if (! $absolute) { 373 | return '/'.ltrim(str_replace($root, '', $uri), '/'); 374 | } 375 | 376 | return $uri; 377 | } 378 | ``` 379 | 380 | ## 路由重定向 scheme 381 | 382 | 路由的重定向 `scheme` 需要先判断路由的 `scheme` 属性: 383 | 384 | ```php 385 | protected function getRouteScheme($route) 386 | { 387 | if ($route->httpOnly()) { 388 | return 'http://'; 389 | } elseif ($route->httpsOnly()) { 390 | return 'https://'; 391 | } else { 392 | return $this->url->formatScheme(null); 393 | } 394 | } 395 | ``` 396 | 397 | ## 路由重定向 domain 398 | 399 | ```php 400 | 401 | public function to($route, $parameters = [], $absolute = false) 402 | { 403 | $domain = $this->getRouteDomain($route, $parameters); 404 | 405 | $uri = $this->addQueryString($this->url->format( 406 | $root = $this->replaceRootParameters($route, $domain, $parameters), 407 | $this->replaceRouteParameters($route->uri(), $parameters) 408 | ), $parameters); 409 | ... 410 | } 411 | 412 | protected function getRouteDomain($route, &$parameters) 413 | { 414 | return $route->domain() ? $this->formatDomain($route, $parameters) : null; 415 | } 416 | 417 | protected function formatDomain($route, &$parameters) 418 | { 419 | return $this->addPortToDomain( 420 | $this->getRouteScheme($route).$route->domain() 421 | ); 422 | } 423 | 424 | protected function addPortToDomain($domain) 425 | { 426 | $secure = $this->request->isSecure(); 427 | 428 | $port = (int) $this->request->getPort(); 429 | 430 | return ($secure && $port === 443) || (! $secure && $port === 80) 431 | ? $domain : $domain.':'.$port; 432 | } 433 | 434 | protected function replaceRootParameters($route, $domain, &$parameters) 435 | { 436 | $scheme = $this->getRouteScheme($route); 437 | 438 | return $this->replaceRouteParameters( 439 | $this->url->formatRoot($scheme, $domain), $parameters 440 | ); 441 | } 442 | ``` 443 | 444 | 可以看出路由重定向时,域名的生成主要先经过函数 `getRouteDomain`, 判断路由是否有 `domain` 属性,如果有域名属性,则将会作为 `formatRoot` 函数的参数传入,否则就会默认启动 1`uri` 重定向的域名生成方法。 445 | 446 | ## 路由重定向参数绑定 447 | 448 | 路由重定向可以利用函数 `replaceRootParameters` 在域名当中参数绑定,,也可以在路径当中利用函数 `replaceRouteParameters` 进行参数绑定。参数绑定分为命名参数绑定与匿名参数绑定: 449 | 450 | ```php 451 | protected function replaceRouteParameters($path, array &$parameters) 452 | { 453 | $path = $this->replaceNamedParameters($path, $parameters); 454 | 455 | $path = preg_replace_callback('/\{.*?\}/', function ($match) use (&$parameters) { 456 | return (empty($parameters) && ! Str::endsWith($match[0], '?}')) 457 | ? $match[0] 458 | : array_shift($parameters); 459 | }, $path); 460 | 461 | return trim(preg_replace('/\{.*?\?\}/', '', $path), '/'); 462 | } 463 | ``` 464 | 465 | 对于命名参数绑定,程序会分别从变量列表、默认变量列表中获取并替换路由参数对应的数值,若不存在该参数,则直接返回: 466 | 467 | ```php 468 | protected function replaceNamedParameters($path, &$parameters) 469 | { 470 | return preg_replace_callback('/\{(.*?)\??\}/', function ($m) use (&$parameters) { 471 | if (isset($parameters[$m[1]])) { 472 | return Arr::pull($parameters, $m[1]); 473 | } elseif (isset($this->defaultParameters[$m[1]])) { 474 | return $this->defaultParameters[$m[1]]; 475 | } else { 476 | return $m[0]; 477 | } 478 | }, $path); 479 | } 480 | ``` 481 | 命名参数绑定结束后,剩下的未被替换的路由参数将会被未命名的变量按顺序来替换。 482 | 483 | ## 路由重定向 queryString 484 | 485 | 如果变量列表在绑定路由后仍然有剩余,那么变量将会作为路由的 `queryString`: 486 | 487 | ```php 488 | protected function addQueryString($uri, array $parameters) 489 | { 490 | if (! is_null($fragment = parse_url($uri, PHP_URL_FRAGMENT))) { 491 | $uri = preg_replace('/#.*/', '', $uri); 492 | } 493 | 494 | $uri .= $this->getRouteQueryString($parameters); 495 | 496 | return is_null($fragment) ? $uri : $uri."#{$fragment}"; 497 | } 498 | 499 | protected function getRouteQueryString(array $parameters) 500 | { 501 | if (count($parameters) == 0) { 502 | return ''; 503 | } 504 | 505 | $query = http_build_query( 506 | $keyed = $this->getStringParameters($parameters) 507 | ); 508 | 509 | 510 | if (count($keyed) < count($parameters)) { 511 | $query .= '&'.implode( 512 | '&', $this->getNumericParameters($parameters) 513 | ); 514 | } 515 | 516 | return '?'.trim($query, '&'); 517 | } 518 | ``` 519 | 520 | ## 路由重定向结束 521 | 522 | 路由 `uri` 构建完成后,将会继续判断是否存在违背绑定的路由参数,是否显示 `absolute` 的路由地址 523 | 524 | ```php 525 | public function to($route, $parameters = [], $absolute = false) 526 | { 527 | ... 528 | if (preg_match('/\{.*?\}/', $uri)) { 529 | throw UrlGenerationException::forMissingParameters($route); 530 | } 531 | 532 | $uri = strtr(rawurlencode($uri), $this->dontEncode); 533 | 534 | if (! $absolute) { 535 | return '/'.ltrim(str_replace($root, '', $uri), '/'); 536 | } 537 | 538 | return $uri; 539 | } 540 | ``` -------------------------------------------------------------------------------- /Laravel Providers——服务提供者的注册与启动源码解析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 服务提供者是 `laravel` 框架的重要组成部分,承载着各种服务,自定义的应用以及所有 `Laravel` 的核心服务都是通过服务提供者启动。本文将会介绍服务提供者的源码分析,关于服务提供者的使用,请参考官方文档 :[服务提供者](http://laravelacademy.org/post/6703.html)。 4 | 5 | # 服务提供者的注册 6 | 7 | 服务提供者的启动由类 `\Illuminate\Foundation\Bootstrap\RegisterProviders::class` 负责,该类用于加载所有服务提供者的 `register` 函数,并保存延迟加载的服务的信息,以便实现延迟加载。 8 | 9 | ```php 10 | class RegisterProviders 11 | { 12 | public function bootstrap(Application $app) 13 | { 14 | $app->registerConfiguredProviders(); 15 | } 16 | } 17 | 18 | class Application extends Container implements ApplicationContract, HttpKernelInterface 19 | { 20 | public function registerConfiguredProviders() 21 | { 22 | (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) 23 | ->load($this->config['app.providers']); 24 | } 25 | 26 | public function getCachedServicesPath() 27 | { 28 | return $this->bootstrapPath().'/cache/services.php'; 29 | } 30 | } 31 | ``` 32 | 以上可以看出,所有服务提供者都在配置文件 `app.php` 文件的 `providers` 数组中。类 `ProviderRepository` 负责所有的服务加载功能: 33 | 34 | ```php 35 | class ProviderRepository 36 | { 37 | public function load(array $providers) 38 | { 39 | $manifest = $this->loadManifest(); 40 | 41 | if ($this->shouldRecompile($manifest, $providers)) { 42 | $manifest = $this->compileManifest($providers); 43 | } 44 | 45 | foreach ($manifest['when'] as $provider => $events) { 46 | $this->registerLoadEvents($provider, $events); 47 | } 48 | 49 | foreach ($manifest['eager'] as $provider) { 50 | $this->app->register($provider); 51 | } 52 | 53 | $this->app->addDeferredServices($manifest['deferred']); 54 | } 55 | } 56 | ``` 57 | 58 | ## 加载服务缓存文件 59 | 60 | `laravel` 会把所有的服务整理起来,作为缓存写在缓存文件中: 61 | 62 | ```php 63 | return array ( 64 | 'providers' => 65 | array ( 66 | 0 => 'Illuminate\\Auth\\AuthServiceProvider', 67 | 1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', 68 | ... 69 | ), 70 | 71 | 'eager' => 72 | array ( 73 | 0 => 'Illuminate\\Auth\\AuthServiceProvider', 74 | 1 => 'Illuminate\\Cookie\\CookieServiceProvider', 75 | ... 76 | ), 77 | 78 | 'deferred' => 79 | array ( 80 | 'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', 81 | 'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', 82 | ... 83 | ), 84 | 85 | 'when' => 86 | array ( 87 | 'Illuminate\\Broadcasting\\BroadcastServiceProvider' => 88 | array ( 89 | ), 90 | ... 91 | ), 92 | ``` 93 | 94 | - 缓存文件中 `providers` 放入了所有自定义和框架核心的服务。 95 | - `eager` 数组中放入了所有需要立即启动的服务提供者。 96 | - `deferred` 数组中放入了所有需要延迟加载的服务提供者。 97 | - `when` 放入了延迟加载需要激活的事件。 98 | 99 | 加载服务提供者缓存文件: 100 | 101 | ```php 102 | public function loadManifest() 103 | { 104 | if ($this->files->exists($this->manifestPath)) { 105 | $manifest = $this->files->getRequire($this->manifestPath); 106 | 107 | if ($manifest) { 108 | return array_merge(['when' => []], $manifest); 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | ## 编译服务提供者 115 | 116 | 若 `laravel` 中的服务提供者没有缓存文件或者有变动,那么就会重新生成缓存文件: 117 | 118 | ```php 119 | public function shouldRecompile($manifest, $providers) 120 | { 121 | return is_null($manifest) || $manifest['providers'] != $providers; 122 | } 123 | 124 | protected function compileManifest($providers) 125 | { 126 | $manifest = $this->freshManifest($providers); 127 | 128 | foreach ($providers as $provider) { 129 | $instance = $this->createProvider($provider); 130 | 131 | if ($instance->isDeferred()) { 132 | foreach ($instance->provides() as $service) { 133 | $manifest['deferred'][$service] = $provider; 134 | } 135 | 136 | $manifest['when'][$provider] = $instance->when(); 137 | } 138 | 139 | else { 140 | $manifest['eager'][] = $provider; 141 | } 142 | } 143 | 144 | return $this->writeManifest($manifest); 145 | } 146 | 147 | protected function freshManifest(array $providers) 148 | { 149 | return ['providers' => $providers, 'eager' => [], 'deferred' => []]; 150 | } 151 | ``` 152 | - 如果服务提供者是需要立即注册的,那么将会放入缓存文件中 `eager` 数组中。 153 | - 如果服务提供者是延迟加载的,那么其函数 `provides()` 通常会提供服务别名,这个服务别名通常是向服务容器中注册的别名,别名将会放入缓存文件的 `deferred` 数组中。 154 | - 延迟加载若有 `event` 事件激活,那么可以在 `when` 函数中写入事件类,并写入缓存文件的 `when` 数组中。 155 | 156 | ## 延迟服务提供者事件注册 157 | 158 | 延迟服务提供者除了利用 `IOC` 容器解析服务方式激活,还可以利用 `Event` 事件来激活: 159 | 160 | ```php 161 | protected function registerLoadEvents($provider, array $events) 162 | { 163 | if (count($events) < 1) { 164 | return; 165 | } 166 | 167 | $this->app->make('events')->listen($events, function () use ($provider) { 168 | $this->app->register($provider); 169 | }); 170 | } 171 | ``` 172 | 173 | ## 注册即时启动的服务提供者 174 | 175 | 服务提供者的注册函数 `register()` 由类 `Application` 来调用: 176 | 177 | ```php 178 | class Application extends Container implements ApplicationContract, HttpKernelInterface 179 | { 180 | public function register($provider, $options = [], $force = false) 181 | { 182 | if (($registered = $this->getProvider($provider)) && ! $force) { 183 | return $registered; 184 | } 185 | 186 | if (is_string($provider)) { 187 | $provider = $this->resolveProvider($provider); 188 | } 189 | 190 | if (method_exists($provider, 'register')) { 191 | $provider->register(); 192 | } 193 | 194 | $this->markAsRegistered($provider); 195 | 196 | if ($this->booted) { 197 | $this->bootProvider($provider); 198 | } 199 | 200 | return $provider; 201 | } 202 | 203 | public function getProvider($provider) 204 | { 205 | $name = is_string($provider) ? $provider : get_class($provider); 206 | 207 | return Arr::first($this->serviceProviders, function ($value) use ($name) { 208 | return $value instanceof $name; 209 | }); 210 | } 211 | 212 | public function resolveProvider($provider) 213 | { 214 | return new $provider($this); 215 | } 216 | 217 | protected function markAsRegistered($provider) 218 | { 219 | $this->serviceProviders[] = $provider; 220 | 221 | $this->loadedProviders[get_class($provider)] = true; 222 | } 223 | 224 | protected function bootProvider(ServiceProvider $provider) 225 | { 226 | if (method_exists($provider, 'boot')) { 227 | return $this->call([$provider, 'boot']); 228 | } 229 | } 230 | } 231 | ``` 232 | 233 | 可以看出,服务提供者的注册过程: 234 | 235 | - 判断当前服务提供者是否被注册过,如注册过直接返回对象 236 | - 解析服务提供者 237 | - 调用服务提供者的 `register` 函数 238 | - 标记当前服务提供者已经注册完毕 239 | - 若框架已经加载注册完毕所有的服务容器,那么就启动服务提供者的 `boot` 函数,该函数由于是 `call` 调用,所以支持依赖注入。 240 | 241 | ## 延迟服务提供者激活与注册 242 | 243 | 延迟服务提供者首先需要添加到 `Application` 中: 244 | 245 | ```php 246 | public function addDeferredServices(array $services) 247 | { 248 | $this->deferredServices = array_merge($this->deferredServices, $services); 249 | } 250 | ``` 251 | 252 | 我们之前说过,延迟服务提供者的激活注册有两种方法:事件与服务解析。 253 | 254 | 当特定的事件被激发后,就会调用 `Application` 的 `register` 函数,进而调用服务提供者的 `register` 函数,实现服务的注册。 255 | 256 | 当利用 `Ioc` 容器解析服务名时,例如解析服务名 `BroadcastingFactory`: 257 | 258 | ```php 259 | class BroadcastServiceProvider extends ServiceProvider 260 | { 261 | protected $defer = true; 262 | 263 | public function provides() 264 | { 265 | return [ 266 | BroadcastManager::class, 267 | BroadcastingFactory::class, 268 | BroadcasterContract::class, 269 | ]; 270 | } 271 | } 272 | ``` 273 | 274 | ```php 275 | public function make($abstract) 276 | { 277 | $abstract = $this->getAlias($abstract); 278 | 279 | if (isset($this->deferredServices[$abstract])) { 280 | $this->loadDeferredProvider($abstract); 281 | } 282 | 283 | return parent::make($abstract); 284 | } 285 | 286 | public function loadDeferredProvider($service) 287 | { 288 | if (! isset($this->deferredServices[$service])) { 289 | return; 290 | } 291 | 292 | $provider = $this->deferredServices[$service]; 293 | 294 | if (! isset($this->loadedProviders[$provider])) { 295 | $this->registerDeferredProvider($provider, $service); 296 | } 297 | } 298 | ``` 299 | 300 | 由 `deferredServices` 数组可以得知,`BroadcastingFactory` 为延迟服务,接着程序会利用函数 `loadDeferredProvider` 来加载延迟服务提供者,调用服务提供者的 `register` 函数,若当前的框架还未注册完全部服务。那么将会放入服务启动的回调函数中,以待服务启动时调用: 301 | 302 | ```php 303 | public function registerDeferredProvider($provider, $service = null) 304 | { 305 | if ($service) { 306 | unset($this->deferredServices[$service]); 307 | } 308 | 309 | $this->register($instance = new $provider($this)); 310 | 311 | if (! $this->booted) { 312 | $this->booting(function () use ($instance) { 313 | $this->bootProvider($instance); 314 | }); 315 | } 316 | } 317 | ``` 318 | 319 | 关于服务提供者的注册函数: 320 | 321 | ```php 322 | class BroadcastServiceProvider extends ServiceProvider 323 | { 324 | protected $defer = true; 325 | 326 | public function register() 327 | { 328 | $this->app->singleton(BroadcastManager::class, function ($app) { 329 | return new BroadcastManager($app); 330 | }); 331 | 332 | $this->app->singleton(BroadcasterContract::class, function ($app) { 333 | return $app->make(BroadcastManager::class)->connection(); 334 | }); 335 | 336 | $this->app->alias( 337 | BroadcastManager::class, BroadcastingFactory::class 338 | ); 339 | } 340 | 341 | public function provides() 342 | { 343 | return [ 344 | BroadcastManager::class, 345 | BroadcastingFactory::class, 346 | BroadcasterContract::class, 347 | ]; 348 | } 349 | } 350 | 351 | ``` 352 | 353 | 函数 `register` 为类 `BroadcastingFactory` 向 `Ioc` 容器绑定了特定的实现类 `BroadcastManager`,这样 `Ioc` 容器中的 `make` 函数: 354 | 355 | ```php 356 | public function make($abstract) 357 | { 358 | $abstract = $this->getAlias($abstract); 359 | 360 | if (isset($this->deferredServices[$abstract])) { 361 | $this->loadDeferredProvider($abstract); 362 | } 363 | 364 | return parent::make($abstract); 365 | } 366 | ``` 367 | 368 | `parent::make($abstract)` 就会正确的解析服务 `BroadcastingFactory`。 369 | 370 | 因此函数 `provides()` 返回的元素一定都是 `register()` 向 `IOC` 容器中绑定的类名或者别名。这样当我们利用服务容器来利用 `App::make()` 解析这些类名的时候,服务容器才会根据服务提供者的 `register()` 函数中绑定的实现类,从而正确解析服务功能。 371 | 372 | # 服务容器的启动 373 | 374 | 服务容器的启动由类 `\Illuminate\Foundation\Bootstrap\BootProviders::class` 负责: 375 | 376 | ```php 377 | class BootProviders 378 | { 379 | public function bootstrap(Application $app) 380 | { 381 | $app->boot(); 382 | } 383 | } 384 | 385 | class Application extends Container implements ApplicationContract, HttpKernelInterface 386 | { 387 | public function boot() 388 | { 389 | if ($this->booted) { 390 | return; 391 | } 392 | 393 | $this->fireAppCallbacks($this->bootingCallbacks); 394 | 395 | array_walk($this->serviceProviders, function ($p) { 396 | $this->bootProvider($p); 397 | }); 398 | 399 | $this->booted = true; 400 | 401 | $this->fireAppCallbacks($this->bootedCallbacks); 402 | } 403 | 404 | protected function bootProvider(ServiceProvider $provider) 405 | { 406 | if (method_exists($provider, 'boot')) { 407 | return $this->call([$provider, 'boot']); 408 | } 409 | } 410 | } 411 | ``` 412 | 413 | -------------------------------------------------------------------------------- /PHP Composer-——-注册与运行源码分析.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | --- 3 |   [上一篇](http://leoyang90.cn/2017/03/13/Composer%20Autoload%20Source%20Reading%20%E2%80%94%E2%80%94%20Start%20and%20Initialize/)文章我们讲到了Composer自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,换句话说,如果有命名空间'App\\Console\\Kernel,我们已经知道了App\\对应的目录,接下来我们就要解决下面的就是\\Console\\Kernel这一段。 4 | 5 | # Composer自动加载源码分析——注册 6 | --- 7 |   我们先回顾一下自动加载引导类: 8 | 9 | ```php 10 | public static function getLoader() 11 | { 12 | /***************************经典单例模式********************/ 13 | if (null !== self::$loader) { 14 | return self::$loader; 15 | } 16 | 17 | /***********************获得自动加载核心类对象********************/ 18 | spl_autoload_register(array('ComposerAutoloaderInit 19 | 832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'), true, true); 20 | 21 | self::$loader = $loader = new \Composer\Autoload\ClassLoader(); 22 | 23 | spl_autoload_unregister(array('ComposerAutoloaderInit 24 | 832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader')); 25 | 26 | /***********************初始化自动加载核心类对象********************/ 27 | $useStaticLoader = PHP_VERSION_ID >= 50600 && 28 | !defined('HHVM_VERSION'); 29 | 30 | if ($useStaticLoader) { 31 | require_once __DIR__ . '/autoload_static.php'; 32 | 33 | call_user_func(\Composer\Autoload\ComposerStaticInit 34 | 832ea71bfb9a4128da8660baedaac82e::getInitializer($loader)); 35 | 36 | } else { 37 | $map = require __DIR__ . '/autoload_namespaces.php'; 38 | foreach ($map as $namespace => $path) { 39 | $loader->set($namespace, $path); 40 | } 41 | 42 | $map = require __DIR__ . '/autoload_psr4.php'; 43 | foreach ($map as $namespace => $path) { 44 | $loader->setPsr4($namespace, $path); 45 | } 46 | 47 | $classMap = require __DIR__ . '/autoload_classmap.php'; 48 | if ($classMap) { 49 | $loader->addClassMap($classMap); 50 | } 51 | } 52 | 53 | /***********************注册自动加载核心类对象********************/ 54 | $loader->register(true); 55 | 56 | /***********************自动加载全局函数********************/ 57 | if ($useStaticLoader) { 58 | $includeFiles = Composer\Autoload\ComposerStaticInit 59 | 832ea71bfb9a4128da8660baedaac82e::$files; 60 | } else { 61 | $includeFiles = require __DIR__ . '/autoload_files.php'; 62 | } 63 | 64 | foreach ($includeFiles as $fileIdentifier => $file) { 65 | composerRequire 66 | 832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file); 67 | } 68 | 69 | return $loader; 70 | } 71 | ``` 72 | 现在我们开始引导类的第四部分:注册自动加载核心类对象。我们来看看核心类的register()函数: 73 | 74 | ```php 75 | public function register($prepend = false) 76 | { 77 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 78 | } 79 | ``` 80 | 简单到爆炸啊!一行代码实现自动加载有木有!其实奥秘都在自动加载核心类ClassLoader的loadClass()函数上,这个函数负责按照PSR标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将'App\\Console\\Kernel中'Console\\Kernel这一段转为目录,至于怎么转的我们在下面“Composer自动加载源码分析——运行”讲。核心类ClassLoader将loadClass()函数注册到PHP SPL中的spl_autoload_register()里面去,这个函数的来龙去脉我们之前[文章](http://leoyang90.cn/2017/03/11/PHP-Composer-autoload/)讲过。这样,每当PHP遇到一个不认识的命名空间的时候,PHP会自动调用注册到spl_autoload_register里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。 81 | 82 | # Composer自动加载源码分析——全局函数的自动加载 83 |   Composer不止可以自动加载命名空间,还可以加载全局函数。怎么实现的呢?很简单,把全局函数写到特定的文件里面去,在程序运行前挨个require就行了。这个就是composer自动加载的第五步,加载全局函数。 84 | 85 | ```php 86 | if ($useStaticLoader) { 87 | $includeFiles = Composer\Autoload\ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files; 88 | } else { 89 | $includeFiles = require __DIR__ . '/autoload_files.php'; 90 | } 91 | foreach ($includeFiles as $fileIdentifier => $file) { 92 | composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file); 93 | } 94 | ``` 95 | 跟核心类的初始化一样,全局函数自动加载也分为两种:静态初始化和普通初始化,静态加载只支持PHP5.6以上并且不支持HHVM。 96 | ## 静态初始化: 97 | ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files: 98 | 99 | ```php 100 | public static $files = array ( 101 | '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', 102 | '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', 103 | ... 104 | ); 105 | ``` 106 | 看到这里我们可能又要有疑问了,为什么不直接放文件路径名,还要一个hash干什么呢?这个我们一会儿讲,我们这里先了解一下这个数组的结构。 107 | 108 | ## 普通初始化 109 | 110 | autoload_files: 111 | 112 | ```php 113 | $vendorDir = dirname(dirname(__FILE__)); 114 | $baseDir = dirname($vendorDir); 115 | 116 | return array( 117 | '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', 118 | '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', 119 | .... 120 | ); 121 | ``` 122 | 其实跟静态初始化区别不大。 123 | 124 | ## 加载全局函数 125 | 126 | ```php 127 | class ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e{ 128 | public static function getLoader(){ 129 | ... 130 | foreach ($includeFiles as $fileIdentifier => $file) { 131 | composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file); 132 | } 133 | ... 134 | } 135 | } 136 | 137 | function composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file) 138 | { 139 | if (empty(\$GLOBALS['__composer_autoload_files'][\$fileIdentifier])) { 140 | require $file; 141 | 142 | $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; 143 | } 144 | } 145 | ``` 146 | 147 |   这一段很有讲究,**第一个问题**:为什么自动加载引导类的getLoader()函数不直接require \$includeFiles里面的每个文件名,而要用类外面的函数composerRequire832ea71bfb9a4128da8660baedaac82e0?(顺便说下这个函数名hash仍然为了避免和用户定义函数冲突)因为怕有人在全局函数所在的文件**写\$this或者self**。 148 |   假如\$includeFiles有个app/helper.php文件,这个helper.php文件的函数外有一行代码:\$this->foo(),如果引导类在getLoader()函数直接require(\$file),那么引导类就会运行这句代码,调用自己的foo()函数,这显然是错的。事实上helper.php就不应该出现\$this或self这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生,第一种情况:引导类恰好有foo()函数,那么就会莫名其妙执行了引导类的foo();第二种情况:引导类没有foo()函数,但是却甩出来引导类没有foo()方法这样的错误提示,用户不知道自己哪里错了。把require语句放到引导类的外面,遇到$this或者self,程序就会告诉用户根本没有类,$this或self无效,错误信息更加明朗。 149 |   **第二个问题**,为什么要用hash作为\$fileIdentifier,上面的代码明显可以看出来这个变量是用来控制全局函数只被require一次的,那为什么不用require_once呢?事实上require_once比require效率低很多,使用全局变量\$GLOBALS这样控制加载会更快。还有一个原因我猜测应该是require_once对相对路径的支持并不理想,所以composer尽量少用require_once。 150 | 151 | # Composer自动加载源码分析——运行 152 |   我们终于来到了核心的核心——composer自动加载的真相,命名空间如何通过composer转为对应目录文件的奥秘就在这一章。 153 |   前面说过,ClassLoader的register()函数将loadClass()函数注册到PHP的SPL函数堆栈中,每当PHP遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以loadClass()函数就是自动加载的关键 154 | 了。 155 | loadClass(): 156 | 157 | ```php 158 | public function loadClass($class) 159 | { 160 | if ($file = $this->findFile($class)) { 161 | includeFile($file); 162 | 163 | return true; 164 | } 165 | } 166 | 167 | public function findFile($class) 168 | { 169 | // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 170 | if ('\\' == $class[0]) { 171 | $class = substr($class, 1); 172 | } 173 | 174 | // class map lookup 175 | if (isset($this->classMap[$class])) { 176 | return $this->classMap[$class]; 177 | } 178 | if ($this->classMapAuthoritative) { 179 | return false; 180 | } 181 | 182 | $file = $this->findFileWithExtension($class, '.php'); 183 | 184 | // Search for Hack files if we are running on HHVM 185 | if ($file === null && defined('HHVM_VERSION')) { 186 | $file = $this->findFileWithExtension($class, '.hh'); 187 | } 188 | 189 | if ($file === null) { 190 | // Remember that this class does not exist. 191 | return $this->classMap[$class] = false; 192 | } 193 | 194 | return $file; 195 | } 196 | ``` 197 |   我们看到loadClass(),主要调用findFile()函数。findFile()在解析命名空间的时候主要分为两部分:classMap和findFileWithExtension()函数。classMap很简单,直接看命名空间是否在映射数组中即可。麻烦的是findFileWithExtension()函数,这个函数包含了PSR0和PSR4标准的实现。还有个值得我们注意的是查找路径成功后includeFile()仍然类外面的函数,并不是ClassLoader的成员函数,原理跟上面一样,防止有用户写$this或self。还有就是如果命名空间是以\\开头的,要去掉\\然后再匹配。 198 | findFileWithExtension: 199 | 200 | ```php 201 | private function findFileWithExtension($class, $ext) 202 | { 203 | // PSR-4 lookup 204 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 205 | 206 | $first = $class[0]; 207 | if (isset($this->prefixLengthsPsr4[$first])) { 208 | foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { 209 | if (0 === strpos($class, $prefix)) { 210 | foreach ($this->prefixDirsPsr4[$prefix] as $dir) { 211 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { 212 | return $file; 213 | } 214 | } 215 | } 216 | } 217 | } 218 | 219 | // PSR-4 fallback dirs 220 | foreach ($this->fallbackDirsPsr4 as $dir) { 221 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 222 | return $file; 223 | } 224 | } 225 | 226 | // PSR-0 lookup 227 | if (false !== $pos = strrpos($class, '\\')) { 228 | // namespaced class name 229 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 230 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 231 | } else { 232 | // PEAR-like class name 233 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 234 | } 235 | 236 | if (isset($this->prefixesPsr0[$first])) { 237 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 238 | if (0 === strpos($class, $prefix)) { 239 | foreach ($dirs as $dir) { 240 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 241 | return $file; 242 | } 243 | } 244 | } 245 | } 246 | } 247 | 248 | // PSR-0 fallback dirs 249 | foreach ($this->fallbackDirsPsr0 as $dir) { 250 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 251 | return $file; 252 | } 253 | } 254 | 255 | // PSR-0 include paths. 256 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 257 | return $file; 258 | } 259 | } 260 | ``` 261 |   下面我们通过举例来说下上面代码的流程: 262 |   如果我们在代码中写下'phpDocumentor\\Reflection\\example',PHP会通过SPL调用loadClass->findFile->findFileWithExtension。首先默认用php作为文件后缀名调用findFileWithExtension函数里,利用PSR4标准尝试解析目录文件,如果文件不存在则继续用PSR0标准解析,如果解析出来的目录文件仍然不存在,但是环境是HHVM虚拟机,继续用后缀名为hh再次调用findFileWithExtension函数,如果不存在,说明此命名空间无法加载,放到classMap中设为false,以便以后更快地加载。 263 |   对于phpDocumentor\\Reflection\\example,当尝试利用PSR4标准映射目录时,步骤如下: 264 | 265 | ## PSR4标准加载 266 | > - 将\\转为文件分隔符/,加上后缀php或hh,得到\$logicalPathPsr4即phpDocumentor//Reflection//example.php(hh); 267 | > - 利用命名空间第一个字母p作为前缀索引搜索prefixLengthsPsr4数组,查到下面这个数组: 268 | 269 | ```php 270 | p' => 271 | array ( 272 | 'phpDocumentor\\Reflection\\' => 25, 273 | 'phpDocumentor\\Fake\\' => 19, 274 | ) 275 | ``` 276 | 277 | > - 遍历这个数组,得到两个顶层命名空间phpDocumentor\\Reflection\\和phpDocumentor\\Fake\\ 278 | > - 用这两个顶层命名空间与phpDocumentor\\Reflection\\example_e相比较,可以得到phpDocumentor\\Reflection\\这个顶层命名空间 279 | > - 在prefixLengthsPsr4映射数组中得到phpDocumentor\\Reflection\\长度为25。 280 | > - 在prefixDirsPsr4映射数组中得到phpDocumentor\\Reflection\\的目录映射为: 281 | 282 | ```php 283 | 'phpDocumentor\\Reflection\\' => 284 | array ( 285 | 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src', 286 | 1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src', 287 | 2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src', 288 | ), 289 | ``` 290 | > - 遍历这个映射数组,得到三个目录映射; 291 | > - 查看 “目录+文件分隔符//+substr(\$logicalPathPsr4, \$length)”文件是否存在,存在即返回。这里就是'\__DIR\__/../phpdocumentor/reflection-common/src + /+ substr(phpDocumentor/Reflection/example_e.php(hh),25)' 292 | > - 如果失败,则利用fallbackDirsPsr4数组里面的目录继续判断是否存在文件,具体方法是“目录+文件分隔符//+\$logicalPathPsr4” 293 | 294 | ## PSR0标准加载 295 | 如果PSR4标准加载失败,则要进行PSR0标准加载: 296 | > - 找到phpDocumentor\\Reflection\\example_e最后“\\”的位置,将其后面文件名中’‘_’‘字符转为文件分隔符“/”,得到$logicalPathPsr0即phpDocumentor/Reflection/example/e.php(hh) 297 | > 利用命名空间第一个字母p作为前缀索引搜索prefixLengthsPsr4数组,查到下面这个数组: 298 | 299 | ```php 300 | 'P' => 301 | array ( 302 | 'Prophecy\\' => 303 | array ( 304 | 0 => __DIR__ . '/..' . '/phpspec/prophecy/src', 305 | ), 306 | 'phpDocumentor' => 307 | array ( 308 | 0 => __DIR__ . '/..' . '/erusev/parsedown', 309 | ), 310 | ), 311 | ``` 312 | 313 | > - 遍历这个数组,得到两个顶层命名空间phpDocumentor和Prophecy 314 | > - 用这两个顶层命名空间与phpDocumentor\\Reflection\\example_e相比较,可以得到phpDocumentor这个顶层命名空间 315 | > - 在映射数组中得到phpDocumentor目录映射为'\__DIR\__ . '/..' . '/erusev/parsedown' 316 | >- 查看 “目录+文件分隔符//+\$logicalPathPsr0”文件是否存在,存在即返回。这里就是 317 | > “\__DIR\__ . '/..' . '/erusev/parsedown + //+ phpDocumentor//Reflection//example/e.php(hh)” 318 | > - 如果失败,则利用fallbackDirsPsr0数组里面的目录继续判断是否存在文件,具体方法是“目录+文件分隔符//+\$logicalPathPsr0” 319 | > - 如果仍然找不到,则利用stream_resolve_include_path(),在当前include目录寻找该文件,如果找到返回绝对路径。 320 | 321 | # 结语 322 |   经过三篇文章,终于写完了PHP Composer自动加载的原理与实现,结下来我们开始讲解laravel框架下的门面Facade,这个门面功能和自动加载有着一些联系. -------------------------------------------------------------------------------- /PHP Composer——自动加载原理.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 这篇文章是对 `PHP自动加载功能` 的一个总结,内容涉及 `PHP自动加载功能` 、`PHP命名空间`、`PSR0/PSR4标准` 等内容。 4 | 5 | # 一、PHP 自动加载功能 6 | 7 | ## PHP 自动加载功能的由来 8 | 9 | 在 PHP 开发过程中,如果希望从外部引入一个 Class ,通常会使用 `include` 和 `require` 方法,去把定义这个 Class 的文件包含进来。这个在小规模开发的时候,没什么大问题。但在大型的开发项目中,使用这种方式会带来一些隐含的问题:如果一个 PHP 文件需要使用很多其它类,那么就需要很多的 `require/include` 语句,这样有可能会 **造成遗漏** 或者 **包含进不必要的类文件**。如果大量的文件都需要使用其它的类,那么要保证每个文件都包含正确的类文件肯定是一个噩梦, 况且 require_once 的代价很大。 10 | 11 | PHP5 为这个问题提供了一个解决方案,这就是 `类的自动加载(autoload)机制`。`autoload机制` 可以使得 PHP 程序有可能在使用类时才自动包含类文件,而不是一开始就将所有的类文件`include`进来,这种机制也称为 `Lazy loading (延迟加载)`。 12 | 13 | * 总结起来,自动加载功能带来了几处优点: 14 | 15 | > 1. 使用类之前无需 `include / require` 16 | > 2. 使用类的时候才会 `include / require` 文件,实现了 `lazy loading` ,避免了 `include / require` 多余文件。 17 | > 3. 无需考虑引入 **类的实际磁盘地址** ,实现了逻辑和实体文件的分离。 18 | 19 | 20 | * 如果想具体详细的了解关于自动加载的功能,可以查看资料: 21 | >[PHP 类自动加载机制](http://blog.csdn.net/hguisu/article/details/7463333) 22 | > 23 | >[PHP autoload机制的实现解析](http://www.jb51.net/article/31279.htm) 24 | 25 | 26 | ## PHP 自动加载函数 __autoload() 27 | 28 | 29 | * 通常 PHP5 在使用一个类时,如果发现这个类没有加载,就会自动运行 __autoload() 函数,这个函数是我们在程序中自定义的,在这个函数中我们可以加载需要使用的类。下面是个简单的示例: 30 | ```php 31 | 从这个例子中,我们可以看出 __autoload 至少要做三件事情: 40 | > 1. 根据类名确定类文件名; 41 | > 42 | > 2. 确定类文件所在的磁盘路径 ``(在我们的例子是最简单的情况,类与调用它们的PHP程序文件在同一个文件夹下)``; 43 | > 44 | > 3. 将类从磁盘文件中加载到系统中。 45 | 46 | 47 | * 第三步最简单,只需要使用 `include / require` 即可。要实现第一步,第二步的功能,必须在开发时约定类名与磁盘文件的映射方法,只有这样我们才能根据类名找到它对应的磁盘文件。 48 | 49 | * 当有大量的类文件要包含的时候,我们只要确定相应的规则,然后在 **`__autoload()` 函数中,将类名与实际的磁盘文件对应起来,就可以实现 `lazy loading` 的效果** 。从这里我们也可以看出 `__autoload()` 函数的实现中最重要的是类名与实际的磁盘文件映射规则的实现。 50 | 51 | ## __autoload() 函数存在的问题 52 | 53 | * 如果在一个系统的实现中,如果需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的, 其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须 **在__autoload()函数中将所有的映射规则全部实现**,这样的话 `__autoload()` 函数有可能会非常复杂,甚至无法实现。最后可能会导致 `__autoload()` 函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。 54 | 55 | * 那么问题出现在哪里呢?问题出现在 **__autoload() 是全局函数只能定义一次** ,不够灵活,所以所有的类名与文件名对应的逻辑规则都要在一个函数里面实现,造成这个函数的臃肿。那么如何来解决这个问题呢?答案就是使用一个 **__autoload调用堆栈** ,不同的映射关系写到不同的 `__autoload函数` 中去,然后统一注册统一管理,这个就是PHP5引入的 `SPL Autoload` 。 56 | 57 | ## SPL Autoload 58 | 59 | * SPL是Standard PHP Library(标准PHP库)的缩写。它是PHP5引入的一个扩展库,其主要功能包括autoload机制的实现及包括各种Iterator接口或类。SPL Autoload具体有几个函数: 60 | > 1. *spl_autoload_register:注册__autoload()函数* 61 | > 2. *spl_autoload_unregister:注销已注册的函数* 62 | > 3. *spl_autoload_functions:返回所有已注册的函数* 63 | > 4. *spl_autoload_call:尝试所有已注册的函数来加载类* 64 | > 5. *spl_autoload :__autoload()的默认实现* 65 | > 6. *spl_autoload_extionsions: 注册并返回spl_autoload函数使用的默认文件扩展名。* 66 | 67 | # todo Continue 68 | # todo Continue 69 | # todo Continue 70 | # todo Continue 71 | # todo Continue 72 | # todo Continue 73 | 74 |    75 |   这几个函数具体详细用法可见 [php中spl_autoload详解](http://www.jb51.net/article/56370.htm) 76 | 77 |   简单来说,spl_autoload 就是 SPL 自己的定义 `__autoload()`函数,功能很简单,就是去注册的目录(由set_include_path设置)找与$classname同名的.php/.inc文件。当然,你也可以指定特定类型的文件,方法是注册扩展名(spl_autoload_extionsions)。 78 | 79 |   而 spl_autoload_register() 就是我们上面所说的\_\_autoload调用堆栈,我们可以向这个函数注册多个我们自己的_autoload()函数,当PHP找不到类名时,PHP就会调用这个堆栈,一个一个去调用自定义的_autoload()函数,实现自动加载功能。如果我们不向这个函数输入任何参数,那么就会注册spl_autoload()函数。 80 | 81 |   好啦,PHP自动加载的底层就是这些,注册机制已经非常灵活,但是还缺什么呢?我们上面说过,自动加载关键就是类名和文件的映射,这种映射关系不同框架有不同方法,非常灵活,但是过于灵活就会显得杂乱,PHP有专门对这种映射关系的规范,那就是PSR标准中PSR0与PSR4。 82 | 83 |   不过在谈PSR0与PSR4之前,我们还需要了解PHP的命名空间的问题,因为这两个标准其实针对的都不是类名与目录文件的映射,而是命名空间与文件的映射。为什么会这样呢?在我的理解中,规范的面向对象PHP思想,命名空间在一定程度上算是类名的别名,那么为什么要推出命名空间,命名空间的优点是什么呢 84 | 85 | ---------- 86 | 87 | # 二、Namespace命名空间 88 | 89 |            要了解命名空间,首先先看看[官方文档](http://php.net/manual/zh/language.namespaces.rationale.php) 中对命名空间的介绍: 90 | 91 | >   什么是命名空间?从广义上来说,**命名空间是一种封装事物的方法。**在很多地方都可以见到这种抽象概念。例如,在操作系统中目录用来将相关文件分组, **对于目录中的文件来说,它就扮演了命名空间的角色。** 具体举个例子,文件 `foo.txt` 可以同时在目录 `/home/greg` 和 `/home/other` 中存在,但在同一个目录中不能存在两个 foo.txt 文件。另外,在目录 `/home/greg` 外访问 `foo.txt` 文件时,我们必须将目录名以及目录分隔符放在文件名之前得到 `/home/greg/foo.txt`。这个原理应用到程序设计领域就是命名空间的概念。 92 | 93 | >   ** 在PHP中,命名空间用来解决在编写类库或应用程序时创建可重用的代码如类或函数时碰到的两类问题:** 94 | 95 | >   1. 用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的 **名字冲突** * 96 | 97 | >   2. 为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个**别名**(或简短)的名称,提高源代码的可读性。* 98 | 99 | >   * **PHP 命名空间提供了一种将相关的类、函数和常量组合到一起的途径。*** 100 | 101 |    102 |   简单来说就是PHP是不允许程序中存在两个名字一样一样的类或者函数或者变量名的,那么有人就很疑惑了,那就不起一样名字不就可以了?事实上很多大程序依赖很多第三方库,名字冲突什么的不要太常见,这个就是官网中的第一个问题。那么如何解决这个问题呢?在没有命名空间的时候,可怜的程序员只能给类名起`a_b_c_d_e_f`这样的,其中`a/b/c/d/e/f`一般有其特定意义,这样一般就不会发生冲突了,但是这样长的类名编写起来累,读起来更是难受。因此PHP5就推出了命名空间,类名是类名,命名空间是命名空间,程序写/看的时候直接用类名,运行起来机器看的是命名空间,这样就解决了问题。 103 | 104 |   另外,命名空间提供了一种将相关的类、函数和常量组合到一起的途径。这也是面向对象语言命名空间的很大用途,把特定用途所需要的类、变量、函数写到一个命名空间中,进行封装。 105 | 106 |   解决了类名的问题,我们终于可以回到PSR标准来了,那么PSR0与PSR4是怎么 **规范** 文件与命名空间的映射关系的呢?答案就是:对 **命名空间的命名**(额,有点绕)、**类文件目录的位置** 和 **两者映射关系** 做出了限制,这个就是标准的核心了。 107 | 108 | 更完整的描述可见[现代 PHP 新特性系列(一) —— 命名空间](http://laravelacademy.org/post/4221.html) 109 | 110 | ---- 111 | 112 | # 三、PSR标准 113 |    114 |   在说PSR0与PSR4之前先介绍一下PSR标准。PSR标准的发明者和规范者是:PHP-FIG,它的网站是:[www.php-fig.org](www.php-fig.org)。就是这个联盟组织发明和创造了PSR-[0-4]规范。FIG 是 Framework Interoperability Group(框架可互用性小组)的缩写,由几位开源框架的开发者成立于 2009 年,从那开始也选取了很多其他成员进来,虽然不是 “官方” 组织,但也代表了社区中不小的一块。组织的目的在于:以最低程度的限制,来统一各个项目的编码规范,避免各家自行发展的风格阻碍了程序设计师开发的困扰,于是大伙发明和总结了PSR,PSR是Proposing a Standards Recommendation(提出标准建议)的缩写,截止到目前为止,总共有5套PSR规范,分别是: 115 | 116 | > PSR-0 (Autoloading Standard) *自动加载标准* 117 | 118 | > PSR-1 (Basic Coding Standard)*基础编码标准* 119 | 120 | > PSR-2 (Coding Style Guide) *编码风格向导* 121 | 122 | > PSR-3 (Logger Interface) *日志接口* 123 | 124 | > PSR-4 (Improved Autoloading) *自动加载的增强版,可以替换掉PSR-0了。* 125 | 126 |    127 |   具体详细的规范标准可以查看[PHP中PSR-[0-4]规范](https://www.zybuluo.com/phper/note/65033) 128 | 129 | 130 | ## PSR0标准 131 | 132 |    133 |   PRS-0规范是他们出的第1套规范,主要是制定了一些自动加载标准(Autoloading Standard)PSR-0强制性要求几点: 134 | 135 | > 1. 一个完全合格的namespace和class必须符合这样的结构:`[]*` 136 | 137 | > 2. 每个namespace必须有一个顶层的namespace("Vendor Name"提供者名字) 138 | 139 | > 3. 每个namespace可以有多个子namespace 140 | 141 | > 4. 当从文件系统中加载时,每个namespace的分隔符(/)要转换成 DIRECTORY_SEPARATOR(操作系统路径分隔符) 142 | 143 | > 5. 在类名中,每个下划线(\_) 符号要转换成 `DIRECTORY_SEPARATOR(操作系统路径分隔符)`。在`namespace`中,下划线 \_ 符号是没有(特殊)意义的。 144 | 145 | > 6. 当从文件系统中载入时,合格的`namespace`和`class`一定是以 `.php` 结尾的 146 | 147 | > 7. `verdor name`, `namespaces`, `class`名可以由大小写字母组合而成(大小写敏感的) 148 | 149 |    150 |    具体规则可能有些让人晕,我们从头讲一下。 151 | 152 |    153 |   我们先来看PSR0标准大致内容,第1、2、3、7条对命名空间的名字做出了限制,第4、5条对命名空间和文件目录的映射关系做出了限制,第6条是文件后缀名。 154 | 155 |    156 |    前面我们说过,PSR标准是如何规范命名空间和所在文件目录之间的映射关系?是通过限制命名空间的名字、所在文件目录的位置和两者映射关系。 157 | 158 |    159 |   那么我们可能就要问了,哪里限制了文件所在目录的位置了呢?其实答案就是: 160 |    161 | > **限制命名空间名字** `+` **限制命名空间名字与文件目录映射** `=` **限制文件目录** 162 | 163 |    164 |   好了,我们先想一想,对于一个具体程序来说,如果它想要支持PSR0标准,它需要做什么调整呢? 165 | 166 | > 1. 首先,程序必须定义一个符合PSR0标准第4、5条的映射函数,然后把这个函数注册到spl_register()中; 167 | > 2. 其次,定义一个新的命名空间时,命名空间的名字和所在文件的目录位置必须符合第1、2、3、7条。 168 | 169 |    170 |    一般为了代码维护方便,我们会在一个文件只定义一个命名空间。 171 | 172 |    好了,我们有了符合PSR0的命名空间的名字,通过符合PSR0标准的映射关系就可以得到符合PSR0标准的文件目录地址,如果我们按照PSR0标准正确存放文件,就可以顺利require该文件了,我们就可以使用该命名空间啦,是不是很神奇呢? 173 | 174 |    175 |    接下来,我们详细地来看看PSR0标准到底规范了什么呢? 176 | 177 |    178 |    我们以laravel中第三方库Symfony其中一个命名空间`/Symfony/Core/Request`为例,讲一讲上面PSR0标准。 179 |    180 | > 1. 一个完全合格的namespace和class必须符合这样的结构:`[]*` 181 | 182 |   上面所展示的/Symfony就是Vendor Name,也就是第三方库的名字,/Core是Namespace名字,一般是我们命名空间的一些属性信息(例如request是Symfony的核心功能);最后Request就是我们命名空间的名字,这个标准规范就是让人看到命名空间的来源、功能非常明朗,有利于代码的维护。 183 |    184 | > 2 . 每个namespace必须有一个顶层的namespace("Vendor Name"提供者名字) 185 | 186 |   也就是说每个命名空间都要有一个类似于/Symfony的顶级命名空间,为什么要有这种规则呢?因为PSR0标准只负责顶级命名空间之后的映射关系,也就是/Symfony/Core/Request这一部分,关于/Symfony应该关联到哪个目录,那就是用户或者框架自己定义的了。所谓的顶层的namespace,就是自定义了映射关系的命名空间,一般就是提供者名字(第三方库的名字)。换句话说顶级命名空间是自动加载的基础。为什么标准要这么设置呢?原因很简单,如果有个命名空间是/Symfony/Core/Transport/Request,还有个命名空间是/Symfony/Core/Transport/Request1,如果没有顶级命名空间,我们就得写两个路径和这两个命名空间相对应,如果再有Request2、Request3呢。有了顶层命名空间/Symfony,那我们就仅仅需要一个目录对应即可,剩下的就利用PSR标准去解析就行了。 187 |    188 | 189 | > 3.每个namespace可以有多个子namespace 190 | 191 |   这个很简单,Request可以定义成/Symfony/Core/Request,也可以定义成/Symfony/Core/Transport/Request,/Core这个命名空间下面可以有很多子命名空间,放多少层命名空间都是自己定义。 192 | 193 |    194 | > 4.当从文件系统中加载时,每个namespace的分隔符(/)要转换成 DIRECTORY_SEPARATOR(操作系统路径分隔符) 195 | 196 |   现在我们终于来到了映射规范了。命名空间的/符号要转为路径分隔符,也就是说要把/Symfony/Core/Request这个命名空间转为\Symfony\Core\Request这样的目录结构。 197 |    198 | > 5.在类名中,每个下划线_符号要转换成DIRECTORYSEPARATOR(操作系统路径分隔符)。在namespace中,下划线\符号是没有(特殊)意义的。 199 | 200 |   这句话的意思就是说,如果我们的命名空间是\Symfony\Core\Request_a,那么我们就应该把它映射到\Symfony\Core\Request\a这样的目录。为什么会有这种规定呢?这是因为PHP5之前并没有命名空间,程序员只能把名字起成Symfony_Core_Request_a这样,PSR0的这条规定就是为了兼容这种情况。 201 | 202 |   剩下两个很简单就不说了。 203 |    204 | 205 |   有这样的命名空间命名规则和映射标准,我们就可以推理出我们应该把命名空间所在的文件该放在哪里了。依旧以\Symfony\Core\Request为例, 它的目录是/path/to/project/vendor/Symfony/Core/Request.php,其中/path/to/project是你项目在磁盘的位置,/path/to/project/vendor是项目用的所有第三方库所在目录。/path/to/project/vendor/Symfony就是与顶级命名空间\Symfony存在对应关系的目录,再往下的文件目录就是按照PSR0标准建立的: 206 | >    \Symfony\Core\Request => /Symfony/Core/Request.php 207 | 208 | 209 |    一切很完满了是吗?不,还有一些瑕疵: 210 | > 1. 我们是否应该还兼容没有命名空间的情况呢? 211 | > 2. 按照PSR0标准,命名空间\A\B\C\D\E\F必然对应一个目录结构/A/B/C/D/E/F,这种目录结构层次是不是太深了? 212 | 213 | ## PSR4标准 214 |   2013年底,新出了第5个规范——PSR-4。 215 | 216 |   PSR-4规范了如何指定文件路径从而自动加载类定义,同时规范了自动加载文件的位置。这个乍一看和PSR-0重复了,实际上,在功能上确实有所重复。区别在于PSR-4的规范比较干净,去除了兼容PHP 5.3以前版本的内容,有一点PSR-0升级版的感觉。当然,PSR-4也不是要完全替代PSR-0,而是在必要的时候补充PSR-0——当然,如果你愿意,PSR-4也可以替代PSR-0。PSR-4可以和包括PSR-0在内的其他自动加载机制共同使用。 217 | 218 |    219 |    PSR4标准与PSR0标准的区别: 220 | > 1. 在类名中使用下划线没有任何特殊含义。 221 | > 2. 命名空间与文件目录的映射方法有所调整。 222 | 223 |   对第二项我们详细解释一下([Composer自动加载的原理](http://hanfeng.name/blog/2015/08/17/composer-autoload/)): 224 |   假如我们有一个命名空间:Foo/class,Foo是顶级命名空间,其存在着用户定义的与目录的映射关系: 225 | 226 | 227 | "Foo/" => "src/" 228 | 229 |   按照PSR0标准,映射后的文件目录是:src/Foo/class.php,但是按照PSR4标准,映射后的文件目录就会是:src/class.php,为什么要这么更改呢?原因就是怕命名空间太长导致目录层次太深,使得命名空间和文件目录的映射关系更加灵活。 230 | 231 |   再举一个例子,来源[PSR-4——新鲜出炉的PHP规范](https://segmentfault.com/a/1190000000380008): 232 | 233 |   PSR-0风格 234 | 235 | ```php 236 |

2 | 3 | 4 | # laravel 源码详解 5 | 6 | `laravel` 是一个非常简洁、优雅的 `PHP` 开发框架。`laravel` 中除了提供最为中心的 `Ioc` 容器之外,还提供了强大的 `路由`、`数据库模型` 等常用功能模块。 7 | 8 | 对于开发者来说,在使用 `laravel` 框架进行 `web` 开发的同时,一定很好奇 `laravel` 内部各个模块的原理,知其然更知其所以然,有助于提供开发的稳定与效率。 9 | 10 | 本项目针对 `laravel 5.4` 各个重要模块的源码进行了较为详尽的分析,希望给想要了解 `laravel` 底层原理与源码的同学一些指引。 11 | 12 | 由于本人能力有限,文章中可能会有一些问题,敬请提出意见与建议,谢谢。 13 | 14 | GitBook 地址 :https://www.gitbook.com/book/leoyang90/laravel-source-analysis 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * PHP Composer 自动加载 5 | * [PHP Composer——自动加载原理](/PHP Composer——自动加载原理.md) 6 | * [PHP Composer—— 初始化源码分析](/PHP Composer—— 初始化源码分析.md) 7 | * [PHP Composer-——-注册与运行源码分析](/PHP Composer-——-注册与运行源码分析.md) 8 | * Laravel Facade 门面 9 | * [Laravel Facade——Facade 门面源码分析](/Laravel Facade——Facade 门面源码分析.md) 10 | * Laravel Ioc 容器 11 | * [Laravel Container——IoC 服务容器](/Laravel Container——IoC 服务容器.md) 12 | * [Laravel Container——IoC 服务容器源码解析\(服务器绑定\)](/Laravel Container——IoC 服务容器源码解析%28服务器绑定%29.md) 13 | * [Laravel Container——IoC 服务容器源码解析\(服务器解析\)](/Laravel Container——IoC 服务容器源码解析%28服务器解析%29.md) 14 | * [Laravel Container——服务容器的细节特性](/Laravel Container——服务容器的细节特性.md) 15 | * Laravel Route 路由 16 | * [Laravel HTTP——路由](/Laravel HTTP——路由.md) 17 | * [Laravel HTTP——路由加载源码分析](/Laravel HTTP——路由加载源码分析.md) 18 | * [Laravel HTTP——Pipeline中间件处理源码分析](/Laravel HTTP——Pipeline中间件处理源码分析.md) 19 | * [Laravel HTTP——路由的正则编译](/Laravel HTTP——路由的正则编译.md) 20 | * [Laravel HTTP——路由的匹配与参数绑定](/Laravel HTTP——路由的匹配与参数绑定.md) 21 | * [Laravel HTTP——路由中间件源码分析](/Laravel HTTP——路由中间件源码分析.md) 22 | * [Laravel HTTP——SubstituteBindings 参数绑定中间件的使用与源码解析](/Laravel HTTP——SubstituteBindings 参数绑定中间件的使用与源码解析.md) 23 | * [Laravel HTTP——控制器方法的参数构建与运行](/Laravel HTTP——控制器方法的参数构建与运行.md) 24 | * [Laravel HTTP—— RESTFul 风格路由的使用与源码分析](/Laravel HTTP—— RESTFul 风格路由的使用与源码分析.md) 25 | * [Laravel HTTP——重定向的使用与源码分析](/Laravel HTTP——重定向的使用与源码分析.md) 26 | * Laravel ENV 环境变量 27 | * [Laravel ENV—— 环境变量的加载与源码解析](/Laravel ENV—— 环境变量的加载与源码解析.md) 28 | * Laravel Config 配置文件 29 | * [Laravel Config—— 配置文件的加载与源码解析](/Laravel Config—— 配置文件的加载与源码解析.md) 30 | * Laravel Exceptions 异常处理 31 | * [Laravel Exceptions——异常与错误处理.md](/Laravel Exceptions——异常与错误处理.md) 32 | * Laravel Providers 服务提供者 33 | * [Laravel Providers——服务提供者的注册与启动源码解析](/Laravel Providers——服务提供者的注册与启动源码解析.md) 34 | * Laravel Database 数据库 35 | * [Laravel Database——数据库服务的启动与连接](/Laravel Database——数据库服务的启动与连接.md) 36 | * [Laravel Database——数据库的 CRUD 操作](/Laravel Database——数据库的 CRUD 操作.md) 37 | * [Laravel Database——查询构造器与语法编译器源码分析\(上\)](/Laravel Database——查询构造器与语法编译器源码分析%28上%29.md) 38 | * [Laravel Database——查询构造器与语法编译器源码分析\(中\)](/Laravel Database——查询构造器与语法编译器源码分析%28中%29.md) 39 | * [Laravel Database——查询构造器与语法编译器源码分析\(下\)](/Laravel Database——查询构造器与语法编译器源码分析%28下%29.md) 40 | * [Laravel Database——分页原理与源码分析](/Laravel Database——分页原理与源码分析.md) 41 | * [Laravel Database——Eloquent Model 源码分析\(上\)](/Laravel Database——Eloquent Model 源码分析%28上%29.md) 42 | * [Laravel Database——Eloquent Model 源码分析(下)](/Laravel Database——Eloquent Model 源码分析(下).md) 43 | * [Laravel Database——Eloquent Model 关联源码分析](/Laravel Database——Eloquent Model 关联源码分析.md) 44 | * [Laravel Database——Eloquent Model 模型关系加载与查询](/Laravel Database——Eloquent Model 模型关系加载与查询.md) 45 | * [Laravel Database——Eloquent Model 更新关联模型](/Laravel Database——Eloquent Model 更新关联模型.md) 46 | * Laravel Session 47 | * [Laravel Session——session 的启动与运行源码分析](/Laravel Session——session 的启动与运行源码分析.md) 48 | * Laravel Event 事件系统 49 | * [Laravel Event——事件系统的启动与运行源码分析](/Laravel Event——事件系统的启动与运行源码分析.md) 50 | * Laravel Queue 队列 51 | * [Laravel Queue——消息队列任务与分发源码剖析](/Laravel Queue——消息队列任务与分发源码剖析.md) 52 | * [Laravel Queue——消息队列任务处理器源码剖析](/Laravel Queue——消息队列任务处理器源码剖析.md) 53 | * Laravel 广播系统 54 | * [Laravel Broadcast——广播系统源码剖析](/Laravel Broadcast——广播系统源码剖析.md) 55 | * Laravel Passport 56 | * [Laravel Passport——OAuth2 API 认证系统源码解析](/Laravel Passport——OAuth2 API 认证系统源码解析.md) 57 | * [Laravel Passport——OAuth2 API 认证系统源码解析\(下\)](/Laravel Passport——OAuth2 API 认证系统源码解析(下).md) 58 | 59 | --------------------------------------------------------------------------------