跟控制器说再见吧,从今天开始使用请求处理器(Request Handlers) 范式

在过去几年中, PHP 开发环境发生了很大的变化。我们开始使用更多更好的设计模式,比如 DRY 和 SOLID) 设计模式原则。但为什么我们仍然在使用控制器?如果您以前曾经参与过大型项目的架构编写,那么您可能已经注意到迟早会出现控制器过多的这种现象。即使您将控制器逻辑分离到各种类库或服务类中,大量的依赖项和方法以及代码的行数还是会随着时间的推移不断增长。我来介绍一下请求处理器。这个概念很简单,但很多 PHP 开发人员都不知道。请求处理器可以理解为仅包含单个动作(Action)的控制器,能够使请求到响应的流程更加清晰明确。这个概念与 Paul M. Jones 提出的 Action-Domain-Responder 设计模式有相似之处,后者是MVC模式的替代品。一个好的方法去建立请求处理器就是使用调用类。可调用类是使用PHP中的魔术方法 __invoke ,把他们变成一个 Callable ,这将允许他们作为函数调用。这里有一个关于调用类的简单例子:class Greeting{ public function __invoke($name) { echo ‘Hello ’ . $name; }}$welcome = new Greeting();$welcome(‘John Doe’); //输出 Hello John Doe看到这里你大概会想;“我为什么要这样做?”。我知道这是一个有点荒谬的例子。但是它与某些代码一起使用时例如可调用对象和依赖注入,它将变得很有意义。一个好的使用例子是路由的请求处理在Laravel和Slim框架中。Route::get(’/{name}’, Greeting::class);是否让你大吃一惊?没有?让我们把它和你通常写的比较一下:Route::get(’/{name}’, ‘SomeController@greeting’);还没有?除了代码好看之外,还有其他优点。让我们先去看看使用请求处理程序比控制器有那些优点。单一模式SOLID 的第一个原则是“单一模式”。在我看来,控制器中存在许多的方法,就打破了这个原则。请求处理程序提供了一个很好的解决方案,可以将这些操作分成它们自己的类,使它们更易于维护,重构和测试。这是从 UsersController 中提取的2个请求处理程序的示例,它处理用户配置文件的编辑和保存:class EditUserHandler{ public function __construct( UserRepository $repository, Twig $twig ) { … } public function __invoke(Request $request, Response $response) { … }}class UpdateUserHandler{ public function __construct( UserRepository $repository, UpdateUserValidator $validator, ImageManager $resizer, Filesystem $storage ) { … } public function __invoke(Request $request, Response $response) { … }}接下来让我们看下一个优势;测试性能你最近有没有为你的项目编写过单元测试?在编写单元测试的时候你可能编写了一些与测试无关的模拟依赖项。由于请求处理器将不同的控制器操作拆分为单独的类,因此您只需注入或绑定该动作所需要的依赖项即可。这是 Jeffrey Way 的一些建议 Twitter 。提示:让你的功能测试尽可能更加详细具体,使用测试用例来描述重要的规则和能力。这基本不会让你的请求处理器都有一个测试文件。对于那些繁琐的控制器测试文件来说是一个非常好的改进。重构PhpStorm 和其他的编辑器都有强大的代码重构功能,但是如果你使用的是 Laravel 或者 Slim 框架默认的路由方法将控制器绑定到路由,那么你可能会遇到这种问题。例如重命名:Route::get(’/{name}’, Greeting::class);比这简单得很多:Route::get(’/{name}’, ‘SomeController@greeting’);结论请求处理器是控制器很好的替代品。控制器的动作(Actions)被分为多个独立的请求处理器类,分别负责响应单一的动作。这使整个项目的代码更易于维护、重构和测试。您是否应当使用请求处理器替换所有控制器?可能不是。对于小型应用程序而言,为了简单,将动作组合成控制器或许更加合理。当我开始在 Teamleader 工作后,我才开始发掘请求处理器,我觉得近期没什么换回控制器的必要了。如果有什么不清楚或有疑问,请在下面留下评论告诉我,我会更新这篇文章。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 7, 2019 · 1 min · jiezi

laravel 框架配置404等异常页面

在Laravel中所有的异常都由Handler类处理,该类包含两个方法:report和render,其中render方法将异常渲染到http响应中。laravel的Handler类文件位置:app/Exceptions/Handler,由于render方法时间异常渲染到http响应中,所以我们只需要修改下render方法即可网上很多的方法是将render方法修改成:public function render($request, Exception $exception){ if ($exception) { return response()->view(’error.’.$exception->getStatusCode(), [],$exception->getStatusCode()); } return parent::render($request, $exception);}这时候你的测试可能是没有问题的,但是如果你如果写了登录的方法的话,这时候如果你访问必须要登录的页面的时候,这时候会报错这是由于如果你访问了必须要登录的页面的时候,这时候就会进入app/Exceptions/Handler.php的render方法,这时候$exception->getStatusCode()是不存在的,这时候就会报错了,那么如何解决呢?这时候我们找到parent::render的方法所在:这时候我们发现原来laravel框架已经将我们的这种情况包含进去了,那么我们就可以即将上面的方法改为:public function render($request, Exception $exception){ if (!($exception instanceof AuthenticationException)) { return response()->view(’error.’.$exception->getStatusCode(), [],$exception->getStatusCode()); } return parent::render($request, $exception);}这时候就完美解决了这个问题然后在resources/view/error/下面新建错误页面,错误页面的命名为:{errorcode}..balde.php,其中的errorcode为错误码,例如404..balde.php配置完成后访问一个不存在的路由时即可跳转到你配置的404页面作者:huaweichenai 来源:www.wj0511.com原文:https://www.wj0511.com/site/d…版权声明:本文为博主原创文章,转载请附上博文链接!

January 7, 2019 · 1 min · jiezi

wamp环境下运行composer的坑

今天在用composer安装laravel时报错The openssl extension is required for SSL/TLS protection but is not availab le. If you can not enable the openssl extension, you can disable this error , at your own risk, by setting the ‘disable-tls’ option to true.网上说是OpenSSL没有打开的问题,打开php.ini,启用插件并设置相应的证书,然后重启Apache。理论上来说,走到这一步应该没什么问题了,phpinfo();里也有OpenSSL的扩展,但是报错依旧。研究后发现,composer判断OpenSSL的依据是:当前环境变量下的php目录下的php.ini文件,但是wamp下php.ini文件实际上对应的是php目录下的phpForApache.ini而不是php.ini,所以把phpForApache.ini里的内容全部复制到php.ini,再次运行composer install就没有报错了。

January 7, 2019 · 1 min · jiezi

带小白理解php的自动加载

什么是自动加载自动加载是指在你想使用某个类,但你没有require 对应的.php文件的时候,程序帮你自动加载了php文件。(require是件很痛苦的事情OvO)在没有自动加载之前,你的代码可能是如下这样的:<?phprequire “app/Database.php”;require “app/Models/User.php”;require “config/app.php”;……$user = new Database();$user = new User();引入了好多的require,随着项目的不断迭代,会很乱。这样的设计并不好。php5.3之后,实现了自动加载,可以通过spl_autoload_register()方法进行php文件的的自动引入。spl_autoload_register()有三个参数。spl_autoload_register(‘autoload1’,true,true);第一个参数是当需要创建的类不存在时,调用autoload1()这个方法。第二个参数为true时,当类的自动加载函数无法成功注册时会抛出异常。第三个参数为true时,spl_autoload_register()方法会添加类的自动加载函数到队列之首,而不是队列尾部。因此,我们最后的代码会是这样:define(‘BASEDIR’, DIR);public static function autoload($class) { require BASEDIR . ‘/’ . str_replace(’\’, ‘/’, $class) . ‘.php’; }spl_autoload_register(‘autoload’);$operation = new IMooc\Operation(10);执行过程 首先定义了一个常量为BASEDIR为当前的目录(根目录),(1)当程序执行到&dollar;operation = new IMoocOperation(10);时,php引擎就会搜索作用域下是否有IMoocOperation这个类,如果有,则正常引入,如果没有则(2)调用spl_autoload_register()方法,然后再执行(3)autoload方法,autoload的参数&dollar;class为文件路径,根据psr-4规范,文件名要与类名相互对应,(也就是说,User.php文件只能有一个class,而且class名字必须是User),所以你在引入了php文件后,相当于引入了这个类,就可以调用相应的方法了。antuoload()在这个例子中被解析为public static function autoload($class) { require ‘imooc/IMooc/Operation.php’; }引入了这个类,自然就可以使用new Operation()对象。也就是说,只要你的代码符合psr-4规范,不需要require就可以自动加载相应的类。想象一下,你并不需要引入对应的类就可以直接使用,这很棒。如果本文对你有帮助,不妨点一个赞!

January 6, 2019 · 1 min · jiezi

Eloquent: 修改器

感觉好长时间没写东西了,一方面主要是自己的角色发生了变化,每天要面对各种各样的事情和突发事件,不能再有一个完整的长时间让自己静下来写代码,或者写文章。另一方面现在公司技术栈不再停留在只有 Laravel + VUE 了,我们还有小程序、APP 等开发,所以我关注的东西也就多了。接下来我还是会继续持续「高产」,把写技术文章当作一个习惯,坚持下去。好了,废话不多说,今天来说一说「Eloquent: 修改器」。一直想好好研究下 Eloquent。但苦于 Eloquent 有太多可研究的,无法找到一个切入点。前两天看一同事好像对这个「Eloquent: 修改器」了解不多,所以今天就拿它作为入口,扒一扒其实现源代码。首先还是拿一个 Demo 为例:Demo<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;use Carbon\Carbon;class Baby extends Model{ protected $table = ‘baby’; protected $appends = [‘age’]; public function getAgeAttribute() { $date = new Carbon($this->birthday); return Carbon::now()->diffInYears($date); }}这个代码比较简单,就是通过已有属性 birthday,计算 Baby 几岁了,得到 age 属性。前端就可以直接拿到结果:return $baby->age;同样的,还有 setXxxAttribute 方法来定义一个修改器。源代码读代码还是从使用入手,如上通过 $baby->age 调用 age 属性,这个属性没在类中定义,所以只能通过 PHP 的魔术方法 __get() 调用了。我们看看 Model 类的 __get() 方法:/** * Dynamically retrieve attributes on the model. * * @param string $key * @return mixed /public function __get($key){ return $this->getAttribute($key);}好了,我们开始解读源代码了:/* * Get an attribute from the model. * * @param string $key * @return mixed /public function getAttribute($key){ if (! $key) { return; } // If the attribute exists in the attribute array or has a “get” mutator we will // get the attribute’s value. Otherwise, we will proceed as if the developers // are asking for a relationship’s value. This covers both types of values. if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) { return $this->getAttributeValue($key); } …}重点自然就在第二个 if 上,主要判断 attributes 数组中是否包含该属性,如果没有,则会执行函数 $this->hasGetMutator($key):/* * Determine if a get mutator exists for an attribute. * * @param string $key * @return bool /public function hasGetMutator($key){ return method_exists($this, ‘get’.Str::studly($key).‘Attribute’);}这就对上了我们的 Demo 中自定义的函数 getAgeAttribute(),也就返回 true 了。接下来就是执行函数 $this->getAttributeValue($key),进而执行函数:return $this->mutateAttribute($key, $value);/* * Get the value of an attribute using its mutator. * * @param string $key * @param mixed $value * @return mixed */protected function mutateAttribute($key, $value){ return $this->{‘get’.Str::studly($key).‘Attribute’}($value);}好了,到此我们基本就知道了获取自定义 Attribute 的流程了。相信解析 set XxxAttribute 也是很简单的。总结好长时间没写东西了,先从最简单的入手,练练手。解析 Eloquent 需要费很多脑细胞,接下来的一段时间我会围绕着这个主题好好研究下去,尽可能的全部解读一遍::.|____Capsule| |____Manager.php|____composer.json|____Concerns| |____BuildsQueries.php| |____ManagesTransactions.php|____Connection.php|____ConnectionInterface.php|____ConnectionResolver.php|____ConnectionResolverInterface.php|____Connectors| |____ConnectionFactory.php| |____Connector.php| |____ConnectorInterface.php| |____MySqlConnector.php| |____PostgresConnector.php| |____SQLiteConnector.php| |____SqlServerConnector.php|____Console| |____Factories| | |____FactoryMakeCommand.php| | |____stubs| | | |____factory.stub| |____Migrations| | |____BaseCommand.php| | |____FreshCommand.php| | |____InstallCommand.php| | |____MigrateCommand.php| | |____MigrateMakeCommand.php| | |____RefreshCommand.php| | |____ResetCommand.php| | |____RollbackCommand.php| | |____StatusCommand.php| |____Seeds| | |____SeedCommand.php| | |____SeederMakeCommand.php| | |____stubs| | | |____seeder.stub|____DatabaseManager.php|____DatabaseServiceProvider.php|____DetectsDeadlocks.php|____DetectsLostConnections.php|____Eloquent| |____Builder.php| |____Collection.php| |____Concerns| | |____GuardsAttributes.php| | |____HasAttributes.php| | |____HasEvents.php| | |____HasGlobalScopes.php| | |____HasRelationships.php| | |____HasTimestamps.php| | |____HidesAttributes.php| | |____QueriesRelationships.php| |____Factory.php| |____FactoryBuilder.php| |____JsonEncodingException.php| |____MassAssignmentException.php| |____Model.php| |____ModelNotFoundException.php| |____QueueEntityResolver.php| |____RelationNotFoundException.php| |____Relations| | |____BelongsTo.php| | |____BelongsToMany.php| | |____Concerns| | | |____InteractsWithPivotTable.php| | | |____SupportsDefaultModels.php| | |____HasMany.php| | |____HasManyThrough.php| | |____HasOne.php| | |____HasOneOrMany.php| | |____MorphMany.php| | |____MorphOne.php| | |____MorphOneOrMany.php| | |____MorphPivot.php| | |____MorphTo.php| | |____MorphToMany.php| | |____Pivot.php| | |____Relation.php| |____Scope.php| |____SoftDeletes.php| |____SoftDeletingScope.php|____Events| |____ConnectionEvent.php| |____QueryExecuted.php| |____StatementPrepared.php| |____TransactionBeginning.php| |____TransactionCommitted.php| |____TransactionRolledBack.php|____Grammar.php|____Migrations| |____DatabaseMigrationRepository.php| |____Migration.php| |____MigrationCreator.php| |____MigrationRepositoryInterface.php| |____Migrator.php| |____stubs| | |____blank.stub| | |____create.stub| | |____update.stub|____MigrationServiceProvider.php|____MySqlConnection.php|____PostgresConnection.php|____Query| |____Builder.php| |____Expression.php| |____Grammars| | |____Grammar.php| | |____MySqlGrammar.php| | |____PostgresGrammar.php| | |____SQLiteGrammar.php| | |____SqlServerGrammar.php| |____JoinClause.php| |____JsonExpression.php| |____Processors| | |____MySqlProcessor.php| | |____PostgresProcessor.php| | |____Processor.php| | |____SQLiteProcessor.php| | |____SqlServerProcessor.php|____QueryException.php|____README.md|____Schema| |____Blueprint.php| |____Builder.php| |____Grammars| | |____ChangeColumn.php| | |____Grammar.php| | |____MySqlGrammar.php| | |____PostgresGrammar.php| | |____RenameColumn.php| | |____SQLiteGrammar.php| | |____SqlServerGrammar.php| |____MySqlBuilder.php| |____PostgresBuilder.php| |____SQLiteBuilder.php| |____SqlServerBuilder.php|____Seeder.php参考Eloquent: 修改器 https://laravel-china.org/docs/laravel/5.7/eloquent-mutators/2297__get()使用说明 http://php.net/manual/zh/language.oop5.overloading.php#object.get未完待续 ...

January 5, 2019 · 2 min · jiezi

php链式操作实现四则链式运算

重点在于,返回$this指针,方便调用后者函数。Operation.php<?phpnamespace IMooc;class Operation{ protected $number = 0; public function __construct($number) { $this->number = $number; } public function add($number) { $this->number += $number; return $this; } public function decrease($number) { $this->number -= $number; return $this; } public function multiply($number) { $this->number *= $number; return $this; } public function division($number) { $this->number /= $number; return $this; } public function get() { return $this->number; }}index.phprequire DIR . ‘/IMooc/Operation.php’;$operation = new IMooc\Operation(10);$result = $operation->add(2)->decrease(2) ->multiply(3)->division(4) ->get();var_dump($result);执行结果masaki@masaki-Inspiron:/var/www/imooc$ php index.phpfloat(7.5) ...

January 5, 2019 · 1 min · jiezi

laravel-admin 用户头像显示不出的原因及解决方法

原因:浏览器中右键检查头像元素发现图片链接显示的是 http://localhost/storage/imag…,为绝对路径,导致了404错误。对于虚拟主机来说绝对路径未必会显示,但是相对路径只要文件存在就一定可以显示。故解决思路就是把链接改为相对链接。解决方法:①:将 config/admin.php 里的 ‘disk’ => ‘public’修改为 ‘disk’ => ‘admin’在 config/filesystems.php 里面添加一个 admin 磁盘’disks’ => [ ‘admin’=>[ ‘driver’=>’local’, ‘root’=>storage_path(‘app/public’), ] ……],②:在项目目录下执行命令创建过软链接:php artisan storage:link 刷新页面修改头像即可显示。

January 4, 2019 · 1 min · jiezi

Laravel项目上传github后,clone到本地运行时报错500的解决方法

这几天自己在捣鼓一个laravel的项目,本地开发上传到github,再次clone到本地开发的时候报错500。主要原因是因为上传到github时 .env 文件会被忽略上传,毕竟 .env 文件中有各种数据库的连接信息,上传之后有很严重的安全隐患。另外,vendor文件夹也会被忽略上传,太多第三方类库的话上传会很慢,其他忽略文件可在 .gitignore 文件中查看。所以clone到本地的时候,需要执行命令 composer install 安装依赖类库,不然会报找不到依赖的错误,此时你会发现项目中已添加了vendor文件夹。这个时候再次执行项目还是会报错500,因为项目中没有 .env 文件呀,执行以下命令:cp -a .env.example .env此时项目中会多了 .env 文件,去到 .env 文件修改配置数据库连接信息。再次执行还是会报错:No application encryption key has been specified.此时依次执行以下命令:php artisan key:generatephp artisan serve重启项目会发现完美解决啦。记录一下踩过的坑,希望下次不会再犯,

January 4, 2019 · 1 min · jiezi

Lumen微服务生成Swagger文档

作为一名phper,在使用Lumen框架开发微服务的时候,API文档的书写总是少不了的,比较流行的方式是使用swagger来写API文档,但是与Java语言原生支持 annotation 不同,php只能单独维护一份swagger文档,或者在注释中添加annotations来实现类似的功能,但是注释中书写Swagger注解是非常痛苦的,没有代码提示,没有格式化。本文将会告诉你如何借助phpstorm中annotations插件,在开发Lumen微服务项目时(Laravel项目和其它php项目方法类似)快速的在代码中使用注释来创建swagger文档。本文将会持续修正和更新,最新内容请参考我的 GITHUB 上的 程序猿成长计划 项目,欢迎 Star,更多精彩内容请 follow me。框架配置我们使用当前最新的 Lumen 5.7 来演示。演示代码放到了github,感兴趣的可以参考一下https://github.com/mylxsw/lumen-swagger-demo安装依赖在Lumen项目中,首先需要使用 composer 安装SwaggerLume项目依赖composer require darkaonline/swagger-lume项目配置在bootstrap/app.php文件中,去掉下面配置的注释(大约在26行),启用Facades支持。$app->withFacades();启用SwaggerLume 项目的配置文件,在 Register Container Bindings部 分前面,添加$app->configure(‘swagger-lume’);然后,在 Register Service Providers 部分,注册 SwaggerLume 的ServiceProvider$app->register(\SwaggerLume\ServiceProvider::class);在项目的根目录,执行命令 php artisan swagger-lume:publish 发布swagger相关的配置执行该命令后,主要体现以下几处变更在 config/ 目录中,添加了项目的配置文件 swagger-lume.php在 resources/views/vendor 目录中,生成了 swagger-lume/index.blade.php 视图文件,用于预览生成的API文档从配置文件中我们可以获取以下关键信息api.title 生成的API文档显示标题routes.api 用于访问生成的API文档UI的路由地址默认为 /api/documentationroutes.docs 用于访问生成的API文档原文,json格式,默认路由地址为 /docspaths.docs 和 paths.docs_json 组合生成 api-docs.json 文件的地址,默认为 storage/api-docs/api-docs.json,执行php artisan swagger-lume:generate命令时,将会生成该文件语法自动提示纯手写swagger注释肯定是要不得的,太容易出错,还需要不停的去翻看文档参考语法,因此我们很有必要安装一款能够自动提示注释中的注解语法的插件,我们常用的IDE是 phpstorm,在 phpstorm 中,需要安装 PHP annotation 插件安装插件之后,我们在写Swagger文档时,就有代码自动提示功能了书写文档Swagger文档中包含了很多与具体API无关的信息,我们在 app/Http/Controllers 中创建一个 SwaggerController,该控制器中我们不实现业务逻辑,只用来放置通用的文档信息<?phpnamespace App\Http\Controllers;use OpenApi\Annotations\Contact;use OpenApi\Annotations\Info;use OpenApi\Annotations\Property;use OpenApi\Annotations\Schema;use OpenApi\Annotations\Server;/** * * @Info( * version=“1.0.0”, * title=“演示服务”, * description=“这是演示服务,该文档提供了演示swagger api的功能”, * @Contact( * email=“mylxsw@aicode.cc”, * name=“mylxsw” * ) * ) * * @Server( * url=“http://localhost”, * description=“开发环境”, * ) * * @Schema( * schema=“ApiResponse”, * type=“object”, * description=“响应实体,响应结果统一使用该结构”, * title=“响应实体”, * @Property( * property=“code”, * type=“string”, * description=“响应代码” * ), * @Property(property=“message”, type=“string”, description=“响应结果提示”) * ) * * * @package App\Http\Controllers /class SwaggerController{}接下来,在业务逻辑控制器中,我们就可以写API了<?phpnamespace App\Http\Controllers;use App\Http\Responses\DemoAdditionalProperty;use App\Http\Responses\DemoResp;use Illuminate\Http\Request;use OpenApi\Annotations\Get;use OpenApi\Annotations\MediaType;use OpenApi\Annotations\Property;use OpenApi\Annotations\RequestBody;use OpenApi\Annotations\Response;use OpenApi\Annotations\Schema;class ExampleController extends Controller{ /* * @Get( * path="/demo", * tags={“演示”}, * summary=“演示API”, * @RequestBody( * @MediaType( * mediaType=“application/json”, * @Schema( * required={“name”, “age”}, * @Property(property=“name”, type=“string”, description=“姓名”), * @Property(property=“age”, type=“integer”, description=“年龄”), * @Property(property=“gender”, type=“string”, description=“性别”) * ) * ) * ), * @Response( * response=“200”, * description=“正常操作响应”, * @MediaType( * mediaType=“application/json”, * @Schema( * allOf={ * @Schema(ref="#/components/schemas/ApiResponse"), * @Schema( * type=“object”, * @Property(property=“data”, ref="#/components/schemas/DemoResp") * ) * } * ) * ) * ) * ) * * @param Request $request * * @return DemoResp / public function example(Request $request) { // TODO 业务逻辑 $resp = new DemoResp(); $resp->name = $request->input(’name’); $resp->id = 123; $resp->age = $request->input(‘age’); $resp->gender = $request->input(‘gender’); $prop1 = new DemoAdditionalProperty(); $prop1->key = “foo”; $prop1->value = “bar”; $prop2 = new DemoAdditionalProperty(); $prop2->key = “foo2”; $prop2->value = “bar2”; $resp->properties = [$prop1, $prop2]; return $resp; }}这里,我们在响应结果中,引用了在SwaggerController中定义的 ApiResponse,还引用了一个没有定义的ExampleResp对象,我们可以 app\Http\Responses 目录(自己创建该目录)中实现该ExampleResp对象,我们将响应对象都放在这个目录中<?phpnamespace App\Http\Responses;use OpenApi\Annotations\Items;use OpenApi\Annotations\Property;use OpenApi\Annotations\Schema;/* * @Schema( * title=“demo响应内容”, * description=“demo响应内容描述” * ) * * @package App\Http\Responses /class DemoResp extends JsonResponse{ /* * @Property( * type=“integer”, * description=“ID” * ) * * @var int / public $id = 0; /* * @Property( * type=“string”, * description=“用户名” * ) * * @var string / public $name; /* * @Property( * type=“integer”, * description=“年龄” * ) * * @var integer / public $age; /* * @Property( * type=“string”, * description=“性别” * ) * * @var string / public $gender; /* * @Property( * type=“array”, * @Items(ref="#/components/schemas/DemoAdditionalProperty") * ) * * @var array / public $properties = [];}返回对象引用其它对象<?phpnamespace App\Http\Responses;use OpenApi\Annotations\Property;use OpenApi\Annotations\Schema;/* * * @Schema( * title=“额外属性”, * description=“额外属性描述” * ) * * @package App\Http\Responses /class DemoAdditionalProperty{ /* * @Property( * type=“string”, * description=“KEY” * ) * * @var string / public $key; /* * @Property( * type=“string”, * description=“VALUE” * ) * * @var string */ public $value;}生成文档执行下面的命令,就可以生成文档了,生成的文档在storage/api-docs/api-docs.json。php artisan swagger-lume:generate预览文档打开浏览器访问 http://访问地址/docs,可以看到如下内容访问 http://访问地址/api/documentation,我们看到接口详细信息展开更多本文简述了如何在Lumen项目中使用代码注释自动生成Swagger文档,并配合phpstorm的代码提示功能,然而,学会了这些还远远不够,你还需要去了解Swagger文档的语法结构,在 swagger-php 项目的 Examples 目录中包含很多使用范例,你可以参考一下。团队项目中使用了swagger文档,但是总得有个地方管理文档吧,这里推荐一下 Wizard 项目,该项目是一款用于团队协作的文档管理工具,支持Markdown文档和Swagger文档,感兴趣的不妨尝试一下。 ...

January 2, 2019 · 3 min · jiezi

修复 github 项目的语言属性

issueLaravel 开源电商项目源码 被 github 判断认为是 HTML 项目,但是实际项目并没有 html 代码。这就尴尬了,只有默默的通过 google 搜索 github change project type 发现这篇文章:How to Change Repo Language in GitHub简单来说只要在项目根目录下添加 .gitattributes 文件,然后通过设置相关目录或者类型文件标记为相关语言即可。gitattributes支持的属性语法如下:linguist-documentationlinguist-languagelinguist-vendoredlinguist-generatedlinguist-detectableusage*.css linguist-vendored*.js linguist-language=Vuemodules/* linguist-language=PHPresourcesgithub/linguist

December 29, 2018 · 1 min · jiezi

Laravel 开源电商体验与部署

体验开源项目已经部署了体验环境,开源通过扫描下方小程序码进行体验:我们部署了 Laravel API demo 环境,访问地址:https://demo-open-admin.ibran… , 访问默认是 Laravel 的欢迎页面,可通过 API 文档了解请求地址和相关参数说明。我们提供了完整的 Postman 文件,可以通过百度网盘下载:Postman 软件下载 https://pan.baidu.com/s/1bqVD5MJ 密码:4lkuPostman API 请求下载 https://pan.baidu.com/s/17Etk… 提取码: 9m54Laravel API 部署要本地开发部署,需要先搭建好本地的开发环境,本文已经假设你已经会通过各类工具(homestead)等来开发 Laravel 项目下载源码git clone https://github.com/ibrandcc/ecommerce-open-api或者composer create-project ibrand/open-ecommerceLaravel 常规安装以下步骤基本是 Laravel 项目安装需要执行的必须步骤安装依赖包我们为了方便大家使用,在项目的 composer.json 中已经默认使用了国内的 composer 镜像源,感谢 laravel-china下载好源码后,直接执行composer install -vvv设置 .env.env 文件中的数据库部分设置成自己开发的数据库配置cp .env.example .env应用密钥通过以下命令来生成应用密钥,密钥值在 .env 文件 APP_KEYphp artisan key:generate发布相关资源执行 publish 命令发布所有相关的资源,包含配置项,静态资源等。php artisan vendor:publish –all设定公共磁盘软连接Laravel 中上传文件通常是存储在 storage/app/public 目录下,该目录下的文件可以通过 php artisan storage:link 命令软连接到 public 目录下,以供外部访问。更多细节请见:文件系统完成安装执行内置命令完成数据库及其他配置和数据初始化等任务。php artisan ibrand:store-install 导入商品数据该项目使用标准的 Laravel migration 来创建数据表,虽然 ibrand:store-install 命令进行了数据初始化,但是为了方便,我们准备一份完整的商品数据,有助于理解商品模块的系统设计和快速体验。商品示例数据SQL文件在 modules/EC.Open.Core/database 目录下,可以通过使用各类 mysql 管理工具 或者 mysql 命令执行 sql 文件导入。sql 文件地址: goods_demo_data.sql最后一步请把 .env 文件中 APP_URL 值设置为你当前的域名,比如开源 demo 环境中APP_URL=https://demo-open-admin.ibrand.cc因为后续为了方便上 https ,所以此处 APP_URL 值必须指定当前项目所在域名。欢迎提交问题,觉得项目不错,记得 star : ) 项目传送门:ibrand-ecommerce-open-source ...

December 26, 2018 · 1 min · jiezi

laravel学习日志 - 在Ubuntu下安装homestead

更新软件源sudo apt-get update安装 VirtualBoxsudo apt-get install virtualbox安装 Vagrantsudo apt-get install vagrant如果在最后一步提示版本过低的话去官网下载安装debian版本文件,然后使用dpkg -i 文件名 的命令安装按照Homestead下载laravel-china提供的本地化Homestead Boxwget -nc -o /dev/null –restrict-file-names=nocontrol –no-check-certificate -P /tmp http://download.fsdhub.com/lc-homestead-6.1.1-2018090400.zip 2>&1解压upzip lc-homestead-6.1.1-2018090400.zip -d /tmp在解压目录运行导入命令vagrant box add metadata.json后面的没啥的跟着入门教程走就可以了。

December 26, 2018 · 1 min · jiezi

Laravel 5~嵌套评论的实现

经常我们看见评论显示形式有很多,比如’@‘某某,又或者像知乎的收缩式的评论,又或者是嵌套式的评论,那么最一开始也是最常见的就是嵌套式评论,因为这个更加醒目.准备工作1.设计三张表users,posts,comments,表结构如下:usersSchema::create(‘users’, function (Blueprint $table) { $table->increments(‘id’); $table->string(’name’); $table->string(’email’)->unique(); $table->string(‘password’); $table->rememberToken(); $table->timestamps();});postsSchema::create(‘posts’, function (Blueprint $table) { $table->increments(‘id’); $table->string(’title’); $table->integer(‘user_id’)->index(); $table->text(‘content’); $table->timestamps();});commentsSchema::create(‘comments’, function (Blueprint $table) { $table->increments(‘id’); $table->integer(‘user_id’)->index(); $table->integer(‘post_id’)->index(); $table->integer(‘parent_id’)->index()->default(0); $table->text(‘body’); $table->timestamps();});2.Model层:Post.php文件/** * 一篇文章有多个评论 * @return \Illuminate\Database\Eloquent\Relations\HasMany /public function comments(){ return $this->hasMany(Comment::class);}/* * 获取这篇文章的评论以parent_id来分组 * @return static /public function getComments(){ return $this->comments()->with(‘owner’)->get()->groupBy(‘parent_id’);}Comments.php文件/* * 这个评论的所属用户 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo /public function owner(){ return $this->belongsTo(User::class, ‘user_id’);}/* * 这个评论的子评论 * @return \Illuminate\Database\Eloquent\Relations\HasMany */public function replies(){ return $this->hasMany(Comment::class, ‘parent_id’);}逻辑编写我们所要实现的嵌套评论其实在我们准备工作中已经 有点思路了,我们首先将一篇文章显示出来,同时利用文章与评论的一对多关系,进行显示所有的评论,但是我们的评论里面涉及到一个字段就是parent_id,这个字段其实非常的特殊,我们利用这个字段来进行分组, 代码就是上面的return $this->comments()->with(‘owner’)->get()->groupBy(‘parent_id’),具体的过程如下:web.php文件\Auth::loginUsingId(1); //用户id为1的登录//显示文章和相应的评论Route::get(’/post/show/{post}’, function (\App\Post $post) { $post->load(‘comments.owner’); $comments = $post->getComments(); $comments[‘root’] = $comments[’’]; unset($comments[’’]); return view(‘posts.show’, compact(‘post’, ‘comments’));});//用户进行评论Route::post(‘post/{post}/comments’, function (\App\Post $post) { $post->comments()->create([ ‘body’ => request(‘body’), ‘user_id’ => \Auth::id(), ‘parent_id’ => request(‘parent_id’, null), ]); return back();});视图代码视图方面我们需要实现嵌套,那么随着用户互相评论的越来越多的话,那么嵌套的层级也就越多,所以说,我们这里需要使用各小技巧来显示整个评论,我们使用@include()函数来显示,那么我们试图的结构如下: - commentscomments.blade.phpform.blade.phplist.blade.php - postsshow.blade.php代码如下:show.blade.php<!DOCTYPE html><html lang=“en”><head> <meta charset=“utf-8”> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <meta name=“viewport” content=“width=device-width, initial-scale=1”> <link href="//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css" rel=“stylesheet”></head><body><div class=“container” style=“margin-top: 100px”> <div class=“col-md-10 col-md-offset-1”> <h2>{{$post->title}}</h2> <h4>{{$post->content}}</h4> <hr> @include(‘comments.list’,[‘collections’=>$comments[‘root’]]) <h3>留下您的评论</h3> @include(‘comments.form’,[‘parentId’=>$post->id]) </div></div></body></html>comment.blade.php<div class=“col-md-12”> <h5><span style=“color:#31b0d5”>{{$comment->owner->name}}</span>:</h5> <h5>{{$comment->body}}</h5> @include(‘comments.form’,[‘parentId’=>$comment->id]) @if(isset($comments[$comment->id])) @include(‘comments.list’,[‘collections’=>$comments[$comment->id]]) @endif <hr></div>form.blade.php<form method=“POST” action="{{url(‘post/’.$post->id.’/comments’)}}" accept-charset=“UTF-8”> {{csrf_field()}} @if(isset($parentId)) <input type=“hidden” name=“parent_id” value="{{$parentId}}"> @endif <div class=“form-group”> <label for=“body” class=“control-label”>Info:</label> <textarea id=“body” name=“body” class=“form-control” required=“required”></textarea> </div> <button type=“submit” class=“btn btn-success”>回复</button></form>list.blade.php@foreach($collections as $comment) @include(‘comments.comment’,[‘comment’=>$comment])@endforeach最终效果图如下最近在研究laravel,看到一个之前做过的功能,之前写的比较繁琐,特记录下来。摘自:https://laravel-china.org/art…https://laravel-china.org/art… ...

December 20, 2018 · 1 min · jiezi

laravel5.5和laravel-admin 安装小坑笔记

配置laravel-admin官方的教程还是没问题的,但也遇到了一点点小小坑,再次做个记录吧安装 LaravelLaravel 使用 Composer 管理依赖,所以,安装之前确保已经在机器上安装了 Composer(如果尚未安装的话参考这篇文档去安装吧)。通过 Laravel 安装器首先,通过 Composer 安装 Laravel 安装器:composer global require “laravel/installer"确保 $HOME/.composer/vendor/bin 在系统路径中(Mac中对应路径是 ~/.composer/vendor/bin,Windows对应路径是 ~/AppData/Roaming/Composer/vendor/bin,其中 ~ 表示当前用户家目录),否则不能在命令行任意路径下调用 laravel 命令。安装完成后,通过简单的 laravel new 命令即可在当前目录下创建一个新的 Laravel 应用,例如,laravel new blog 将会创建一个名为 blog 的新应用,且包含所有 Laravel 依赖。该安装方法比通过 Composer 安装要快很多:laravel new blog如果之前已经安装过旧版本的 Laravel 安装器,需要更新后才能安装最新的 Laravel 5.5 框架应用:composer global update通过 Composer Create-Project你还可以在终端中通过 Composer 的 create-project 命令来安装 Laravel 应用:composer create-project –prefer-dist laravel/laravel laravel-admin如果要下载安装 Laravel 其他版本应用,比如 5.5 版本,可以使用这个命令:composer create-project –prefer-dist laravel/laravel laravel-admin 5.5.*。注意PHP7.0.30报错,文档写的PHP>7.0.0即可,但是这种写法貌似是7.1才可以的.这种写法不识别,去掉就OK了.或者 升级更高版本PHP即可,注意打开openssl.env文件按照常规配置就可以了注意和数据库表名称不要写错然后首先确保安装好了laravel,并且数据库连接设置正确。然后进入laravel目录执行composer require encore/laravel-admin然后运行下面的命令来发布资源:php artisan vendor:publish –provider=“Encore\Admin\AdminServiceProvider"在该命令会生成配置文件config/admin.php,可以在里面修改安装的地址、数据库连接、以及表名,建议都是用默认配置不修改。然后运行下面的命令完成安装:php artisan admin:install成功:启动服务后,在浏览器打开 http://localhost/admin/ ,使用用户名 admin 和密码 admin登陆.报错:最后进入 config/filesystems.php 加入’admin’ => [ ‘driver’ => ’local’, ‘root’ => public_path(‘upload’), ‘visibility’ => ‘public’, ‘url’ => env(‘APP_URL’).’/public/upload/’,],修改语言:打开 config/app.php 修改 en -> zh-CN’locale’ => ‘zh-CN’, ...

December 20, 2018 · 1 min · jiezi

来!狂撸一款PHP现代化框架 (路由的设计)

前言上一篇的标题改了一下,以一、二、三为章节对读者来说是种困扰,现在的标题是依照项目进度来编写的。上篇文章地址为 https://segmentfault.com/a/11…这一系列文章并不准备写太多章节,大概规划的只有4~5章左右,具体实现代码还请移步Githubhttps://github.com/CrazyCodes…本章详细讲解一下Route(路由的实现),Come on Up Image上图大概说明了实现路由要经过两个步骤将所有路由信息存储到超全局变量中用户请求时从全局变量中查找路由映射的服务脚本并实例化OK,大概流程就是酱紫,下面开始“撸”目录路由的代码暂分为以下几个文件(这并不是确定的,详细可查看Github)文件名注释Route转发文件:为实现 Route::get 效果RouteCollection路由信息处理存储RouteInterface无需解释RouteModel路由模型,将每个路由信息以结构体方式存储到$_SERVERRouter路由的核心类莫急,我们一个一个文件来看。先从RouteInterface开始RouteInterface参照RESTful规定设定接口方法分别为 GET、POST、PATCH、PUT、DELETE、OPTIONS,当然Laravel也是规范了以上标准请求。GitHub : https://github.com/CrazyCodes...interface RouteInterface{ /** * @param $uri * @param null $action * * @return mixed / public function get($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function post($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function patch($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function put($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function delete($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function options($uri, $action = null);}Router先写一个栗子public function get($uri, $action = null){ return $this->addRoute(“GET”, $uri, $action);}用户调用下方代码会指向上述方法,方法既调用addRoute方法将路由信息存储到$_SERVER中Route::get(’/’,‘Controller’)以下为addRoute部分的代码public function addRoute($methods, $uri, $action){ // 这里判断请求方式是否合规,既是否存在 GET、POST、PATCH、PUT、DELETE、OPTIONS其中之一 if ($this->verify($methods) == false) { return false; } // 之后我们去往RouteCollection路由信息的处理类中 return $this->routes->add($uri, $this->createRoute($methods, $action));}RouteCollection最终达到 add 方法,将路由信息存储到$_SERVER中public function add($uri, RouteModel $model){ if (empty($_SERVER[“routes”][$uri])) { $_SERVER[“routes”][$uri] = $model; }}第二个参数RouteModel开始我们说过这是路由模型,将每个路由以结构体的方式存储到变量中,存储后的结果’routes’ => array(6) { ’test/get’ => class Zero\Routing\RouteModel#13 (2) { public $method => string(3) “GET” public $action => string(19) “testController@test” } ’test/post’ => class Zero\Routing\RouteModel#14 (2) { public $method => string(4) “POST” public $action => string(19) “testController@test” } ’test/put’ => class Zero\Routing\RouteModel#15 (2) { public $method => string(3) “PUT” public $action => string(18) “testController@put” } ’test/del’ => class Zero\Routing\RouteModel#16 (2) { public $method => string(6) “DELETE” public $action => string(18) “testController@del” } ’test/patch’ => class Zero\Routing\RouteModel#17 (2) { public $method => string(5) “PATCH” public $action => string(20) “testController@patch” } ’test/opt’ => class Zero\Routing\RouteModel#18 (2) { public $method => string(7) “OPTIONS” public $action => string(18) “testController@opt” } }Route最后通过__callStatic将代码重定向到核心类中public static function __callStatic($name, $arguments){ $router = new Router; return $router->{$name}($arguments[0], $arguments[1]);}上述套路部分是Laravel的设计思想,通过这款简单的框架可对Laravel核心设计有丁点的理解。测试测试上次做的有点糙,从本章到系列结束,我们都以PHPunit来测试。/* * @content tests all methods storage -> $_SERVER[“routes”] /public function testAllMethodsStorage(){ $this->routes->get($methodGet = “test/get”, “testController@test”); $this->assertArrayHasKey($methodGet, $_SERVER[$this->methodsDataKey]); $this->routes->post($methodPost = “test/post”, “testController@test”); $this->assertArrayHasKey($methodPost, $_SERVER[$this->methodsDataKey]); $this->routes->put($methodPut = “test/put”, “testController@put”); $this->assertArrayHasKey($methodPut, $_SERVER[$this->methodsDataKey]); $this->routes->delete($methodDel = “test/del”, “testController@del”); $this->assertArrayHasKey($methodDel, $_SERVER[$this->methodsDataKey]); $this->routes->patch($methodPatch = “test/patch”, “testController@patch”); $this->assertArrayHasKey($methodPatch, $_SERVER[$this->methodsDataKey]); $this->routes->options($methodOpt = “test/opt”, “testController@opt”); $this->assertArrayHasKey($methodOpt, $_SERVER[$this->methodsDataKey]);}上述贴出部分代码,以过程化的方法去测试。查看存储是否符合预期。/* * @content RouteModel Success */public function testCreateRoute(){ $response = $this->routes->createRoute(“GET”, “TestController@Get”); $this->assertInstanceOf(RouteModel::class, $response);}包括测试对路由创建后是否为RouteModel的实现。具体可查看Githubhttps://github.com/CrazyCodes…致谢上述已完成了路由的基本设计,下一章将讲解从启动到请求路由映射到服务脚本的过程。希望本章可以帮到你,谢谢。 ...

December 14, 2018 · 2 min · jiezi

来!狂撸一款PHP现代化框架 (一)

前言从本章开始,我们继续造轮子,去完成一款类似于Laravel的现代化PHP框架,为什么说是现代化?因为他必须具备一下几点遵守PSR-4加载规范使用Composer进行包管理标准的HTTP请求方式优雅的使用设计模式开始我们无需关心性能问题,先考虑框架具体需要实现哪些功能,这与实现业务就大不相同了,来!开始我的表演。前期做任何一件事情都要有个前期准备工作。作为PSR-4的规定,我们命名空间得有一个祖宗名字,这里我叫他神圣的 《z_framework》至少需要一个GITHUB库来存储这个项目 https://github.com/CrazyCodes…创建一个composer.json文件用于进行包管理,灰常简单,phpunit搞进来。通过psr-4加载个项目命名{ “name”: “z framework”, “require-dev”: { “phpunit/phpunit”: “^7.0” }, “autoload”: { “psr-4”: { “Zero\”: “src/Zero”, } }, “autoload-dev”: { “psr-4”: { “Zero\Tests\”: “tests/” } }}最后我们就需要考虑下目录的结构及其我们第一步要完成的功能,核心的结构(这里并非只的项目结构哦。是框架的核心结构)暂且是这样srcZeroConfig // 可能存放一些配置文件的解析器Container // 容器的解析器Http // 请求处理的一些工具Routes // 路由处理的一些功能Bootstrap.php // 这可能是一个启动脚本Zero.php // 可能是核心的入口文件tests // 测试目录.gitignorecomposer.jsonLICENSEREADME.md路由还记得第一次使用Laravel时我们第一步做的事情吗?是的,去研究路由,所以我们把路由作为框架的第一步。在研究路由前,我们要知道http://www.domain.com/user/create是如何实现的,php默认是必须请求index.php或者default.php的,上述链接实际隐藏了index.php或default.php ,这是Nginx等服务代理帮我们做到的优雅的链接,具体配置如下,实际与Laravel官方提供无差别server { listen 80; server_name www.zf.com; root /mnt/app/z_framework/server/public; index index.php index.html index.htm; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ .php$ { fastcgi_pass php71:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }}通过try_files $uri $uri/ /index.php?$query_string;去解析请求,通过上述可以得出http://www.domain.com/user/create=======http://www.domain.com/index.php?user/create好了,明白了其中奥秘后,我们开始路由的编写,在src/Routes/Route.phpnamespace Zero\Routes; class Route {}实现首先我们先创建一个简单的接口文件src/Routes/RouteInterface.phpnamespace Zero\Routes; interface RouteInterface{ public function Get($url, $callFile); public function Post($url, $callFile); public function Put($url, $callFile); public function Delete($url, $callFile);}从Get请求开始namespace Zero\Routes; class Route implements RouteInterface{ public function Get($url, $callFile) { }}最后实现Get代码块if (parent::isRequestMethod(“GET”)) { // 判读请求方式 if (is_callable($callFile)) { // 判断是否是匿名函数 return $callFile(); } if ($breakUpString = parent::breakUpString($callFile)) { // 获取Get解析。既/user/create header(‘HTTP/1.1 404 Not Found’); } try { // 通过反射类获取对象 $breakUpString[0] = user $reflectionClass = new \ReflectionClass(‘App\Controllers\’ . $breakUpString[0]); // 实例化对象 $newInstance = $reflectionClass->newInstance(); // 获取对象中的指定方法,$breakUpString[1] = create call_user_func([ $newInstance, $breakUpString[1], ], []); } catch (\ReflectionException $e) { header(‘HTTP/1.1 404 Not Found’); }} else { header(‘HTTP/1.1 404 Not Found’);}return “";如果你想测试上述代码,可使用phpunit,或者傻大粗的方式,这里便于理解使用傻大粗的方式创建一个目录,随后按照Laravel的目录形式创建几个目录,<?php namespace App\Controllers;class UserController{ public function create() { var_dump(0); }}最后public/index.php文件中去调用路由require_once “../../vendor/autoload.php”;Zero\Zero::Get(“user”, “UserController@create”);到这里我们就基本完成了路由的功能,下一章将完善路由的编码致谢感谢你看到这里,希望本篇可以帮到你。具体代码在 https://github.com/CrazyCodes… ...

December 6, 2018 · 1 min · jiezi

Laravel核心解读--Console内核

Console内核上一篇文章我们介绍了Laravel的HTTP内核,详细概述了网络请求从进入应用到应用处理完请求返回HTTP响应整个生命周期中HTTP内核是如何调动Laravel各个核心组件来完成任务的。除了处理HTTP请求一个健壮的应用经常还会需要执行计划任务、异步队列这些。Laravel为了能让应用满足这些场景设计了artisan工具,通过artisan工具定义各种命令来满足非HTTP请求的各种场景,artisan命令通过Laravel的Console内核来完成对应用核心组件的调度来完成任务。 今天我们就来学习一下Laravel Console内核的核心代码。内核绑定跟HTTP内核一样,在应用初始化阶有一个内核绑定的过程,将Console内核注册到应用的服务容器里去,还是引用上一篇文章引用过的bootstrap/app.php里的代码<?php// 第一部分: 创建应用实例$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));// 第二部分: 完成内核绑定$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);// console内核绑定$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);return $app;Console内核 \App\Console\Kernel继承自Illuminate\Foundation\Console, 在Console内核中我们可以注册artisan命令和定义应用里要执行的计划任务。/*** Define the application’s command schedule.** @param \Illuminate\Console\Scheduling\Schedule $schedule* @return void*/protected function schedule(Schedule $schedule){ // $schedule->command(‘inspire’) // ->hourly();}/*** Register the commands for the application.** @return void*/protected function commands(){ $this->load(DIR.’/Commands’); require base_path(‘routes/console.php’);}在实例化Console内核的时候,内核会定义应用的命令计划任务(shedule方法中定义的计划任务)public function __construct(Application $app, Dispatcher $events){ if (! defined(‘ARTISAN_BINARY’)) { define(‘ARTISAN_BINARY’, ‘artisan’); } $this->app = $app; $this->events = $events; $this->app->booted(function () { $this->defineConsoleSchedule(); });}应用解析Console内核查看aritisan文件的源码我们可以看到, 完成Console内核绑定的绑定后,接下来就会通过服务容器解析出console内核对象$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);$status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, new Symfony\Component\Console\Output\ConsoleOutput);执行命令任务解析出Console内核对象后,接下来就要处理来自命令行的命令请求了, 我们都知道PHP是通过全局变量$_SERVER[‘argv’]来接收所有的命令行输入的, 和命令行里执行shell脚本一样(在shell脚本里可以通过$0获取脚本文件名,$1 $2这些依次获取后面传递给shell脚本的参数选项)索引0对应的是脚本文件名,接下来依次是命令行里传递给脚本的所有参数选项,所以在命令行里通过artisan脚本执行的命令,在artisan脚本中$_SERVER[‘argv’]数组里索引0对应的永远是artisan这个字符串,命令行里后面的参数会依次对应到$_SERVER[‘argv’]数组后续的元素里。因为artisan命令的语法中可以指定命令参数选项、有的选项还可以指定实参,为了减少命令行输入参数解析的复杂度,Laravel使用了Symfony\Component\Console\Input对象来解析命令行里这些参数选项(shell脚本里其实也是一样,会通过shell函数getopts来解析各种格式的命令行参数输入),同样地Laravel使用了Symfony\Component\Console\Output对象来抽象化命令行的标准输出。引导应用在Console内核的handle方法里我们可以看到和HTTP内核处理请求前使用bootstrapper程序引用应用一样在开始处理命令任务之前也会有引导应用这一步操作其父类 「IlluminateFoundationConsoleKernel」 内部定义了属性名为 「bootstrappers」 的 引导程序 数组:protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\SetRequestForConsole::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class,];数组中包括的引导程序基本上和HTTP内核中定义的引导程序一样, 都是应用在初始化阶段要进行的环境变量、配置文件加载、注册异常处理器、设置Console请求、注册应用中的服务容器、Facade和启动服务。其中设置Console请求是唯一区别于HTTP内核的一个引导程序。执行命令执行命令是通过Console Application来执行的,它继承自Symfony框架的Symfony\Component\Console\Application类, 通过对应的run方法来执行命令。name Illuminate\Foundation\Console;class Kernel implements KernelContract{ public function handle($input, $output = null) { try { $this->bootstrap(); return $this->getArtisan()->run($input, $output); } catch (Exception $e) { $this->reportException($e); $this->renderException($output, $e); return 1; } catch (Throwable $e) { $e = new FatalThrowableError($e); $this->reportException($e); $this->renderException($output, $e); return 1; } }}namespace Symfony\Component\Console;class Application{ //执行命令 public function run(InputInterface $input = null, OutputInterface $output = null) { …… try { $exitCode = $this->doRun($input, $output); } catch { …… } …… return $exitCode; } public function doRun(InputInterface $input, OutputInterface $output) { //解析出命令名称 $name = $this->getCommandName($input); //解析出入参 if (!$name) { $name = $this->defaultCommand; $definition = $this->getDefinition(); $definition->setArguments(array_merge( $definition->getArguments(), array( ‘command’ => new InputArgument(‘command’, InputArgument::OPTIONAL, $definition->getArgument(‘command’)->getDescription(), $name), ) )); } …… try { //通过命令名称查找出命令类(命名空间、类名等) $command = $this->find($name); } …… //运行命令类 $exitCode = $this->doRunCommand($command, $input, $output); return $exitCode; } protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) { …… //执行命令类的run方法来处理任务 $exitCode = $command->run($input, $output); …… return $exitcode; }}执行命令时主要有四步操作:通过命令行输入解析出命令名称和参数选项。通过命令名称查找命令类的命名空间和类名。执行命令类的run方法来完成任务处理并返回状态码。和命令行脚本的规范一样,如果执行命令任务程序成功会返回0, 抛出异常退出则返回1。还有就是打开命令类后我们可以看到并没有run方法,我们把处理逻辑都写在了handle方法中,仔细查看代码会发现run方法定义在父类中,在run方法会中会调用子类中定义的handle方法来完成任务处理。 严格遵循了面向对象程序设计的SOLID 原则。结束应用执行完命令程序返回状态码后, 在artisan中会直接通过exit($status)函数输出状态码并结束PHP进程,接下来shell进程会根据返回的状态码是否为0来判断脚本命令是否执行成功。到这里通过命令行开启的程序进程到这里就结束了,跟HTTP内核一样Console内核在整个生命周期中也是负责调度,只不过Http内核最终将请求落地到了Controller程序中而Console内核则是将命令行请求落地到了Laravel中定义的各种命令类程序中,然后在命令类里面我们就可以写其他程序一样自由地使用Laravel中的各个组件和注册到服务容器里的服务了。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

December 1, 2018 · 2 min · jiezi

PHP工具箱:PHPStan —— PHP 静态代码分析工具

PHPStan:无需写测试就能找到代码中的 Bug每当我看到开发人员从 Java 或 C# 等编译语言切换到 PHP 这样的解释语言时解放了生产力后感到很高兴。除了这些常规的执行模型(发起、处理请求和结束请求)和更短的反馈环(无需等待编译器)外,还有一个能解决开发人员日常问题的开源框架生态系统,因此,PHP 是目前来说 web 开发中最流行的语言。但它有一个缺点。你会在什么时候发现错误?编译型语言需要在程序运行之前了解每个变量的类型,每个方法的返回类型。这就是为什么编译器需要确保程序是没有错误的,并且会在源码中向你指出这些类型的错误,比如调用了未定义的方法或者是向某个函数传递了错误数量的参数。在把应用程序部署到生产环境前,编译器算是第一道防线。然而 PHP 就不会这样了。如果程序出错,会执行到错误的代码的时候崩溃。在测试 PHP 应用时,不管是自动化测试还是手动测试,开发人员都会花费大量时间去查一些其它编译型语言不会犯的错从而减少测试实际业务逻辑的时间。我想改变这一点。欢迎来到 PHPStan 的世界现阶段 PHP 实践所产生的代码库中,我们可以确定大部分数据的类型,并且转换为静态类型的语言,尽管还保留着一些动态语言的特性。人们把现在的 PHP 代码库变得跟其他语言一样更加有趣。面向对象,依赖注入以及设计模式的使用已经变得非常普遍。这让我想到了 PHP 的 静态分析工具,它将替代其他语言的编译器角色。我花了很多时间研究它,并且已经使用它的各种开发版本来检查我们的代码库超过一年。它就是 PHPStan, 开源且免费它目前校验什么?有关类中涉及的,对象实例, 错误/异常捕获,类型约束以及其他语言结构的存在性。 PHP 照旧不会检查这些, 但是会展现其中未被使用的代码。被调用的方法和函数的存在性和可访问性。同样也会检查他们的参数个数。方法是否返回了它声明的返回值类型。被访问成员变量的存在性和可见性。它也可指出是否将一个其他的类型的值赋给了既定类型的成员变量。sprintf/printf 函数基于格式化字符串所应接收的参数个数。分支和循环范围中的变量的存在性。无用的形式指定。例如 (string) ‘foo’ ,以及不同类型变量间的严格比较 (=== 和 !==),因为他们的结果总为 false。这个清单的内容随着每次发布都在递增。但成就 PHPStan 也不会只仰赖此一技之微。PHPStan 迅疾如飞…它设法一次性检查整个代码库。 它无需多次遍历代码。 只需浏览您想要分析的代码,例如 你写的代码。它无需解析和分析第三方依赖项。 相反,它使用反射来找出有关你代码库中引用的他人代码的有用信息。PHPStan 能在一分钟里检查我们的代码库 (6000 个文件, 600k LOCs) 。它可在一秒内完成自查。…可扩展性即便当前正在使用静态类型,开发者也可以合法的使用 PHP 的动态语法特性,例如 get, set 和 __call 这些魔术方法。它们可以在运行时去定义新属性和方法。通常,静态分析都会爆出属性和方法未定义,但是有一种机制可以告诉引擎如何创建新的属性和方法。它得益于对允许用户扩展的原生 PHP 反射的自定义抽象。更多细节可查看 README 中类反射扩展章节。某些方法返回的类型取决于它的参数。它可以取决于你传递给它的类名,也可能返回与传递的对象相同的类的对象。这就是 动态返回类型扩展 的用途。压轴语: 如果你想自己出一个 PHPStan 的新的检查项, 你可以自力更生。可以提出基于特定框架的规则,例如检查 DQL查询中引用的实体和字段是否存在,或者你选择的 MVC 框架中生成的链接是否和现存的控制器有关。选择规范级别我使用过其他工具,并将之集成进现有的代码库中,这种体验真是往事不堪回首。他们爆出成千上万的错误让你没法使用。取而代之,我回顾如何集成 PHPStan 到刚进入开发阶段的代码库中。 首个版本的功能不是很强大,这时并未发现多少错误。但从集成的角度来看,它还是非常不错的 — 有空时,我就为它增加新规则,我修复了它在版本库中找到的错误,并将新代码合并到主分支。我们会使用新版本几周用来发现其找到的错误,并不断重复这件事。这种逐级增加的规范性的做法在实践中看来大有裨益,所以我使用 PHPStan 的现有功能来模拟它。默认情况下,PHPStan 只检查它确定的代码—常量,实例化,调用$ this的方法,静态调用的方法,函数和各种语言结构中的现有类。 通过增加级别(从默认值0到当前值4),您还可以增加它对代码所做的假设数量以及它检查的规则数量。如果内建级别无法满足你的要求,你同样也可以自定义规则。少写单元测试! (披沙拣金)可能这个建议你闻所未闻。即便是非常细碎的代码,开发者也不得不编写单元测试,因为这方面犯错的几率都是均等的,例如简单的拼写错误或者忘记将结果赋值给变量。为那些经常出现在控制器或者门脸中的转发代码编写单元测试是很不划算的事。单元测试也有其成本。它们同样也是代码,难逃编写和维护的窠臼。最理想的做法就是在持续集成服务器上,每次更改时都运行 PHPStan,从而在无需单元测试的情况下防止此类错误的产生。实现100%的代码覆盖率真的很难,并且非常昂贵,但你可以静态分析100%的代码。至于单元测试的重点应当集中在静态分析代码难以察觉的,容易出错的地方。包括:复杂的数据过滤,循环,条件判断,乘除法包含舍入的计算等。站在巨人的肩膀上如果不是 Nikita Popov 创建了 PHP Parser。就不会有 PHPStan 的出现。PHP 在 2016 年开始广泛使用 包管理, 单元测试 和 编码标准 的工具。然而到现在也没有一个广泛使用的工具,可以在不运行代码的情况下检查代码中的错误。所以我创建了一个易于使用,快速,可扩展的版本,既不会对您的代码有严格的要求,你还会从这些检查中受益。查看 GitHub 仓库 ,了解如何将其集成到您的项目中!更多文章:https://laravel-china.org/c/t… ...

November 14, 2018 · 1 min · jiezi

Laravel核心解读--HTTP内核

Http KernelHttp Kernel是Laravel中用来串联框架的各个核心组件来网络请求的,简单的说只要是通过public/index.php来启动框架的都会用到Http Kernel,而另外的类似通过artisan命令、计划任务、队列启动框架进行处理的都会用到Console Kernel, 今天我们先梳理一下Http Kernel做的事情。内核绑定既然Http Kernel是Laravel中用来串联框架的各个部分处理网络请求的,我们来看一下内核是怎么加载到Laravel中应用实例中来的,在public/index.php中我们就会看见首先就会通过bootstrap/app.php这个脚手架文件来初始化应用程序:下面是 bootstrap/app.php 的代码,包含两个主要部分创建应用实例和绑定内核至 APP 服务容器<?php// 第一部分: 创建应用实例$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));// 第二部分: 完成内核绑定$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);return $app;HTTP 内核继承自 IlluminateFoundationHttpKernel类,在 HTTP 内核中 内它定义了中间件相关数组, 中间件提供了一种方便的机制来过滤进入应用的 HTTP 请求和加工流出应用的HTTP响应。<?phpnamespace App\Http;use Illuminate\Foundation\Http\Kernel as HttpKernel;class Kernel extends HttpKernel{ /** * The application’s global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array / protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, ]; /* * The application’s route middleware groups. * * @var array / protected $middlewareGroups = [ ‘web’ => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ‘api’ => [ ’throttle:60,1’, ‘bindings’, ], ]; /* * The application’s route middleware. * * These middleware may be assigned to groups or used individually. * * @var array / protected $routeMiddleware = [ ‘auth’ => \Illuminate\Auth\Middleware\Authenticate::class, ‘auth.basic’ => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, ‘bindings’ => \Illuminate\Routing\Middleware\SubstituteBindings::class, ‘can’ => \Illuminate\Auth\Middleware\Authorize::class, ‘guest’ => \App\Http\Middleware\RedirectIfAuthenticated::class, ’throttle’ => \Illuminate\Routing\Middleware\ThrottleRequests::class, ];}在其父类 「IlluminateFoundationHttpKernel」 内部定义了属性名为 「bootstrappers」 的 引导程序 数组:protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class,];引导程序组中 包括完成环境检测、配置加载、异常处理、Facades 注册、服务提供者注册、启动服务这六个引导程序。有关中间件和引导程序相关内容的讲解可以浏览我们之前相关章节的内容。应用解析内核在将应用初始化阶段将Http内核绑定至应用的服务容器后,紧接着在public/index.php中我们可以看到使用了服务容器的make方法将Http内核实例解析了出来:$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);在实例化内核时,将在 HTTP 内核中定义的中间件注册到了 路由器,注册完后就可以在实际处理 HTTP 请求前调用路由上应用的中间件实现过滤请求的目的:namespace Illuminate\Foundation\Http;…class Kernel implements KernelContract{ /* * Create a new HTTP kernel instance. * * @param \Illuminate\Contracts\Foundation\Application $app * @param \Illuminate\Routing\Router $router * @return void / public function __construct(Application $app, Router $router) { $this->app = $app; $this->router = $router; $router->middlewarePriority = $this->middlewarePriority; foreach ($this->middlewareGroups as $key => $middleware) { $router->middlewareGroup($key, $middleware); } foreach ($this->routeMiddleware as $key => $middleware) { $router->aliasMiddleware($key, $middleware); } }}namespace Illuminate/Routing;class Router implements RegistrarContract, BindingRegistrar{ /* * Register a group of middleware. * * @param string $name * @param array $middleware * @return $this / public function middlewareGroup($name, array $middleware) { $this->middlewareGroups[$name] = $middleware; return $this; } /* * Register a short-hand name for a middleware. * * @param string $name * @param string $class * @return $this / public function aliasMiddleware($name, $class) { $this->middleware[$name] = $class; return $this; }}处理HTTP请求通过服务解析完成Http内核实例的创建后就可以用HTTP内核实例来处理HTTP请求了//public/index.php$response = $kernel->handle( $request = Illuminate\Http\Request::capture());在处理请求之前会先通过Illuminate\Http\Request的 capture() 方法以进入应用的HTTP请求的信息为基础创建出一个 Laravel Request请求实例,在后续应用剩余的生命周期中Request请求实例就是对本次HTTP请求的抽象,关于Laravel Request请求实例的讲解可以参考以前的章节。将HTTP请求抽象成Laravel Request请求实例后,请求实例会被传导进入到HTTP内核的handle方法内部,请求的处理就是由handle方法来完成的。namespace Illuminate\Foundation\Http;class Kernel implements KernelContract{ /* * Handle an incoming HTTP request. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response / public function handle($request) { try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } $this->app[’events’]->dispatch( new Events\RequestHandled($request, $response) ); return $response; }}handle 方法接收一个请求对象,并最终生成一个响应对象。其实handle方法我们已经很熟悉了在讲解很多模块的时候都是以它为出发点逐步深入到模块的内部去讲解模块内的逻辑的,其中sendRequestThroughRouter方法在服务提供者和中间件都提到过,它会加载在内核中定义的引导程序来引导启动应用然后会将使用Pipeline对象传输HTTP请求对象流经框架中定义的HTTP中间件们和路由中间件们来完成过滤请求最终将请求传递给处理程序(控制器方法或者路由中的闭包)由处理程序返回相应的响应。关于handle方法的注解我直接引用以前章节的讲解放在这里,具体更详细的分析具体是如何引导启动应用以及如何将传输流经各个中间件并到达处理程序的内容请查看服务提供器、中间件还有路由这三个章节。protected function sendRequestThroughRouter($request){ $this->app->instance(‘request’, $request); Facade::clearResolvedInstance(‘request’); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter());} /引导启动Laravel应用程序1. DetectEnvironment 检查环境2. LoadConfiguration 加载应用配置3. ConfigureLogging 配置日至4. HandleException 注册异常处理的Handler5. RegisterFacades 注册Facades 6. RegisterProviders 注册Providers 7. BootProviders 启动Providers/public function bootstrap(){ if (! $this->app->hasBeenBootstrapped()) { /**依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数 $bootstrappers = [ ‘Illuminate\Foundation\Bootstrap\DetectEnvironment’, ‘Illuminate\Foundation\Bootstrap\LoadConfiguration’, ‘Illuminate\Foundation\Bootstrap\ConfigureLogging’, ‘Illuminate\Foundation\Bootstrap\HandleExceptions’, ‘Illuminate\Foundation\Bootstrap\RegisterFacades’, ‘Illuminate\Foundation\Bootstrap\RegisterProviders’, ‘Illuminate\Foundation\Bootstrap\BootProviders’, ];/ $this->app->bootstrapWith($this->bootstrappers()); }}发送响应经过上面的几个阶段后我们最终拿到了要返回的响应,接下来就是发送响应了。//public/index.php$response = $kernel->handle( $request = Illuminate\Http\Request::capture());// 发送响应$response->send();发送响应由 Illuminate\Http\Response的send()方法完成父类其定义在父类Symfony\Component\HttpFoundation\Response中。public function send(){ $this->sendHeaders();// 发送响应头部信息 $this->sendContent();// 发送报文主题 if (function_exists(‘fastcgi_finish_request’)) { fastcgi_finish_request(); } elseif (!\in_array(PHP_SAPI, array(‘cli’, ‘phpdbg’), true)) { static::closeOutputBuffers(0, true); } return $this;}关于Response对象的详细分析可以参看我们之前讲解Laravel Response对象的章节。终止应用程序响应发送后,HTTP内核会调用terminable中间件做一些后续的处理工作。比如,Laravel 内置的「session」中间件会在响应发送到浏览器之后将会话数据写入存储器中。// public/index.php// 终止程序$kernel->terminate($request, $response);//Illuminate\Foundation\Http\Kernelpublic function terminate($request, $response){ $this->terminateMiddleware($request, $response); $this->app->terminate();}// 终止中间件protected function terminateMiddleware($request, $response){ $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge( $this->gatherRouteMiddleware($request), $this->middleware ); foreach ($middlewares as $middleware) { if (! is_string($middleware)) { continue; } list($name, $parameters) = $this->parseMiddleware($middleware); $instance = $this->app->make($name); if (method_exists($instance, ’terminate’)) { $instance->terminate($request, $response); } }}Http内核的terminate方法会调用teminable中间件的terminate方法,调用完成后从HTTP请求进来到返回响应整个应用程序的生命周期就结束了。总结本节介绍的HTTP内核起到的主要是串联作用,其中设计到的初始化应用、引导应用、将HTTP请求抽象成Request对象、传递Request对象通过中间件到达处理程序生成响应以及响应发送给客户端。这些东西在之前的章节里都有讲过,并没有什么新的东西,希望通过这篇文章能让大家把之前文章里讲到的每个点串成一条线,这样对Laravel整体是怎么工作的会有更清晰的概念。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

November 11, 2018 · 3 min · jiezi

使用postman调试jwt开发的接口

我的github博客:https://zgxxx.github.io/上一篇博客文章https://segmentfault.com/a/11… 介绍了laravel使用dingo+jwt开发API的几个步骤,那么在实际操作中,我们需要测试API$api = app(‘Dingo\Api\Routing\Router’);$api->version(‘v1’, [’namespace’ => ‘App\Http\Controllers\V1’], function ($api) { $api->post(‘register’, ‘AuthController@register’); $api->post(’login’, ‘AuthController@login’); $api->post(’logout’, ‘AuthController@logout’); $api->post(‘refresh’, ‘AuthController@refresh’); $api->post(‘me’, ‘AuthController@me’); $api->get(’test’, ‘AuthController@test’);});设置了这几个路由,对应的url类似这样:http://www.yourweb.com/api/me 使用postman来调试这些API。请求API的大致流程我们使用jwt代替session,首先是通过登录(jwt的attempt方法验证账号密码),成功后会返回一个JWT,我们把这个字符串统一叫做token,这个token需要我们客户端保存起来,然后后面需要认证的接口就在请求头里带上这个token,后台验证正确后就会进行下一操作,如果token错误,或者过期就返回401或500错误,拒绝后面的操作。前端可以保存在localStorage,小程序可以 使用wx.setStorageSync保存所以请求头信息Authorization:Bearer + token很重要,但是有个问题,这个token是有一个刷新时间和过期时间的:’ttl’ => env(‘JWT_TTL’, 60),‘refresh_ttl’ => env(‘JWT_REFRESH_TTL’, 20160),refresh_ttl是过期时间,默认14天,很好理解,就像一些网站一样,你好几个月没去登录,你的账号会自动退出登录,因为过期了,需要重新输入账号密码登录。ttl刷新时间默认60分钟,也就是说你拿这个一小时前的token去请求是不行的,会报The token has been blacklisted的错误,意思是说这个旧的token已经被列入黑名单,无法使用token是会被别人盗取的,所以token需要每隔一段时间就更新一次这时候有个问题,每隔一小时就更新,那岂不是要每小时就重新登录一遍来获取新token?当然不需要,我们可以写个中间件来实现无痛刷新token,用户不会察觉我们已经更新了token。<?phpnamespace App\Http\Middleware;use Closure;use Tymon\JWTAuth\Exceptions\JWTException;use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;use Tymon\JWTAuth\Exceptions\TokenExpiredException;use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;class RefreshToken extends BaseMiddleware{ /** * @author: zhaogx * @param $request * @param Closure $next * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|mixed * @throws JWTException */ public function handle($request, Closure $next) { // 检查此次请求中是否带有 token,如果没有则抛出异常。 $this->checkForToken($request); // 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException 异常 try { // 检测用户的登录状态,如果正常则通过 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException(‘jwt-auth’, ‘未登录’); } catch (TokenExpiredException $exception) { // 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中 try { // 刷新用户的 token $token = $this->auth->refresh(); // 使用一次性登录以保证此次请求的成功 \Auth::guard(‘api’)->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()[‘sub’]); } catch (JWTException $exception) { // 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。 throw new UnauthorizedHttpException(‘jwt-auth’, $exception->getMessage()); } } return $next($request)->withHeaders([ ‘Authorization’=> ‘Bearer ‘.$token, ]); }}一旦中间件检测到token过时了,就自动刷新token,然后在响应头把新的token返回来,我们客户端可以根据响应头是否有’Authorization’来决定是否要替换token在使用postman调试这些API的时候就有个问题,postman又没有前端代码,我怎么及时更新这个token,难道每次请求都要去看响应头,发现Authorization后手动去复制黏贴吗,当然也不需要,postman有个强大的环境变量,其实也是前端js的东西。postman自动刷新请求头token登录后自动获取token首先点击设置环境这个按钮,点击Add按钮添加一个变量,我们设置key值为access_token,接着我们在登录接口的Tests中去赋值这个变量var data = JSON.parse(responseBody); if (data.result.access_token) { tests[“Body has token”] = true; var tokenArray = data.result.access_token.split(" “); postman.setEnvironmentVariable(“access_token”, tokenArray[1]); } else { tests[“Body has token”] = false; } 这段js代码就是获取请求成功后返回的access_token值,将其赋值给postman的环境变量,我们看到请求成功后我们后台返回了一个json,其中就有我们需要的access_token,我们可以再去环境变量那里看看这时候的变量有什么变化可以看到这里的变量access_token已经有值了,就是我们后台返回来的access_token字符串,说明赋值成功接着我们到另一个需要认证的接口测试我们在Authorization类型type选择Bearer Token,在后面Token表单那里打一个’{‘就会自动提示我们设置过的变量{{access_token}}发送请求测试下已经成功了。无痛刷新token那如果token刷新了,经过后台中间件无痛刷新后,会在响应头返回一个新的token(这一次请求用的是旧的token,默认认证通过)现在我们需要在这个接口上直接更新我们的变量access_token(如下图),而不需要去再请求一遍登录接口var authHeader = postman.getResponseHeader(‘Authorization’);if (authHeader){ var tokenArray = authHeader.split(” “); postman.setEnvironmentVariable(“access_token”, tokenArray[1]); tests[“Body has refreshtoken”] = true; } else { tests[“Body has no refreshtoken”] = false; }这段js代码就是将响应头中的Authorization赋值给我们的access_token这是响应头的Authorization,截取了最后面的字符串刷新时间过了后,我们试着再发一次请求,我们可以看到,还是可以访问的,而且请求头里的Authorization已经自动更新过来了用一句话整理大概就是,你需要在哪个接口响应后更新变量,就去这个这个口的Test下写js赋值代码postman.setEnvironmentVariable(“access_token”, token);,只要没错误你就可以在别的地方使用{{access_token}}更新替换了。 ...

November 9, 2018 · 1 min · jiezi

laravel5.5+dingo+JWT开发后台API

dingo api 中文文档: https://www.bookstack.cn/read...Laravel中使用JWT:https://laravel-china.org/art…辅助文章: https://www.jianshu.com/p/62b…参考https://www.jianshu.com/p/62b… 这篇文章基本就能搭建出环境,我使用的版本跟他一样 “dingo/api”: “2.0.0-alpha1”,“tymon/jwt-auth”: “^1.0.0-rc.1”,不知道别的版本有啥大的区别,但是网上找的其他一些文章使用的是旧的版本,jwt封装的东西路径可能不一样,可能会保错,有些文档还说要手动添加TymonJWTAuthProvidersLaravelServiceProvider::class和DingoApiProviderLaravelServiceProvider::class,其实新版本不需要。1. composer.json引入包,执行composer update: “require”: { …… “dingo/api”: “2.0.0-alpha1”, “tymon/jwt-auth”: “^1.0.0-rc.1” },2. 执行下面两个语句自动生成dingo和jwt的配置文件:php artisan vendor:publish –provider=“Dingo\Api\Provider\LaravelServiceProvider”//config文件夹中生成dingo配置文件—> api.phpphp artisan vendor:publish –provider=“Tymon\JWTAuth\Providers\LaravelServiceProvider”//config文件夹中生成dingo配置文件—> jwt.php3. 配置 .env具体配置可参考 文档https://www.bookstack.cn/read… ,我的配置是API_STANDARDS_TREE=vndAPI_PREFIX=apiAPI_VERSION=v1API_DEBUG=trueAPI_SUBTYPE=myapp 还需在命令行执行 php artisan jwt:secret,会在.env自动添加JWT_SECRET,其他若需要,可以到各种的配置文件中看,在.env添加即可4. 关键处理’defaults’ => [ ‘guard’ => ‘web’, ‘passwords’ => ‘users’, ], ‘guards’ => [ ‘web’ => [ ‘driver’ => ‘session’, ‘provider’ => ‘users’, ], ‘api’ => [ ‘driver’ => ‘jwt’, ‘provider’ => ‘users’, ], ],这里需要把api原本的driver => session 改为使用jwt机制,provider对应你要用的用户认证表,一般就是登录注册那张表<?phpnamespace App\Models;use Tymon\JWTAuth\Contracts\JWTSubject;use Illuminate\Notifications\Notifiable;use Illuminate\Foundation\Auth\User as Authenticatable;class User extends Authenticatable implements JWTSubject { use Notifiable; /** * The attributes that are mass assignable. * * @var array / protected $fillable = [ ’name’, ’email’, ‘password’, ‘unionid’ ]; /* * The attributes that should be hidden for arrays. * * @var array / protected $hidden = [ ‘password’, ‘remember_token’, ]; // Rest omitted for brevity /* * Get the identifier that will be stored in the subject claim of the JWT. * * @return mixed / public function getJWTIdentifier() { return $this->getKey(); } /* * Return a key value array, containing any custom claims to be added to the JWT. * * @return array / public function getJWTCustomClaims() { return []; }}5. 设置控制器考虑到可能后面需要开发不同版本api,所以在app/Http/Controller下建立了V1,V2目录,根据你自己的需求来,只要写好命名空间就ok <?php/* * Date: 17/10/12 * Time: 01:07 /namespace App\Http\Controllers\V1;use App\Http\Controllers\Controller;use Illuminate\Http\Request;use Illuminate\Support\Facades\Auth;use Validator;use App\User;class AuthController extends Controller{ protected $guard = ‘api’;//设置使用guard为api选项验证,请查看config/auth.php的guards设置项,重要! /* * Create a new AuthController instance. * * @return void / public function __construct() { $this->middleware(‘refresh’, [’except’ => [’login’,‘register’]]); } public function test(){ echo “test!!”; } public function register(Request $request) { $rules = [ ’name’ => [‘required’], ’email’ => [‘required’], ‘password’ => [‘required’, ‘min:6’, ‘max:16’], ]; $payload = $request->only(’name’, ’email’, ‘password’); $validator = Validator::make($payload, $rules); // 验证格式 if ($validator->fails()) { return $this->response->array([’error’ => $validator->errors()]); } // 创建用户 $result = User::create([ ’name’ => $payload[’name’], ’email’ => $payload[’email’], ‘password’ => bcrypt($payload[‘password’]), ]); if ($result) { return $this->response->array([‘success’ => ‘创建用户成功’]); } else { return $this->response->array([’error’ => ‘创建用户失败’]); } } /* * Get a JWT token via given credentials. * * @param \Illuminate\Http\Request $request * * @return \Illuminate\Http\JsonResponse / public function login(Request $request) { $credentials = $request->only(’email’, ‘password’); if ($token = $this->guard()->attempt($credentials)) { return $this->respondWithToken($token); } return $this->response->errorUnauthorized(‘登录失败’); } /* * Get the authenticated User * * @return \Illuminate\Http\JsonResponse / public function me() { //return response()->json($this->guard()->user()); return $this->response->array($this->guard()->user()); } /* * Log the user out (Invalidate the token) * * @return \Illuminate\Http\JsonResponse / public function logout() { $this->guard()->logout(); //return response()->json([‘message’ => ‘Successfully logged out’]); return $this->response->array([‘message’ => ‘退出成功’]); } /* * Refresh a token. * * @return \Illuminate\Http\JsonResponse / public function refresh() { return $this->respondWithToken($this->guard()->refresh()); } /* * Get the token array structure. * * @param string $token * * @return \Illuminate\Http\JsonResponse / protected function respondWithToken($token) { return response()->json([ ‘access_token’ => $token, ’token_type’ => ‘bearer’, ’expires_in’ => $this->guard()->factory()->getTTL() * 60 ]); } /* * Get the guard to be used during authentication. * * @return \Illuminate\Contracts\Auth\Guard / public function guard() { return Auth::guard($this->guard); }}控制器中命名空间namespace需要设置好,路由的时候需要用到, $this->middleware(‘refresh’, [’except’ => [’login’,‘register’]]);这里的中间件使用的是网上找的,用于无痛刷新jwt的token,具体可以参考这篇文章:https://www.jianshu.com/p/9e9…6. refresh中间件<?phpnamespace App\Http\Middleware;use Closure;use Tymon\JWTAuth\Exceptions\JWTException;use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;use Tymon\JWTAuth\Exceptions\TokenExpiredException;use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;class RefreshToken extends BaseMiddleware{ /* * @author: zhaogx * @param $request * @param Closure $next * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|mixed * @throws JWTException */ public function handle($request, Closure $next) { // 检查此次请求中是否带有 token,如果没有则抛出异常。 $this->checkForToken($request); // 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException 异常 try { // 检测用户的登录状态,如果正常则通过 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException(‘jwt-auth’, ‘未登录’); } catch (TokenExpiredException $exception) { // 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中 try { // 刷新用户的 token $token = $this->auth->refresh(); // 使用一次性登录以保证此次请求的成功 \Auth::guard(‘api’)->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()[‘sub’]); } catch (JWTException $exception) { // 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。 throw new UnauthorizedHttpException(‘jwt-auth’, $exception->getMessage()); } } return $next($request)->withHeaders([ ‘Authorization’=> ‘Bearer ‘.$token, ]); }}写好中间件后需要在app/Http/Kernel.php中注入 protected $routeMiddleware = [ …… ‘refresh’ => RefreshToken::class, ];7. routes/api.php 设置路由$api = app(‘Dingo\Api\Routing\Router’);$api->version(‘v1’, [’namespace’ => ‘App\Http\Controllers\V1’], function ($api) { $api->post(‘register’, ‘AuthController@register’); $api->post(’login’, ‘AuthController@login’); $api->post(’logout’, ‘AuthController@logout’); $api->post(‘refresh’, ‘AuthController@refresh’); $api->post(‘me’, ‘AuthController@me’); $api->get(’test’, ‘AuthController@test’);});这里有个坑,不要这样写$api->post(‘me’,[‘middleware’ =>‘refresh’], ‘AuthController@me’);这样虽然能执行这个中间件但执行到$next($request)这里会出错,貌似是一个回调报错 Function name must be a string ,不太清楚具体原因,可以这样写$api->post(‘me’,, ‘AuthController@me’)->middleware(‘refresh’);根据以上几个步骤就可以建立起简单的api后台基础,获取api路由列表可以使用命令行: php artisan api:routesroutes:list貌似无法显示以上api路由,需要在api.php那里再写一遍原始的laravel路由定义才可以显示:比如这样Route::post(‘api/test’, ‘AuthController@test’);后续会用另一篇幅来记录postman和小程序相关知识,可以关注我的博客:https://zgxxx.github.io ...

November 8, 2018 · 3 min · jiezi

Laravel Telescope:优雅的应用调试工具

文章转自:https://laravel-china.org/topics/19013\视频教程:047. 优雅的应用调试工具–laravel/telescope (5.7 新扩展)Laravel Telescope 是由 Mohamed Said 和 Taylor Otwell 开源 的 Laravel 应用的调试工具。你可以使用 Composer 安装到你的应用中。安装完 Telescope 后,你可以访问 /telescope 来访问该应用。Telescope 能做什么事?如果你之前用过 Clockwork 或者 Laravel Debugbar ,那么这两款应用与 Telescope 进行对比的话就是纯 UI 界面和重量级武器。Telescope 由一系列监听器组成,这些 “监听器” 监听每个进入应用的请求,不管是来自 HTTP 、命令行、任务调度还是队列的。这些监听器捕获这些请求以及其相关数据信息 – 例如数据库查询以及其执行时间,是否命中缓存,事件触发邮件触发等等。在它操作界面上有用于检查以下各项的选项栏,每个选项栏都代表它的监听器:RequestsCommandsScheduleJobsExceptionsLogsDumpsQueriesModelsEventsMailNotificationsCacheRedis观察者标签让我们逐步浏览每个选项查看观察到的内容。每个选项都显示一个列表页面,然后您可以点击查看指定项目的详细信息。(HTTP) 请求该选项允许您查看进入应用程序的所有 HTTP 请求。您将能查看所有 HTTP 请求以及每个请求的详细信息。每个请求页面还会显示来自其他观察者关于此请求相关的数据;例如,所有数据库查询以及它们花费时长;该请求已通过身份验证用户;等等。命令行命令选项列出已运行的所有命令及其退出代码。您还可以点击查看所有参数,选项和相关内容。计划任务列出已运行的计划任务。在每个任务的详细信息页面上,查看他们的所有计划信息,例如他们的 cron 计划(例如 * * * * *)。任务任务部分会列出所有运行过,和正在运行的任务。他和Horizon很类似,不过Horizon只支持Redis驱动,而且它不仅仅是UI,它还能和队列沟通,看你队列运行的情况。Telescope,简简单单只是一个UI,一个可以和任何队列驱动玩在一起的UI。在任务列表页上,你会看到任务名,和它在哪个队列和连接上运行,她的工作情况,和其所发生的经历。在任务细节页面上,你会看到以上列举的数据,以及:主机名, 他的FQCN,网络连接,队列,尝试次数,超时,还有标签。任务会自动给用过的Eloquent模型贴标签 (栗子: App\Video:1) ,如果用过用户模型,就会给用户模型贴标签,以此类推。标签\诸如请求,命令等项目,会自动被Telescope贴标签 (举栗子: 如果一个用户发出了请求,他就自动会被贴上 Auth:1 if User 1 ; 如果你点击那个标签, Telescope就只会显示被贴上该标签的项目)如HTTP请求一般,你可以看到所有与此任务相关的信息,比如数据库查询记录,其触发的其他任务,和任何生成的日志。不过,你如若出发了封闭函数,那么你所见之信息不是 App\Jobs\RenderVideo , 取而代之的是 Closure (web.php:43) .新封闭函数队列\Taylor写了一个新的库,加回了队列封闭函数 (Laravel很多年前用过)。 这个库的功能是,当你将一个模型放入封闭函数中,它只会存这个模型的ID,而不是整个对象。(原作者说的: 岂不妙哉?(反正队列的类已经如此作为了),他很兴奋)。\dispatch(function () use ($video) { // do stuff in a queued job // 做一些队列的事情 });\这样做以后,封闭函数会被序列化,并且有一个哈希(Hash)值如影随形。这样可以防止你的代码在进入队列事件后,代码被篡改,很是不好。现在有了哈希,函数会先被检查一遍,妈妈就不怕我的代码被篡改了。\该封闭函数会被序列化为一个长字符串,加上他的哈希(与签名URL如出一辙)Exceptions该功能将记录所有异常,并可查看具体异常情况。界面使用选项卡的形式呈现,包括主机信息,类型,请求,标签,用户身份验证等。除此之外也可看到异常在代码中的位置,使其高亮并展示上下代码段,且包含完整的堆栈追踪。你还可以从抛出异常请求中获取指向异常详情页面的链接。注意:在众多选项卡中,如果您在单个页面上(例如,给定的异常页面),你也可获得指向生成该页面的请求链接。如果产生多次相同的异常,它们将在列表页面上进行分组,但仍然可以深入查看异常显示页面中的各个异常。Logs日志项展示了日志的基本信息,级别和每条日志项的记录时间。当你访问日志的单个详细页面时,你可以看到更多消息,包含所有你传递给日志的上下文数据(作为数组)。“比挖掘原始文本文件棒一点。".当你用数组为你的日志项传递上下文时,你可以查看所有数据,查看触发它的请求,触发的用户。“比挖掘原始文本文件棒一点。Dump screen"这是我最爱的功能之一"如果你代码中使用 dump() 函数,而且你在 Telescope 中打开了 dump screen。你可以在 Telescope 中看到 dumps 并非来自你实际应用。这为你提供了数据的 dd() 样式输出,而不会弄乱您的正常页面加载。每个 dump 还链接到生成它的请求。如果你离开 dump screen,你所有的 dumps 会突然再次显示到你的浏览器上。Queries列出了所有数据查询相关信息,就像 debug bar 一样。如 消耗时常、完整查询、请求触发 等。漂亮的格式化显示。可以在服务中配置慢查询的边界,一旦查询查过其配置时间将会被标记,并配以红色警告显示。注意:每个列表页都有快捷方式和快速搜索。搜索标签和其他内容。Models可以看到 查询、更新、删除事件;以及这些事件产生的变化 等。事件显示所有事件的列表。可以看到哪些事件是通过标记广播的;查看所有侦听器的列表,并深入了解调用的对象。邮件显示发送的所有电子邮件的列表;收件人是谁;什么时候发的;是否还在队列,然后什么时候出队的。可以看到电子邮件主题,当你深入研究它时,你也会看到诸如 MailTrap 的邮件预览。甚至可以下载原始的 .eml 文件并在选定的客户端中打开。Notifications显示所有通知,及其类型,等等。无法预览,因为有些通知是不可预览的,假如是邮件通知,你就会看到它在列表中。如果通知已进入队列,还可以在 Jobs 的请求部分看到。有很多方式可以得到这些数据。Cache显示缓存命中、未命中和更新等。显示键,值,何时过期。可以看到触发它的请求,也可以在请求页面上看到该请求的所有缓存命中/未命中。Redis跟上面的缓存类似。诸如花了多久时间,什么时候发生,什么时候发起请求等等。Authenticated user在任一选项卡的条目上获取已验证用户的相关信息。Authorization可在生产环境的 telescope 服务中,配置可访问的邮件账户列表在Gate 的 viewTelescope 中定义哪些用户可以访问筛选你可能不想在生产环境中把所有东西都存着,所以你可以在 Telescope 服务提供者中, 运行 Telescope::filter(function ($entry))。默认筛选器:function ($entry) { if (local) { return true; } return $entry->isReportableException || $entry->isfailedJob() || $entry->isScheduledTask() || $entry->hasMonitoredTag();}但是你可以自由地修改它。监控标签:点进雷达按钮,声明一个监控标签。你可以在 UI 界面声明一个 Auth:1 监视器。生产环境中不会记录请求,但是如果你有一个像 Auth:1 这样的监视器,你就会看到所有的请求都被记录下来,除非你取消监视。NOTE: 如果你使用的是 Redis 队列的话, Horizon 和 Telescope 能完美搭配。修剪在 Telescope 中任务调度会修剪掉过期的条目。你可以每晚都删除超过__ 小时的东西。这个也是在 config/telescope 中设置。可以随时启用或弃用任意观察者。 E.g. Watchers\CacheWatcher::class 就可以弃用。还有一个 TELESCOPE_LIMIT 默认定义是 100 ;该选项的意义就是一次性进行 100 个查询,100 次 Redis 查询等。它们都可以在env中进行配置。杂项Telescope 可以在本地和生产环境中运行,并有内建授权和工具用来保护私有数据。它可从多角度访问同类数据,具备一系列配置项,提供了健壮的标记和过滤功能。考虑把它放在一个独立的数据库中。Taylor 稍后就在 Twitter 上提到你可以添加过滤器从而确保私有数据不会被记录下来。你可以使用 Telescope::night() 来开启夜晚模式(可能在某个服务提供者那里?)Q&A:数据存放在何处?隐藏在一个 StorageRepository 接口实现之后; 类似数据库一样运作在 Redis 上。你可以随心所欲的实现它。这个接口中只有6-7 个方法。它能存多少数据?不是太多,因为生产环境几乎会抛弃所有的东西,修剪下来,你一次只能保存 100 个。我们能从 Slack 收到通知吗?我们正在努力。我能退出 Bugsnag/etc.吗? 可能不能。虽然它简易且轻便,但并不意味着稳定健壮。小心火烛。我们能否按照时间戳进行过滤?暂时还不能,但是这个是开源项目,帮帮我们在系统引导阶段会产生什么影响?每次只会执行一个查询。生产环境中不会频繁地把所有东西都插入进去。你可以取消你不关心的监听器。我们能在同一个UI中检查多个应用吗?可以;只需要在同一个数据库中指向并记录它们,然后考虑做标记/过滤, 这样你就可以按需做区分了。Laravel 的哪个版本能与之兼容? 5.7.7+。 ...

November 8, 2018 · 1 min · jiezi

laravel sentry

注册登录 GitHub登录创建project 选择 laravel安装扩展使用composer require sentry/sentry-laravelphp artisan vendor:publish –provider=“Sentry\SentryLaravel\SentryLaravelServiceProvider"public function report(Exception $exception){ if (app()->bound(‘sentry’) && $this->shouldReport($exception)) { app(‘sentry’)->captureException($exception); } parent::report($exception);}vi config/sentry.phpreturn array( ‘dsn’ => env(‘DSN’), // capture release as git sha // ‘release’ => trim(exec(‘git log –pretty="%h” -n1 HEAD’)), // Capture bindings on SQL queries ‘breadcrumbs.sql_bindings’ => true, // Capture default user context ‘user_context’ => false, //transport function ’transport’=>new \App\SentryTransport(),);vi /app/SentryTransport.phpnamespace App;class SentryTransport{ public static function __set_state($array){ return function($raven_client,$data){ \Queue::pushOn(‘sentry_log’,new \App\Commands\sentry($raven_client,$data)); }; }}vi app/commands/sentry.phpclass sentry extends Command implements SelfHandling, ShouldBeQueued { use InteractsWithQueue, SerializesModels; private $raven_client; protected $data; /** * Create a new command instance. * * @return void / public function __construct($raven_client, $data) { $raven_client->setTransport(null); $raven_client->close_curl_resource(); $this->raven_client=$raven_client; $this->data=$data; } /* * Execute the command. * * @return void / public function handle() { $this->raven_client->send($this->data);//send方法来自 /vendor/sentry/sentry/lib/Raven/Client.php:1019 }}vi /vendor/sentry/sentry/lib/Raven/Client.phppublic function send(&$data) { if (is_callable($this->send_callback) && call_user_func_array($this->send_callback, array(&$data)) === false ) { // if send_callback returns false, end native send return; } if (!$this->server) { return; } if ($this->transport) { call_user_func($this->transport, $this, $data); return; } // should this event be sampled? if (rand(1, 100) / 100.0 > $this->sample_rate) { return; } $message = $this->encode($data); $headers = array( ‘User-Agent’ => static::getUserAgent(), ‘X-Sentry-Auth’ => $this->getAuthHeader(), ‘Content-Type’ => ‘application/octet-stream’ ); $this->send_remote($this->server, $message, $headers); }配置dsn获取 dsn 测试少写个分号,查看效果monolog 发送到sentrycomposer require monolog/monologvi config/app.php’App\Providers\sentrylog’ vi App\Providers\sentrylog.phpuse Monolog\Handler\RedisHandler;use Monolog\Formatter\JsonFormatter;use Monolog\Formatter\LineFormatter;use Monolog\Processor\MemoryPeakUsageProcessor;use Monolog\Processor\WebProcessor;use Monolog\Handler\RavenHandler;class sentrylog extends ServiceProvider { /* * Bootstrap the application services. * * @return void / public function boot() { $logger = \Log::getMonolog(); $handler = new RedisHandler($redis, “sentry:monolog”, \Monolog\Logger::DEBUG); $handler->setFormatter(new JsonFormatter(JsonFormatter::BATCH_MODE_NEWLINES, true)); $handler->pushProcessor(new MemoryPeakUsageProcessor(true)); /$logger->pushProcessor(function ($record) { $record[’extra’][‘dummy’] = ‘Hello world!’; return $record; }); class MemoryPeakUsageProcessor extends MemoryProcessor{ /** * @param array $record 对象当方法调用时执行 * @return array / public function __invoke(array $record) { $bytes = memory_get_peak_usage($this->realUsage); $formatted = $this->formatBytes($bytes); $record[’extra’][‘memory_peak_usage’] = $formatted; return $record; }}/ $arr = [ ‘uri’ => ‘REQUEST_URI’, ‘ip’ => ‘REMOTE_ADDR’, ‘method’ => ‘REQUEST_METHOD’, ‘query_string’ => ‘QUERY_STRING’, ‘cookie’ => ‘HTTP_COOKIE’, ‘host’ => ‘HTTP_HOST’, ]; $handler->pushProcessor(new WebProcessor(null, $arr)); $logger->pushHandler($handler); } /** * Register the application services. * * @return void */ public function register() { // }}vi app/commands/sentry_monolog.phpuse Monolog\Handler\RavenHandler; while (true) { $data = $redis->lpop(“sentry:monolog”); if (!$data) { sleep(5); continue; } $data = json_decode($data, true); $raven_client= new \Raven_Client($dsn,[’extra’ => $data[’extra’]]); $raven_hanlder = new RavenHandler($raven_client); $raven_hanlder->handle($data); }资源你也可以本地搭建Sentry 之部署到生产环境搭建自己的 sentry 服务CentOS6 基于 Python 安装 SentrySentry 自动化异常提醒Laravel学习笔记之Errors Tracking神器——Sentrysentry使用利用 entry/onpremise 搭建一个 Sentry 异常汇总工具高效利用Sentry追踪日志发现问题sentry monologSentry - 处理异常日志的正确姿势Sentry监控Django应用并使用email+钉钉通知搭建私有的前端监控服务: sentry ...

October 26, 2018 · 2 min · jiezi

Laravel核心解读--ENV的加载和读取

Laravel在启动时会加载项目中的.env文件。对于应用程序运行的环境来说,不同的环境有不同的配置通常是很有用的。 例如,你可能希望在本地使用测试的Mysql数据库而在上线后希望项目能够自动切换到生产Mysql数据库。本文将会详细介绍 env 文件的使用与源码的分析。Env文件的使用多环境env的设置项目中env文件的数量往往是跟项目的环境数量相同,假如一个项目有开发、测试、生产三套环境那么在项目中应该有三个.env.dev、.env.test、.env.prod三个环境配置文件与环境相对应。三个文件中的配置项应该完全一样,而具体配置的值应该根据每个环境的需要来设置。接下来就是让项目能够根据环境加载不同的env文件了。具体有三种方法,可以按照使用习惯来选择使用:在环境的nginx配置文件里设置APP_ENV环境变量fastcgi_param APP_ENV dev;设置服务器上运行PHP的用户的环境变量,比如在www用户的/home/www/.bashrc中添加export APP_ENV dev在部署项目的持续集成任务或者部署脚本里执行cp .env.dev .env 针对前两种方法,Laravel会根据env(‘APP_ENV’)加载到的变量值去加载对应的文件.env.dev、.env.test这些。 具体在后面源码里会说,第三种比较好理解就是在部署项目时将环境的配置文件覆盖到.env文件里这样就不需要在环境的系统和nginx里做额外的设置了。自定义env文件的路径与文件名env文件默认放在项目的根目录中,laravel 为用户提供了自定义 ENV 文件路径或文件名的函数,例如,若想要自定义 env 路径,可以在 bootstrap 文件夹中 app.php 中使用Application实例的useEnvironmentPath方法:$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));$app->useEnvironmentPath(’/customer/path’)若想要自定义 env 文件名称,就可以在 bootstrap 文件夹中 app.php 中使用Application实例的loadEnvironmentFrom方法:$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));$app->loadEnvironmentFrom(‘customer.env’)Laravel 加载ENV配置Laravel加载ENV的是在框架处理请求之前,bootstrap过程中的LoadEnvironmentVariables阶段中完成的。我们来看一下\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables的源码来分析下Laravel是怎么加载env中的配置的。<?phpnamespace Illuminate\Foundation\Bootstrap;use Dotenv\Dotenv;use Dotenv\Exception\InvalidPathException;use Symfony\Component\Console\Input\ArgvInput;use Illuminate\Contracts\Foundation\Application;class LoadEnvironmentVariables{ /** * Bootstrap the given application. * * @param \Illuminate\Contracts\Foundation\Application $app * @return void / public function bootstrap(Application $app) { if ($app->configurationIsCached()) { return; } $this->checkForSpecificEnvironmentFile($app); try { (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); } catch (InvalidPathException $e) { // } } /* * Detect if a custom environment file matching the APP_ENV exists. * * @param \Illuminate\Contracts\Foundation\Application $app * @return void / protected function checkForSpecificEnvironmentFile($app) { if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption(’–env’)) { if ($this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.$input->getParameterOption(’–env’) )) { return; } } if (! env(‘APP_ENV’)) { return; } $this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.env(‘APP_ENV’) ); } /* * Load a custom environment file. * * @param \Illuminate\Contracts\Foundation\Application $app * @param string $file * @return bool */ protected function setEnvironmentFilePath($app, $file) { if (file_exists($app->environmentPath().’/’.$file)) { $app->loadEnvironmentFrom($file); return true; } return false; }}在他的启动方法bootstrap中,Laravel会检查配置是否缓存过以及判断应该应用那个env文件,针对上面说的根据环境加载配置文件的三种方法中的头两种,因为系统或者nginx环境变量中设置了APP_ENV,所以Laravel会在checkForSpecificEnvironmentFile方法里根据 APP_ENV的值设置正确的配置文件的具体路径, 比如.env.dev或者.env.test,而针对第三中情况则是默认的.env, 具体可以参看下面的checkForSpecificEnvironmentFile还有相关的Application里的两个方法的源码:protected function checkForSpecificEnvironmentFile($app){ if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption(’–env’)) { if ($this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.$input->getParameterOption(’–env’) )) { return; } } if (! env(‘APP_ENV’)) { return; } $this->setEnvironmentFilePath( $app, $app->environmentFile().’.’.env(‘APP_ENV’) );}namespace Illuminate\Foundation;class Application ….{ public function environmentPath() { return $this->environmentPath ?: $this->basePath; } public function environmentFile() { return $this->environmentFile ?: ‘.env’; }}判断好后要读取的配置文件的路径后,接下来就是加载env里的配置了。(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();Laravel使用的是Dotenv的PHP版本vlucas/phpdotenvclass Dotenv{ public function __construct($path, $file = ‘.env’) { $this->filePath = $this->getFilePath($path, $file); $this->loader = new Loader($this->filePath, true); } public function load() { return $this->loadData(); } protected function loadData($overload = false) { $this->loader = new Loader($this->filePath, !$overload); return $this->loader->load(); }}它依赖/Dotenv/Loader来加载数据:class Loader{ public function load() { $this->ensureFileIsReadable(); $filePath = $this->filePath; $lines = $this->readLinesFromFile($filePath); foreach ($lines as $line) { if (!$this->isComment($line) && $this->looksLikeSetter($line)) { $this->setEnvironmentVariable($line); } } return $lines; }}Loader读取配置时readLinesFromFile函数会用file函数将配置从文件中一行行地读取到数组中去,然后排除以#开头的注释,针对内容中包含=的行去调用setEnvironmentVariable方法去把文件行中的环境变量配置到项目中去:namespace Dotenv;class Loader{ public function setEnvironmentVariable($name, $value = null) { list($name, $value) = $this->normaliseEnvironmentVariable($name, $value); $this->variableNames[] = $name; // Don’t overwrite existing environment variables if we’re immutable // Ruby’s dotenv does this with ENV[key] ||= value. if ($this->immutable && $this->getEnvironmentVariable($name) !== null) { return; } // If PHP is running as an Apache module and an existing // Apache environment variable exists, overwrite it if (function_exists(‘apache_getenv’) && function_exists(‘apache_setenv’) && apache_getenv($name)) { apache_setenv($name, $value); } if (function_exists(‘putenv’)) { putenv("$name=$value"); } $_ENV[$name] = $value; $_SERVER[$name] = $value; } public function getEnvironmentVariable($name) { switch (true) { case array_key_exists($name, $_ENV): return $_ENV[$name]; case array_key_exists($name, $_SERVER): return $_SERVER[$name]; default: $value = getenv($name); return $value === false ? null : $value; // switch getenv default to null } }}Dotenv实例化Loader的时候把Loader对象的$immutable属性设置成了false,Loader设置变量的时候如果通过getEnvironmentVariable方法读取到了变量值,那么就会跳过该环境变量的设置。所以Dotenv默认情况下不会覆盖已经存在的环境变量,这个很关键,比如说在docker的容器编排文件里,我们会给PHP应用容器设置关于Mysql容器的两个环境变量 environment: - “DB_PORT=3306” - “DB_HOST=database"这样在容器里设置好环境变量后,即使env文件里的DB_HOST为homestead用env函数读取出来的也还是容器里之前设置的DB_HOST环境变量的值database(docker中容器链接默认使用服务名称,在编排文件中我把mysql容器的服务名称设置成了database, 所以php容器要通过database这个host来连接mysql容器)。因为用我们在持续集成中做自动化测试的时候通常都是在容器里进行测试,所以Dotenv不会覆盖已存在环境变量这个行为就相当重要这样我就可以只设置容器里环境变量的值完成测试而不用更改项目里的env文件,等到测试完成后直接去将项目部署到环境上就可以了。如果检查环境变量不存在那么接着Dotenv就会把环境变量通过PHP内建函数putenv设置到环境中去,同时也会存储到$_ENV和$_SERVER这两个全局变量中。在项目中读取env配置在Laravel应用程序中可以使用env()函数去读取环境变量的值,比如获取数据库的HOST:env(‘DB_HOST`, ’localhost’);传递给 env 函数的第二个值是「默认值」。如果给定的键不存在环境变量,则会使用该值。我们来看看env函数的源码:function env($key, $default = null){ $value = getenv($key); if ($value === false) { return value($default); } switch (strtolower($value)) { case ’true’: case ‘(true)’: return true; case ‘false’: case ‘(false)’: return false; case ’empty’: case ‘(empty)’: return ‘’; case ’null’: case ‘(null)’: return; } if (strlen($value) > 1 && Str::startsWith($value, ‘”’) && Str::endsWith($value, ‘"’)) { return substr($value, 1, -1); } return $value;}它直接通过PHP内建函数getenv读取环境变量。我们看到了在加载配置和读取配置的时候,使用了putenv和getenv两个函数。putenv设置的环境变量只在请求期间存活,请求结束后会恢复环境之前的设置。因为如果php.ini中的variables_order配置项成了 GPCS不包含E的话,那么php程序中是无法通过$_ENV读取环境变量的,所以使用putenv动态地设置环境变量让开发人员不用去关注服务器上的配置。而且在服务器上给运行用户配置的环境变量会共享给用户启动的所有进程,这就不能很好的保护比如DB_PASSWORD、API_KEY这种私密的环境变量,所以这种配置用putenv设置能更好的保护这些配置信息,getenv方法能获取到系统的环境变量和putenv动态设置的环境变量。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

October 23, 2018 · 3 min · jiezi

你可能需要了解下Laravel集合

前言集合通过 Illuminate\Database\Eloquent\Collection 进行实例,Laravel的内核大部分的参数传递都用到了集合,但这并不代表集合就是好的。Laravel作为快捷并优雅的开发框架,是有他一定的道理所在的,并非因他的路由、DB、监听器等等。当你需要处理一组数组时,你可能就需要它帮助你快捷的解决实际问题。创建集合$collection = collect([1, 2, 3]);显而易见,这是一部非常简单的操作,请打住你想说“这种操作很复杂”的话,它更类似与早起PHP5.x的版本的声明方式。$collection = array(1,2,3);laravel对于collection也没有做任何复杂的事情,会在下一章 《Laravel源码解析之集合》,谢谢打回原型如果你想将集合转换为数据,其使用方法也非常的简单collect([1, 2, 3])->all();——>[1, 2, 3]在不过与考虑性能的情况下,可以使用Laravel集合,毕竟它将帮你完成数组操作的百分之九十的工作。例如我们需要通过一个水平线切分数组,将其分为2个及以上的数组个数。使用集合可以酱紫做~$collection = collect([1, 2, 3, 4, 5, 6, 7]);$chunks = $collection->chunk(4);$chunks->toArray();// [[1, 2, 3, 4], [5, 6, 7]]并且有些还根据sql语句的查询方式来设计的方法,下面就让来看下具体都有哪些吧。方法列表这里列出一些常用的集合操作方法,具体及全部请操作官方。方法注释all将集合打回原型average & avg计算平均值chunk将集合拆成多个指定大小的小集合collapse将多个数组的集合合并成一个数组的集合combine可以将一个集合的值作为「键」,再将另一个数组或者集合的值作为「值」合并成一个集合concat将给定的数组或集合值附加到集合的末尾contains判断集合是否包含给定的项目count返回该集合内的项目总数dd打印集合的项目并结束脚本执行diff将集合与其它集合或纯 PHP 数组进行值的比较,然后返回原集合中存在而给定集合中不存在的值each迭代集合中的内容并将其传递到回调函数中filter使用给定的回调函数过滤集合的内容,只留下那些通过给定真实测试的内容first返回集合中通过给定真实测试的第一个元素groupBy根据给定的键对集合内的项目进行分组push把给定值添加到集合的末尾put在集合内设置给定的键值对sortBy通过给定的键对集合进行排序。排序后的集合保留了原数组键where通过给定的键值过滤集合致谢感谢你看到这里,希望本篇能够帮助到你。谢谢,还不抓紧去练习下集合?

October 22, 2018 · 1 min · jiezi

Laravel关联模型中过滤结果为空的结果集(has和with区别)

首先看代码:$userCoupons = UserCoupons::with([‘coupon’ => function($query) use($groupId){ return $query->select(‘id’, ‘group_id’, ‘cover’, ‘group_number’, ‘group_cover’)->where([ ‘group_id’ => $groupId, ]);}])// 更多查询省略…数据结构是三张表用户优惠券表(user_coupons)、优惠券表(coupons),商家表(corps),组优惠券表(group_coupons) (为了方便查看,后两项已去除)这里我本意想用模型关联查出用户优惠券中属于给定组gourpId的所有数据(如果为空该条数据就不返回)。但有些结果不是我想要的: array(20) { [“id”]=> int(6) [“user_id”]=> int(1) [“corp_id”]=> int(1) [“coupon_id”]=> int(4) [“obtain_time”]=> int(1539739569) [“receive_time”]=> int(1539739569) [“status”]=> int(1) [“expires_time”]=> int(1540603569) [“is_selling”]=> int(0) [“from_id”]=> int(0) [“sell_type”]=> int(0) [“sell_time”]=> int(0) [“sell_user_id”]=> int(0) [“is_compose”]=> int(0) [“group_cover”]=> string(0) "" [“is_delete”]=> int(0) [“score”]=> int(100) [“created_at”]=> NULL [“updated_at”]=> NULL [“coupon”]=> NULL // 注意返回了coupons为空的数据 }记录中有的coupon有记录,有的为空。想想也是,with只是用sql的in()实现的所谓预加载。无论怎样主user_coupons的数据都是会列出的。它会有两条sql查询,第一条查主数据,第二条查关联,这里第二条sql如下:select id, group_id, cover, group_number, group_cover from youquan_coupons where youquan_coupons.id in (1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 13, 14) and (group_id = 1) and youquan_coupons.deleted_at is null如果第二条为空,主记录的关联字段就是NULL。后来看到了Laravel关联的模型的has()方法,has()是基于存在的关联查询,下面我们用whereHas()(一样作用,只是更高级,方便写条件)这里我们思想是把判断有没有优惠券数据也放在第一次查询逻辑中,所以才能实现筛选空记录。加上whereHas()后的代码如下 $userCoupons = UserCoupons::whereHas(‘coupon’, function($query) use($groupId){ return $query->select(‘id’, ‘group_id’, ‘cover’, ‘group_number’, ‘group_cover’)->where([ ‘group_id’ => $groupId, ]); })->with([‘coupon’ => function($query) use($groupId){ return $query->select(‘id’, ‘group_id’, ‘cover’, ‘group_number’, ‘group_cover’); }])-> // …看下最终的SQL:select * from youquan_user_coupons where exists (select id, group_id, cover, group_number, group_cover from youquan_coupons where youquan_user_coupons.coupon_id = youquan_coupons.id and (group_ids = 1) and youquan_coupons.deleted_at is null) and (status = 1 and user_id = 1)这里实际上是用exists()筛选存在的记录。然后走下一步的with()查询,因为此时都筛选一遍了,所以with可以去掉条件。显然区分这两个的作用很重要,尤其是在列表中,不用特意去筛选为空的数据,而且好做分页。本文最早发布在 Rootrl’s blog ...

October 18, 2018 · 1 min · jiezi

Laravel修炼:服务容器绑定与解析

前言 老实说,第一次老大让我看laravel框架手册的那天早上,我是很绝望的,因为真的没接触过,对我这种渣渣来说,laravel的入门门槛确实有点高了,但还是得硬着头皮看下去(虽然到现在我还有很多没看懂,也没用过)。 后面慢慢根据公司项目的代码对laravel也慢慢熟悉起来了,但还是停留在一些表面的功能,例如依赖注入,ORM操作,用户认证这些和我项目业务逻辑相关的操作,然后对于一些架构基础的,例如服务提供器,服务容器,中间件,Redis等这些一开始就要设置好的东西,我倒是没实际操作过(因为老大一开始就做好了),所以看手册还是有点懵。 所以有空的时候逛逛论坛,搜下Google就发现许多关于laravel核心架构的介绍,以及如何使用的网站(确实看完后再去看手册就好理解多了),下面就根据一个我觉得不错的网站上面的教学来记录一下laravel核心架构的学习网站地址:https://laraweb.net/ 这是一个日本的网站,我觉得挺适合新手的,内容用浏览器翻译过来就ok了,毕竟日文直翻过来很好理解的关于服务容器 手册上是这样介绍的:Laravel 服务容器是用于管理类的依赖和执行依赖注入的工具。依赖注入这个花俏名词实质上是指:类的依赖项通过构造函数,或者某些情况下通过「setter」方法「注入」到类中。。。。。。(真的看不懂啥意思) 服务容器是用于管理类(服务)的实例化的机制。直接看看服务容器怎么用 1.在服务容器中注册类(bind)$this->app->bind(‘sender’,‘MailSender’);//$this->app成为服务容器。 2.从服务容器生成类(make)$sender = $this->app->make(‘sender’);//从服务容器($this->app)创建一个sender类。在这种情况下,将返回MailSender的实例。 这是服务容器最简单的使用,下面是对服务容器的详细介绍(主要参考:https://www.cnblogs.com/lyzg/…)laravel容器基本认识 一开始,index.php 文件加载 Composer 生成定义的自动加载器,然后从 bootstrap/app.php 脚本中检索 Laravel 应用程序的实例。Laravel 本身采取的第一个动作是创建一个 application/ service container 的实例。$app = new Illuminate\Foundation\Application( dirname(DIR)); 这个文件在每一次请求到达laravel框架都会执行,所创建的$app即是laravel框架的应用程序实例,它在整个请求生命周期都是唯一的。laravel提供了很多服务,包括认证,数据库,缓存,消息队列等等,$app作为一个容器管理工具,负责几乎所有服务组件的实例化以及实例的生命周期管理。当需要一个服务类来完成某个功能的时候,仅需要通过容器解析出该类型的一个实例即可。从最终的使用方式来看,laravel容器对服务实例的管理主要包括以下几个方面:服务的绑定与解析服务提供者的管理别名的作用依赖注入先了解如何在代码中获取到容器实例,再学习上面四个关键如何在代码中获取到容器实例第一种是$app = app();//app这个辅助函数定义在\vendor\laravel\framework\src\Illuminate\Foundation\helper.php里面,,这个文件定义了很多help函数,并且会通过composer自动加载到项目中。所以,在参与http请求处理的任何代码位置都能够访问其中的函数,比如app()。第二种是Route::get(’/’, function () { dd(App::basePath()); return ‘’;});//这个其实是用到Facade,中文直译貌似叫门面,在config/app.php中,有一节数组aliases专门用来配置一些类型的别名,第一个就是’App’ => Illuminate\Support\Facades\App::class,具体的Google一下laravel有关门面的具体实现方式第三种是 在服务提供者里面直接使用$this->app。服务提供者后面还会介绍,现在只是引入。因为服务提供者类都是由laravel容器实例化的,这些类都继承自Illuminate\Support\ServiceProvider,它定义了一个实例属性$app:abstract class ServiceProvider{ protected $app; laravel在实例化服务提供者的时候,会把laravel容器实例注入到这个$app上面。所以我们在服务提供者里面,始终能通过$this->$app访问到laravel容器实例,而不需要再使用app()函数或者App Facade了。如何理解服务绑定与解析 浅义层面理解,容器既然用来存储对象,那么就要有一个对象存入跟对象取出的过程。这个对象存入跟对象取出的过程在laravel里面称为服务的绑定与解析。app()->bind(‘service’, ’this is service1’);app()->bind(‘service2’, [ ‘hi’ => function(){ //say hi }]);class Service {}app()->bind(‘service3’, function(){ return new Service();}); 还有一个单例绑定singleton,是bind的一种特殊情况(第三个参数为true),绑定到容器的对象只会被解析一次,之后的调用都返回相同的实例public function singleton($abstract, $concrete = null){$this->bind($abstract, $concrete, true);} 在绑定的时候,我们可以直接绑定已经初始化好的数据(基本类型、数组、对象实例),还可以用匿名函数来绑定。用匿名函数的好处在于,这个服务绑定到容器以后,并不会立即产生服务最终的对象,只有在这个服务解析的时候,匿名函数才会执行,此时才会产生这个服务对应的服务实例。 实际上,当我们使用singleton,bind方法以及数组形式,(这三个方法是后面要介绍的绑定的方法),进行服务绑定的时候,如果绑定的服务形式,不是一个匿名函数,也会在laravel内部用一个匿名函数包装起来,这样的话, 不轮绑定什么内容,都能做到前面介绍的懒初始化的功能,这对于容器的性能是有好处的。这个可以从bind的源码中看到一些细节:if (! $concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete);}看看bind的底层代码public function bind($abstract, $concrete = null, $shared = false) 第一个参数服务绑定名称,第二个参数服务绑定的结果(也就是闭包,得到实例),第三个参数就表示这个服务是否在多次解析的时候,始终返回第一次解析出的实例(也就是单例绑定singleton)。 服务绑定还可以通过数组的方式:app()[‘service’] = function(){ return new Service();};绑定大概就这些,接下来看解析,也就是取出来用$service= app()->make(‘service’); 这个方法接收两个参数,第一个是服务的绑定名称和服务绑定名称的别名,如果是别名,那么就会根据服务绑定名称的别名配置,找到最终的服务绑定名称,然后进行解析;第二个参数是一个数组,最终会传递给服务绑定产生的闭包。看源码:/** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed /public function make($abstract, array $parameters = []){ return $this->resolve($abstract, $parameters);}/* * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed /protected function resolve($abstract, $parameters = []){ $abstract = $this->getAlias($abstract); $needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract) ); // If an instance of the type is currently being managed as a singleton we’ll // just return an existing instance instead of instantiating new instances // so the developer can keep using the same objects instance every time. if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; } $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); // We’re ready to instantiate an instance of the concrete type registered for // the binding. This will instantiate the types, as well as resolve any of // its “nested” dependencies recursively until all have gotten resolved. if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); } // If we defined any extenders for this type, we’ll need to spin through them // and apply them to the object being built. This allows for the extension // of services, such as changing configuration or decorating the object. foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } // If the requested type is registered as a singleton we’ll want to cache off // the instances in “memory” so we can return it later without creating an // entirely new instance of an object on each subsequent request for it. if ($this->isShared($abstract) && ! $needsContextualBuild) { $this->instances[$abstract] = $object; } $this->fireResolvingCallbacks($abstract, $object); // Before returning, we will also set the resolved flag to “true” and pop off // the parameter overrides for this build. After those two things are done // we will be ready to return back the fully constructed class instance. $this->resolved[$abstract] = true; array_pop($this->with); return $object;}第一步:$needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract)); 该方法主要是区分,解析的对象是否有参数,如果有参数,还需要对参数做进一步的分析,因为传入的参数,也可能是依赖注入的,所以还需要对传入的参数进行解析;这个后面再分析。第二步:if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract];} 如果是绑定的单例,并且不需要上面的参数依赖。我们就可以直接返回 $this->instances[$abstract]。第三步:$concrete = $this->getConcrete($abstract);…/* * Get the concrete type for a given abstract. * * @param string $abstract * @return mixed $concrete /protected function getConcrete($abstract){ if (! is_null($concrete = $this->getContextualConcrete($abstract))) { return $concrete; } // If we don’t have a registered resolver or concrete for the type, we’ll just // assume each type is a concrete name and will attempt to resolve it as is // since the container should be able to resolve concretes automatically. if (isset($this->bindings[$abstract])) { return $this->bindings[$abstract][‘concrete’]; } return $abstract;} 这一步主要是先从绑定的上下文找,是不是可以找到绑定类;如果没有,则再从 $bindings[] 中找关联的实现类;最后还没有找到的话,就直接返回 $abstract 本身。// We’re ready to instantiate an instance of the concrete type registered for// the binding. This will instantiate the types, as well as resolve any of// its “nested” dependencies recursively until all have gotten resolved.if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete);} else { $object = $this->make($concrete);}…/* * Determine if the given concrete is buildable. * * @param mixed $concrete * @param string $abstract * @return bool /protected function isBuildable($concrete, $abstract){ return $concrete === $abstract || $concrete instanceof Closure;} 如果之前找到的 $concrete 返回的是 $abstract 值,或者 $concrete 是个闭包,则执行 $this->build($concrete),否则,表示存在嵌套依赖的情况,则采用递归的方法执行 $this->make($concrete),直到所有的都解析完为止。$this->build($concrete)/* * Instantiate a concrete instance of the given type. * * @param string $concrete * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */public function build($concrete){ // If the concrete type is actually a Closure, we will just execute it and // hand back the results of the functions, which allows functions to be // used as resolvers for more fine-tuned resolution of these objects. // 如果传入的是闭包,则直接执行闭包函数,返回结果 if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); } // 利用反射机制,解析该类。 $reflector = new ReflectionClass($concrete); // If the type is not instantiable, the developer is attempting to resolve // an abstract type such as an Interface of Abstract Class and there is // no binding registered for the abstractions so we need to bail out. if (! $reflector->isInstantiable()) { return $this->notInstantiable($concrete); } $this->buildStack[] = $concrete; // 获取构造函数 $constructor = $reflector->getConstructor(); // If there are no constructors, that means there are no dependencies then // we can just resolve the instances of the objects right away, without // resolving any other types or dependencies out of these containers. // 如果没有构造函数,则表明没有传入参数,也就意味着不需要做对应的上下文依赖解析。 if (is_null($constructor)) { // 将 build 过程的内容 pop,然后直接构造对象输出。 array_pop($this->buildStack); return new $concrete; } // 获取构造函数的参数 $dependencies = $constructor->getParameters(); // Once we have all the constructor’s parameters we can create each of the // dependency instances and then use the reflection instances to make a // new instance of this class, injecting the created dependencies in. // 解析出所有上下文依赖对象,带入函数,构造对象输出 $instances = $this->resolveDependencies( $dependencies ); array_pop($this->buildStack); return $reflector->newInstanceArgs($instances);}上面这一段有关解析make的介绍主要参考:coding01:看 Laravel 源代码了解 Container 这一篇就主要学习laravel的服务容器以及它的绑定和解析,虽然目前能力无法对框架源码每一个地方都弄懂,但通过这几篇优秀的文章,我将其进行整理结合,这过程让我更加理解laravel的一些核心内容,起码别人问起来我多多少少能说出一些,这就是进步。 后面有关服务提供者,依赖注入,中间件等内容的学习将放在后续的博客文章中,欢迎看看我的其他博客文章:https://zgxxx.github.io/。 以上相关知识的引用已经注明出处,若有侵权,请联系我,感谢这些优秀文章的作者 ...

October 15, 2018 · 5 min · jiezi

扒一扒 EventServiceProvider 源代码

有了之前的《简述 Laravel Model Events 的使用》https://mp.weixin.qq.com/s/XrhDq1S5RC9UdeULVVksoA,大致了解了 Event 的使用。今天我们就来扒一扒 Event 的源码。开始之前,需要说下两个 EventServiceProvider 的区别:App\Providers\EventServiceProviderIlluminate\Events\EventServiceProvider第一个 App\Providers\EventServiceProvider 主要是定义 event 和 listener 的关联;第二个 Illuminate\Events\EventServiceProvider 是 Laravel 的三大基础 ServiceProvider 之一,主要负责「分派」工作。好了,下面开始具体的分析工作。App\Providers\EventServiceProvider主要是定义 event 和 listener 的关联,如:<?phpnamespace App\Providers;use Illuminate\Support\Facades\Event;use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;class EventServiceProvider extends ServiceProvider{ /** * The event listener mappings for the application. * * @var array / protected $listen = [ ‘App\Events\RssPublicEvent’ => [ ‘App\Listeners\RssPublicListener1’, ], ‘App\Events*Event’ => [ ‘App\Listeners\RssPublicListener2’, ‘App\Listeners\RssPublicListener3’, ], ‘Illuminate\Contracts\Broadcasting\ShouldBroadcast’ => [ ‘App\Listeners\RssPublicListener4’, ‘App\Listeners\RssPublicListener5’, ], ]; /* * Register any events for your application. * * @return void / public function boot() { parent::boot(); }}主要继承 Illuminate\Foundation\Support\Providers\EventServiceProvider:<?phpnamespace Illuminate\Foundation\Support\Providers;use Illuminate\Support\Facades\Event;use Illuminate\Support\ServiceProvider;class EventServiceProvider extends ServiceProvider{ /* * The event handler mappings for the application. * * @var array / protected $listen = []; /* * The subscriber classes to register. * * @var array / protected $subscribe = []; /* * Register the application’s event listeners. * * @return void / public function boot() { foreach ($this->listens() as $event => $listeners) { foreach ($listeners as $listener) { Event::listen($event, $listener); } } foreach ($this->subscribe as $subscriber) { Event::subscribe($subscriber); } } /* * {@inheritdoc} / public function register() { // } /* * Get the events and handlers. * * @return array / public function listens() { return $this->listen; }}把定义 event 和 listener 的关联交给用户自己去做,然后父类 EventServiceProvider 只是做关联工作,在 boot() 中:public function boot(){ foreach ($this->listens() as $event => $listeners) { foreach ($listeners as $listener) { Event::listen($event, $listener); } } foreach ($this->subscribe as $subscriber) { Event::subscribe($subscriber); }}这里主要看两个函数:Event::listen($event, $listener);Event::subscribe($subscriber);就这么简单,我们说完了第一个 EventServiceProvider ,我们开始第二个。Illuminate\Events\EventServiceProvider看过之前文章的知道,Event 有个全局函数:Artisan::command(‘public_echo’, function () { event(new RssPublicEvent());})->describe(’echo demo’);…if (! function_exists(’event’)) { /* * Dispatch an event and call the listeners. * * @param string|object $event * @param mixed $payload * @param bool $halt * @return array|null / function event(…$args) { return app(’events’)->dispatch(…$args); }}而 Illuminate\Events\EventServiceProvider,是 Laravel 三个基础 ServiceProvider 之一:/* * Register all of the base service providers. * * @return void /protected function registerBaseServiceProviders(){ $this->register(new EventServiceProvider($this)); $this->register(new LogServiceProvider($this)); $this->register(new RoutingServiceProvider($this));}我们接着看 Illuminate\Events\EventServiceProvider:<?phpnamespace Illuminate\Events;use Illuminate\Support\ServiceProvider;use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;class EventServiceProvider extends ServiceProvider{ /* * Register the service provider. * * @return void / public function register() { $this->app->singleton(’events’, function ($app) { return (new Dispatcher($app))->setQueueResolver(function () use ($app) { return $app->make(QueueFactoryContract::class); }); }); }}它注册了单例形式,并创建和返回 Dispatcher 对象:use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;use Illuminate\Contracts\Broadcasting\Factory as BroadcastFactory;use Illuminate\Contracts\Container\Container as ContainerContract;class Dispatcher implements DispatcherContract{ /* * The IoC container instance. * * @var \Illuminate\Contracts\Container\Container / protected $container; /* * The registered event listeners. * * @var array / protected $listeners = []; /* * The wildcard listeners. * * @var array / protected $wildcards = []; /* * The queue resolver instance. * * @var callable / protected $queueResolver; …} 主要实现 Dispatcher 接口:<?phpnamespace Illuminate\Contracts\Events;interface Dispatcher{ /* * Register an event listener with the dispatcher. * * @param string|array $events * @param mixed $listener * @return void / public function listen($events, $listener); /* * Determine if a given event has listeners. * * @param string $eventName * @return bool / public function hasListeners($eventName); /* * Register an event subscriber with the dispatcher. * * @param object|string $subscriber * @return void / public function subscribe($subscriber); /* * Dispatch an event until the first non-null response is returned. * * @param string|object $event * @param mixed $payload * @return array|null / public function until($event, $payload = []); /* * Dispatch an event and call the listeners. * * @param string|object $event * @param mixed $payload * @param bool $halt * @return array|null / public function dispatch($event, $payload = [], $halt = false); /* * Register an event and payload to be fired later. * * @param string $event * @param array $payload * @return void / public function push($event, $payload = []); /* * Flush a set of pushed events. * * @param string $event * @return void / public function flush($event); /* * Remove a set of listeners from the dispatcher. * * @param string $event * @return void / public function forget($event); /* * Forget all of the queued listeners. * * @return void / public function forgetPushed();}下面我们来解说每一个函数。listen()Register an event listener with the dispatcher.public function listen($events, $listener){ foreach ((array) $events as $event) { if (Str::contains($event, ‘’)) { $this->wildcards[$event][] = $this->makeListener($listener, true); } else { $this->listeners[$event][] = $this->makeListener($listener); } }}这就好理解了,把通配符的放在 wildcards 数组中,另一个放在 listeners 数组中。接下来看函数 makeListener()。public function makeListener($listener, $wildcard = false){ if (is_string($listener)) { return $this->createClassListener($listener, $wildcard); } return function ($event, $payload) use ($listener, $wildcard) { if ($wildcard) { return $listener($event, $payload); } return $listener(…array_values($payload)); };}如果传入的 $listener 为字符串,则执行函数 createClassListener:public function createClassListener($listener, $wildcard = false){ return function ($event, $payload) use ($listener, $wildcard) { if ($wildcard) { return call_user_func($this->createClassCallable($listener), $event, $payload); } return call_user_func_array( $this->createClassCallable($listener), $payload ); };}先来看看函数 createClassCallable():protected function createClassCallable($listener){ list($class, $method) = Str::parseCallback($listener, ‘handle’); if ($this->handlerShouldBeQueued($class)) { return $this->createQueuedHandlerCallable($class, $method); } return [$this->container->make($class), $method];}第一个函数还是很好理解:public static function parseCallback($callback, $default = null){ return static::contains($callback, ‘@’) ? explode(’@’, $callback, 2) : [$callback, $default];}就看传入的 listener 是不是 class@method 结构,如果是就用 @ 分割,否则就默认的就是 class 类名,然后 method 就是默认的 handle 函数 —— 这也是我们创建 Listener 类提供的做法。接着就看是否可以放入队列中:protected function handlerShouldBeQueued($class){ try { return (new ReflectionClass($class))->implementsInterface( ShouldQueue::class ); } catch (Exception $e) { return false; }}也就判断该 listener 类是否实现了接口类 ShouldQueue。如果实现了,则可以将该类放入队列中 (返回闭包函数):protected function createQueuedHandlerCallable($class, $method){ return function () use ($class, $method) { $arguments = array_map(function ($a) { return is_object($a) ? clone $a : $a; }, func_get_args()); if ($this->handlerWantsToBeQueued($class, $arguments)) { $this->queueHandler($class, $method, $arguments); } };}我们接着看 handlerWantsToBeQueued:protected function handlerWantsToBeQueued($class, $arguments){ if (method_exists($class, ‘shouldQueue’)) { return $this->container->make($class)->shouldQueue($arguments[0]); } return true;}所以说,如果在 listener 类中写了 shouldQueue 方法,则就看该方法是不是返回 true 或者 false 来决定是否放入队列中:protected function queueHandler($class, $method, $arguments){ list($listener, $job) = $this->createListenerAndJob($class, $method, $arguments); $connection = $this->resolveQueue()->connection( $listener->connection ?? null ); $queue = $listener->queue ?? null; isset($listener->delay) ? $connection->laterOn($queue, $listener->delay, $job) : $connection->pushOn($queue, $job);}注:和队列相关的放在之后再做分析,此处省略好了,回到开始的地方:// createClassCallable($listener)return [$this->container->make($class), $method];到此,也就明白了,如果是 通配符 的,则对应的执行函数 (默认的为: handle) 传入的参数有两个:$event 事件对象和 $payload;否则对应执行函数,传入的参数就只有一个了:$payload。同理,如果传入的 listener 是个函数的话,返回的闭包就这样的:return function ($event, $payload) use ($listener, $wildcard) { if ($wildcard) { return $listener($event, $payload); } return $listener(…array_values($payload));};整个流程就通了,listener 函数的作用就是:在 Dispatcher 中的 $listeners 和 $wildcards 的数组中,存储 [’event’ => Callback] 的结构数组,以供执行使用。说完了第一个函数 Event::listen(),第二个函数了:Event::subscribe(),留着之后再说。好了,整个 event 和 listener 就关联在一起了。接下来就开始看执行方法了。dispatch()Dispatch an event and call the listeners.正如上文的 helpers 定义的,所有 Event 都是通过该函数进行「分发」事件和调用所关联的 listeners:/** * Fire an event and call the listeners. * * @param string|object $event * @param mixed $payload * @param bool $halt * @return array|null /public function dispatch($event, $payload = [], $halt = false){ // When the given “event” is actually an object we will assume it is an event // object and use the class as the event name and this event itself as the // payload to the handler, which makes object based events quite simple. list($event, $payload) = $this->parseEventAndPayload( $event, $payload ); if ($this->shouldBroadcast($payload)) { $this->broadcastEvent($payload[0]); } $responses = []; foreach ($this->getListeners($event) as $listener) { $response = $listener($event, $payload); // If a response is returned from the listener and event halting is enabled // we will just return this response, and not call the rest of the event // listeners. Otherwise we will add the response on the response list. if ($halt && ! is_null($response)) { return $response; } // If a boolean false is returned from a listener, we will stop propagating // the event to any further listeners down in the chain, else we keep on // looping through the listeners and firing every one in our sequence. if ($response === false) { break; } $responses[] = $response; } return $halt ? null : $responses;}先理解注释的函数 parseEventAndPayload():When the given “event” is actually an object we will assume it is an event object and use the class as the event name and this event itself as the payload to the handler, which makes object based events quite simple.protected function parseEventAndPayload($event, $payload){ if (is_object($event)) { list($payload, $event) = [[$event], get_class($event)]; } return [$event, Arr::wrap($payload)];}如果 $event 是个对象,则将 $event 的类名作为事件的名称,并将该事件 [$event] 作为 $payload。接着判断 $payload 是否可以「广播」出去,如果可以,那就直接广播出去:protected function shouldBroadcast(array $payload){ return isset($payload[0]) && $payload[0] instanceof ShouldBroadcast && $this->broadcastWhen($payload[0]);}就拿上文的例子来说吧:<?phpnamespace App\Events;use Carbon\Carbon;use Illuminate\Broadcasting\Channel;use Illuminate\Queue\SerializesModels;use Illuminate\Broadcasting\PrivateChannel;use Illuminate\Broadcasting\PresenceChannel;use Illuminate\Foundation\Events\Dispatchable;use Illuminate\Broadcasting\InteractsWithSockets;use Illuminate\Contracts\Broadcasting\ShouldBroadcast;class RssPublicEvent implements ShouldBroadcast{ use Dispatchable, InteractsWithSockets, SerializesModels; /* * Create a new event instance. * * @return void / public function __construct() { // } /* * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array / public function broadcastOn() { return new Channel(‘public_channel’); } /* * 指定广播数据。 * * @return array / public function broadcastWith() { // 返回当前时间 return [’name’ => ‘public_channel_’.Carbon::now()->toDateTimeString()]; }}首先它实现接口 ShouldBroadcast,然后看是不是还有额外的条件来决定是否可以广播:/* * Check if event should be broadcasted by condition. * * @param mixed $event * @return bool /protected function broadcastWhen($event){ return method_exists($event, ‘broadcastWhen’) ? $event->broadcastWhen() : true;}由于本实例没有实现 broadcastWhen 方法,所以返回默认值 true。所以可以将本实例广播出去:/* * Broadcast the given event class. * * @param \Illuminate\Contracts\Broadcasting\ShouldBroadcast $event * @return void /protected function broadcastEvent($event){ $this->container->make(BroadcastFactory::class)->queue($event);}这就交给 BroadcastManager 来处理了,此文不再继续深挖。注:下篇文章我们再来扒一扒 BroadcastManager 源码当把事件广播出去后,我们就开始执行该事件的各个监听了。通过之前的文章知道,一个 Event,不止一个 Listener 监听,所以需要通过一个 foreach 循环来遍历执行 Listener,首先获取这些 Listener:/* * Get all of the listeners for a given event name. * * @param string $eventName * @return array /public function getListeners($eventName){ $listeners = $this->listeners[$eventName] ?? []; $listeners = array_merge( $listeners, $this->getWildcardListeners($eventName) ); return class_exists($eventName, false) ? $this->addInterfaceListeners($eventName, $listeners) : $listeners;}该方法主要通过三种方式累加获取所有 listeners:该类中的属性:$listeners 和 $wildcards,以及如果该 $event 是个对象的,还包括该类的所有接口关联的 listeners 数组。protected function addInterfaceListeners($eventName, array $listeners = []){ foreach (class_implements($eventName) as $interface) { if (isset($this->listeners[$interface])) { foreach ($this->listeners[$interface] as $names) { $listeners = array_merge($listeners, (array) $names); } } } return $listeners;}注:class_implements — 返回指定的类实现的所有接口。接下来就是执行每个 listener 了:$response = $listener($event, $payload);// If a response is returned from the listener and event halting is enabled// we will just return this response, and not call the rest of the event// listeners. Otherwise we will add the response on the response list.if ($halt && ! is_null($response)) { return $response;}// If a boolean false is returned from a listener, we will stop propagating// the event to any further listeners down in the chain, else we keep on// looping through the listeners and firing every one in our sequence.if ($response === false) { break;}$responses[] = $response;由上文可以知道 $listener,实际上就是一个闭包函数,最终的结果相当于执行 handle 函数:public function handle(RssPublicEvent $event){ info(’listener 1’);}…public function handle(RssPublicEvent $event, array $payload){ info(’listener 2’);}写个 demo我们写个 demo,在 EventServiceProvider 的 listen 数组,填入这三种方式的关联情况:protected $listen = [ ‘App\Events\RssPublicEvent’ => [ ‘App\Listeners\RssPublicListener1’, ], ‘App\Events*Event’ => [ ‘App\Listeners\RssPublicListener2’, ‘App\Listeners\RssPublicListener3’, ], ‘Illuminate\Contracts\Broadcasting\ShouldBroadcast’ => [ ‘App\Listeners\RssPublicListener4’, ‘App\Listeners\RssPublicListener5’, ],];然后在每个 RssPublicListener 的 handle 方法输出对应的值,最后运行 php artisan public_echo,看结果:[2018-10-06 20:05:57] local.INFO: listener 1 [2018-10-06 20:05:58] local.INFO: listener 2 [2018-10-06 20:05:59] local.INFO: listener 3 [2018-10-06 20:05:59] local.INFO: listener 4 [2018-10-06 20:06:00] local.INFO: listener 5其他函数说完了执行函数,基本上也就说完了整个 Event 事件流程了。剩下的只有一些附属函数,一看基本都理解:/** * Register an event and payload to be fired later. * * @param string $event * @param array $payload * @return void /public function push($event, $payload = []){ $this->listen($event.’_pushed’, function () use ($event, $payload) { $this->dispatch($event, $payload); });}/* * Flush a set of pushed events. * * @param string $event * @return void /public function flush($event){ $this->dispatch($event.’_pushed’);}/* * Determine if a given event has listeners. * * @param string $eventName * @return bool /public function hasListeners($eventName){ return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);}/* * Fire an event until the first non-null response is returned. * * @param string|object $event * @param mixed $payload * @return array|null /public function until($event, $payload = []){ return $this->dispatch($event, $payload, true);}/* * Remove a set of listeners from the dispatcher. * * @param string $event * @return void /public function forget($event){ if (Str::contains($event, ‘’)) { unset($this->wildcards[$event]); } else { unset($this->listeners[$event]); }}/** * Forget all of the pushed listeners. * * @return void */public function forgetPushed(){ foreach ($this->listeners as $key => $value) { if (Str::endsWith($key, ‘_pushed’)) { $this->forget($key); } }}总结对 Event 做了比较详细的梳理,大致了解了它的整个流程,下一步就是看看怎么和队列结合在一起,和利用「观察者模式」的那部分代码逻辑了。 ...

October 11, 2018 · 9 min · jiezi

Laravel源码解析之Model

前言提前预祝猿人们国庆快乐,吃好、喝好、玩好,我会在电视上看着你们。根据单一责任开发原则来讲,在laravel的开发过程中每个表都应建立一个model对外服务和调用。类似于这样namespace App\Models; use Illuminate\Database\Eloquent\Model; class User extends Model{ protected $table = ‘users’;}解析Laravel的数据操作分两种DB facadeEloquent ORM它们除了有各自的特色外,基本的数据操作都是通过 Illuminate\Database\Query\Builder 调用方法去完成整个SQL。你也可以帮Builder这个类作为整个SQL操作的基类。这个类涵盖了以下的操作方法(部分展示)方法 public function select($columns = [’*’]) public function selectSub($query, $as) public function selectRaw($expression, array $bindings = []) public function fromSub($query, $as) public function fromRaw($expression, $bindings = []) public function addSelect($column) public function distinct() public function from($table) public function join($table, $first, $operator = null, $second = null, $type = ‘inner’, $where = false) public function joinWhere($table, $first, $operator, $second, $type = ‘inner’) public function joinSub($query, $as, $first, $operator = null, $second = null, $type = ‘inner’, $where = false) public function leftJoin($table, $first, $operator = null, $second = null) public function where($column, $operator = null, $value = null, $boolean = ‘and’) public function orWhere($column, $operator = null, $value = null) public function whereRaw($sql, $bindings = [], $boolean = ‘and’) public function whereIn($column, $values, $boolean = ‘and’, $not = false) public function orWhereIn($column, $values) 可见有很多方法在中国laravel站或者官方文档上都没有体现,所以说就算要精通一款框架,不去看它的源码也是不行的。这个文件在你项目目录中的 vendor/laravel/framework/src/Illuminate/Database/Query 下,你可以自行去查看。DB facade正常情况下你可能会这样写一个操作DB::table(‘user’)->get();这个操作首先经过laravel的门面指向文件,不过它并不在 app.php 中,而是通过内核直接加载,它在Illuminate\Foundation\Application -> registerCoreContainerAliases()被注册。门面直接调用 Illuminate\Database\DatabaseManager 类。public function registerCoreContainerAliases(){ foreach ([ … ’encrypter’ => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], ‘db’ => [\Illuminate\Database\DatabaseManager::class], ‘db.connection’ => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], ’events’ => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], ‘files’ => [\Illuminate\Filesystem\Filesystem::class], …. )} Illuminate\Database\DatabaseManager 内并没有太多的代码,大多都是处理数据库链接。当你使用 DB::table()时,会通过public function __call($method, $parameters){ return $this->connection()->$method(…$parameters);}转发,调用的是 Illuminate\Database\Connection ,用户处理 table() 方法,随后会通过 table() 方法指向 Illuminate\Database\Query 类,开头我们讲过这个类了,这里就不多说了,随后就是各种sql的拼接->执行sql->结束战斗Eloquent ORMEloquent ORM 与DB facade 类似,首先每个 Eloquent ORM 都需要继承父类 Illuminate\Database\Eloquent\Model 你大概会这样写User::find(1)父类是不存在这个方法的,它会通过public static function __callStatic($method, $parameters){ return (new static)->$method(…$parameters);}去转发请求调用。同理User::get()则是通过public function __call($method, $parameters){ if (in_array($method, [‘increment’, ‘decrement’])) { return $this->$method(…$parameters); } return $this->newQuery()->$method(…$parameters);}去调用,这个方法最终以 new Builder() 而告终,public function newEloquentBuilder($query){ return new Builder($query);}最后我们到了 Illuminate\Database\Eloquent\Builder 文件下,这个类中涵盖了ORM的基本操作,例如find , findOrFail 等等。如果你在代码用到了get方法,抱歉,这里没有,它依旧会通过__call 方法将你的请求转发到 Illuminate\Database\Query\Builder 类中$this->query->{$method}(…$parameters);至此就完成了整个数据操作。致谢感谢你看到这里,希望本篇文章可以帮助到你,谢谢 ...

September 30, 2018 · 2 min · jiezi

Laravel源码解析之路由的使用

前言我的解析文章并非深层次多领域的解析攻略。但是参考着开发文档看此类文章会让你在日常开发中更上一层楼。废话不多说,我们开始本章的讲解。入口Laravel启动后,会先加载服务提供者、中间件等组件,在查找路由之前因为我们使用的是门面,所以先要查到Route的实体类。注册第一步当然还是通过服务提供者,因为这是laravel启动的关键,在 RouteServiceProvider 内加载路由文件。protected function mapApiRoutes(){ Route::prefix(‘api’) ->middleware(‘api’) ->namespace($this->namespace) // 设置所处命名空间 ->group(base_path(‘routes/api.php’)); //所得路由文件绝对路径}首先require是不可缺少的。因路由文件中没有命名空间。 Illuminate\Routing\Router 下方法protected function loadRoutes($routes){ if ($routes instanceof Closure) { $routes($this); } else { $router = $this; require $routes; }}随后通过路由找到指定方法,依旧是 Illuminate\Routing\Router 内有你所使用的所有路由相关方法,例如get、post、put、patch等等,他们都调用了统一的方法 addRoute public function addRoute($methods, $uri, $action){ return $this->routes->add($this->createRoute($methods, $uri, $action));}之后通过 Illuminate\Routing\RouteCollection addToCollections 方法添加到集合中protected function addToCollections($route){ $domainAndUri = $route->getDomain().$route->uri(); foreach ($route->methods() as $method) { $this->routes[$method][$domainAndUri] = $route; } $this->allRoutes[$method.$domainAndUri] = $route;}添加后的结果如下图所示调用通过 Illuminate\Routing\Router 方法开始运行路由实例化的逻辑protected function runRoute(Request $request, Route $route){ $request->setRouteResolver(function () use ($route) { return $route; }); $this->events->dispatch(new Events\RouteMatched($route, $request)); return $this->prepareResponse($request, $this->runRouteWithinStack($route, $request) );}….protected function runRouteWithinStack(Route $route, Request $request){ $shouldSkipMiddleware = $this->container->bound(‘middleware.disable’) && $this->container->make(‘middleware.disable’) === true; $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); return (new Pipeline($this->container)) ->send($request) ->through($middleware) ->then(function ($request) use ($route) { return $this->prepareResponse( $request, $route->run() // 此处调用run方法 ); });}在 Illuminate\Routing\Route 下 run 方用于执行控制器的方法public function run(){ $this->container = $this->container ?: new Container; try { if ($this->isControllerAction()) { return $this->runController(); //运行一个路由并作出响应 } return $this->runCallable(); } catch (HttpResponseException $e) { return $e->getResponse(); }}从上述方法内可以看出 runController 是运行路由的关键,方法内运行了一个调度程序,将控制器 $this->getController() 和控制器方法 $this->getControllerMethod() 传入到 dispatch 调度方法内protected function runController(){ return $this->controllerDispatcher()->dispatch( $this, $this->getController(), $this->getControllerMethod() );}这里注意 getController() 才是真正的将控制器实例化的方法public function getController(){ if (! $this->controller) { $class = $this->parseControllerCallback()[0]; // 0=>控制器 xxController 1=>方法名 index $this->controller = $this->container->make(ltrim($class, ‘\’)); // 交给容器进行反射 } return $this->controller;}实例化依旧通过反射加载路由指定的控制器,这个时候build的参数$concrete = App\Api\Controllers\XxxController public function build($concrete){ // If the concrete type is actually a Closure, we will just execute it and // hand back the results of the functions, which allows functions to be // used as resolvers for more fine-tuned resolution of these objects. if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); } $reflector = new ReflectionClass($concrete); // If the type is not instantiable, the developer is attempting to resolve // an abstract type such as an Interface of Abstract Class and there is // no binding registered for the abstractions so we need to bail out. if (! $reflector->isInstantiable()) { return $this->notInstantiable($concrete); } $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); // If there are no constructors, that means there are no dependencies then // we can just resolve the instances of the objects right away, without // resolving any other types or dependencies out of these containers. if (is_null($constructor)) { array_pop($this->buildStack); return new $concrete; } $dependencies = $constructor->getParameters(); // Once we have all the constructor’s parameters we can create each of the // dependency instances and then use the reflection instances to make a // new instance of this class, injecting the created dependencies in. $instances = $this->resolveDependencies( $dependencies ); array_pop($this->buildStack); return $reflector->newInstanceArgs($instances);}这时将返回控制器的实例,下面将通过url访问指定方法,一般控制器都会继承父类 Illuminate\Routing\Controller ,laravel为其设置了别名 BaseControllerpublic function dispatch(Route $route, $controller, $method){ $parameters = $this->resolveClassMethodDependencies( $route->parametersWithoutNulls(), $controller, $method ); if (method_exists($controller, ‘callAction’)) { return $controller->callAction($method, $parameters); } return $controller->{$method}(…array_values($parameters));}Laravel通过controller继承的callAction去调用子类的指定方法,也就是我们希望调用的自定义方法。public function callAction($method, $parameters){ return call_user_func_array([$this, $method], $parameters);}致谢感谢你看到这里,本篇文章源码解析靠个人理解。如有出入请拍砖。希望本篇文章可以帮到你。谢谢 ...

September 28, 2018 · 3 min · jiezi

Laravel源码解析之从入口开始

前言提升能力的方法并非使用更多工具,而是解刨自己所使用的工具。今天我们从Laravel启动的第一步开始讲起。入口文件laravel是单入口框架,所有请求必将经过index.phpdefine(‘LARAVEL_START’, microtime(true)); // 获取启动时间使用composer是现代PHP的标志require DIR.’/../vendor/autoload.php’; // 加载composer -> autoload.php加载启动文件$app = require_once DIR.’/../bootstrap/app.php’;获取$app是laravel启动的关键,也可以说$app是用于启动laravel内核的钥匙????。随后就是加载内核,载入服务提供者、门面所映射的实体类,中间件,最后到接收http请求并返回结果。$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); // 加载核心类$response = $kernel->handle( $request = Illuminate\Http\Request::capture());$response->send();$kernel->terminate($request, $response);看似短短的4行代码,这则是laravel的优雅之处。我们开始深层次解刨。bootstrap\app.php这个启动文件也可以看作是一个服务提供者,不过他并没有boot,register方法。因为入口文件直接加载他,所有这些没必要的方法就不存在了。作为启动文件,首页则是加载框架所有必须的要要件,例如registerBaseBindingsregisterBaseServiceProvidersregisterCoreContainerAliases,这其中包括了很多基础性的方法和类,例如db [\Illuminate\Database\DatabaseManager::class]auth [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class] log [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class] queue [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class] redis [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class] 等等 … 而$app这个在服务提供者的核心变量则就是Application实例化所得,而你在服务提供者内使用的make,bind,singleton来自他的父类Container,都说容器是laravel的核心概念。这块的概念后续我们会详细的讲解。$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));上面我们已经获得$app的实例化了,现在通过$app来注册核心类、异常类,并将$app返回给index.php$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);App\Http\Kernel核心类了所有的系统中间件群组中间件路由中间件当然你需要使用中间件也是在这个类中加载,是经常被使用的一个文件。protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, ]; /** * The application’s route middleware groups. * * @var array */ protected $middlewareGroups = [ ‘web’ => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ‘api’ => [ ’throttle:60,1’, ‘bindings’, ], ];这个核心类继承自他的父类Illuminate\Foundation\Http\Kernel::class,核心类做了很多事情,它会将所有的中间件全部存储到一个指定的数组,方便内核调用及其他类调用。namespace App\Http; use App\Api\Middleware\VerifyApiToken;use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel回到起点Laravel的启动经历了很繁琐的一个过程。这也是Laravel优雅的关键点。$response = $kernel->handle( $request = Illuminate\Http\Request::capture());$response->send();$kernel->terminate($request, $response);将请求传入则完成了整个laravel的启动,至于结果的返回则有开发者自行通过控制器或其他可访问类返回。致谢感谢你看到这里,本篇文章源码解析靠个人理解。如有出入请拍砖。希望本篇文章可以帮到你。谢谢 ...

September 27, 2018 · 1 min · jiezi

打造 Laravel 优美架构 谈可维护性与弹性设计

公司项目可能需要对架构进行重建,老大给了我一个视频让我学习里面的思想,看完后觉得收获很大,主讲人对laravel项目各个层次有很清晰的理解,力求做到职责单一分明,提高可维护性。下面是我看完视频对其内容的大概整理,以及一些自己的见解,有错误的请指出。视频:https://www.youtube.com/watch… (有墙各位懂的)Laravel简单架构:简单的小项目可能会把数据库查询,业务逻辑,数据传给View几乎所有操作都放在Controller,如何项目后期需求变大,最后Controller会变得很臃肿,难懂,不易维护(同样,有些会把所有增删改查,功能类写在Model,Controller再从Model一个个的拿,导致Model很乱,Model有关联表的时候可能会引起一些不必要的数据库查询)我自己的理解:用美宜佳卖商品给客人来理解,主要Controller是某个加盟商美宜佳门店,View是客人,Model是商品制造工厂(理解有些粗糙)Repository(商品仓库):跟Eloquent/DB操作相关的,例如增删改查,直接和数据库打交道的基础操作抽出来放在Repository中,repository中文是仓库,我的理解就是我们要从Model拿数据,先放在仓库repository中,统一由仓库管理分配,发挥仓库的职责Service(总部服务平台):商业逻辑,不是简单的查询数据,而是特定的任务,例如判断用户是否是会员,设置用户权限等等,这些操作建议放在Service,之后Controller再调用它个人理解:所以在Controller和Model/Eloquent中间垫两层,如果Repository理解为商品仓库的话,我的理解Service是类似总部内部的服务平台,加盟商Controller需要拿商品给客人View,不能直接去食品工厂Model拿,先通过仓库repository,然后总部服务平台Service进行打包啊,整理啊,发车啊(各种任务),最后再给到加盟商Controller手里Presenter(充值业务):一些比较固定,可以单独调用的,可以用Presenter抽出来,不需要让Model去做,下次修改也单独修改Presenter就行了,例如时间戳转成Y-m-d H:i:s格式,可以单独用Presenter处理后用@inject插入到前端模板,而不是把转化过程写在模板上面个人理解:所以在Controller和View中间可以加一层Presenter,我的理解有点类似:美宜佳商户(Controller)可以给客人(View)充公交卡,这种小事不需要劳费工厂(Model)Transformer(快餐小吃人工筛选):转换器,例如在仓库repository中有一个获取所有用户信息的查询操作:$this->user->all();但有些地方我们不需要用到那么多个字段,我只想有name和email字段,难道我要去改all()里面的参数,变成$this->user->all([’name’,’email’])?这样另外的地方又要全部字段,这不就冲突了?这时候Transformer就有用了,其实原理是对$this->user->all()获得的数据进行筛选后再输出,加了个筛选器。之后要修改结果字段就直接在transform修改即可,当然还可以额外添加需要的字段:array_set()个人理解:这一块我的理解就是有些客人需要点一些快餐,例如美宜佳里面的车仔面呀,烤肠呀,在卖出商品的时候需要根据客人的需求对小吃进行筛选再卖出去,不可能客人指点要一个烤肠,你把店里全部小吃拿给他,让他自个去筛选,中间卖出去的时候需要Transformer进行筛选再给出商品Formatter(包装):主要用于保持API返回格式的一致(使用方法和transform类似):个人理解:Formatter这一块我的理解就是商品包装,客人买东西,买小吃,你需要对商品先进行包装,当然这个包装肯定需要保持一致以上便是我再看完视频后对其进行总结整理,当然理论的说的容易,实际操作起来还有很多未知的问题,还是需要后面继续研究学习。

September 7, 2018 · 1 min · jiezi

使用 Laravel 5.5+ 更好的来实现 404 响应

译文首发于 使用 Laravel 5.5+ 更好的来实现 404 响应,转载请注明出处!Laravel 5.5.10 封装了两个有用的路由器方法,可以帮助我们为用户提供更好的 404 页面。现在,当抛出 404 异常时,Laravel 会显示一个漂亮的 404.blade.php 视图文件,你可以自定义显示给用户 UI,但在该视图中,你无权访问 session,cookie,身份验证(auth)等…在 laravel 5.5.10 中,我们有一个新的 Route::fallback() 方法,用于定义当没有其他路由与请求匹配时 Laravel 回退的路由。Route::fallback(function () { return ‘Sorry’ . auth()->user()->name . ‘! This page does not exist.’;});所以,现在我们可以使用具有正常页面和页脚的应用布局,来替代简单的 404 视图,同时还能给用户显示一条友好的提示信息。Route::fallback(function() { return response()->view(’notFound’, [], 404);});@extends(’layout.app’)@section(‘content’) <h3>Sorry! this page doesn’t exist.</h3>@stop当 Laravel 渲染这个回退(fallback)路由时,会运行所有的中间件,因此当你在 web.php 路由文件中定义了回退路由时,所有处在 web 中间件组的中间件都会被执行,这样我们就可以获取 session 数据了。API 接口说明现在当你点击 /non-existing-page 时,你会看到在回退路由中定义的视图,甚至当你点击 /api/non-existing-endpoint 时,如果你也不想提供这个接口,你可以到 api 回退路由中定义 JSON 响应,让我们到 api.php 路由文件中定义另外一个回退路由:Route::fallback(function() { return response()->json([‘message’ => ‘Not Found!]);});由于 api 中间件组带有 /api 前缀,所有带有 /api 前缀的未定义的路由,都会进入到 api.php 路由文件中的回退路由,而不是 web.php 路由文件中所定义的那个。使用 abort(404) 和 ModelNotFound 异常当使用 abort(404) 时会抛出一个 NotFoundHttpException,此时处理器会为我们渲染出 404.blade.php 视图文件,同样的 ModelNotFoundException 异常也会做同样的处理,那么我们应该如何如何处理才能在更好的渲染出回退路由的视图,而不是一个普通的视图呢?class Handler extends ExceptionHandler{ public function render($request, Exception $exception) { if ($exception instanceof NotFoundHttpException) { return Route::responseWithRoute(‘fallback’); } if ($exception instanceof ModelNotFoundException) { return Route::responseWithRoute(‘fallback’); } return parent::render($request, $exception); }}Route::respondWithRoute(‘fallback’) 回去跑名为 fallback 的路由,我们可以像下面这样为回退路由命名:Route::fallback(function() { return response()->view(’notFound’, [], 404);})->name(‘fallback’);甚至,你还可以为特定的资源指定回退路由:if ($exception instanceof ModelNotFoundException) { return $exception->getModel() == Server::class ? Route::respondWithRoute(‘serverFallback’) : Route::respondWithRoute(‘fallback’);}现在我们需要在路由文件中定义这个回退路由:Route::fallback(function(){ return ‘We could not find this server, there are other ‘. auth()->user()->servers()->count() . ’ under your account ……’;})->name(‘serverFallback’);原文Better 404 responses using Laravel 5.5+ ...

September 3, 2018 · 1 min · jiezi

laravel 模型中三种数据删除方法,效率却各不相同

Laraval模型三种删除方法,效率却各不相同

September 2, 2018 · 1 min · jiezi

Laravel核心解读--Cookie源码分析

Laravel Cookie源码分析使用Cookie的方法为了安全起见,Laravel 框架创建的所有 Cookie 都经过加密并使用一个认证码进行签名,这意味着如果客户端修改了它们则需要对其进行有效性验证。我们使用 IlluminateHttpRequest 实例的 cookie 方法从请求中获取 Cookie 的值:$value = $request->cookie(’name’);也可以使用Facade Cookie来读取Cookie的值:Cookie::get(’name’, ‘’);//第二个参数的意思是读取不到name的cookie值的话,返回空字符串添加Cookie到响应可以使用 响应对象的cookie 方法将一个 Cookie 添加到返回的 IlluminateHttpResponse 实例中,你需要传递 Cookie 的名称、值、以及有效期(分钟)到这个方法:return response(‘Learn Laravel Kernel’)->cookie( ‘cookie-name’, ‘cookie-value’, $minutes);响应对象的cookie 方法接收的参数和 PHP 原生函数 setcookie 的参数一致:return response(‘Learn Laravel Kernel’)->cookie( ‘cookie-name’, ‘cookie-value’, $minutes, $path, $domain, $secure, $httpOnly);还可使用Facade Cookie的queue方法以队列的形式将Cookie添加到响应:Cookie::queue(‘cookie-name’, ‘cookie-value’);queue 方法接收 Cookie 实例或创建 Cookie 所必要的参数作为参数,这些 Cookie 会在响应被发送到浏览器之前添加到响应中。接下来我们来分析一下Laravel中Cookie服务的实现原理。Cookie服务注册之前在讲服务提供器的文章里我们提到过,Laravel在BootStrap阶段会通过服务提供器将框架中涉及到的所有服务注册到服务容器里,这样在用到具体某个服务时才能从服务容器中解析出服务来,所以Cookie服务的注册也不例外,在config/app.php中我们能找到Cookie对应的服务提供器和门面。‘providers’ => [ /* * Laravel Framework Service Providers… / …… IlluminateCookieCookieServiceProvider::class, ……] ‘aliases’ => [ …… ‘Cookie’ => IlluminateSupportFacadesCookie::class, ……]Cookie服务的服务提供器是 IlluminateCookieCookieServiceProvider ,其源码如下:<?phpnamespace IlluminateCookie;use IlluminateSupportServiceProvider;class CookieServiceProvider extends ServiceProvider{ /* * Register the service provider. * * @return void / public function register() { $this->app->singleton(‘cookie’, function ($app) { $config = $app->make(‘config’)->get(‘session’); return (new CookieJar)->setDefaultPathAndDomain( $config[‘path’], $config[‘domain’], $config[‘secure’], $config[‘same_site’] ?? null ); }); }}在CookieServiceProvider里将IlluminateCookieCookieJar类的对象注册为Cookie服务,在实例化时会从Laravel的config/session.php配置中读取出path、domain、secure这些参数来设置Cookie服务用的默认路径和域名等参数,我们来看一下CookieJar里setDefaultPathAndDomain的实现:namespace IlluminateCookie;class CookieJar implements JarContract{ /* * 设置Cookie的默认路径和Domain * * @param string $path * @param string $domain * @param bool $secure * @param string $sameSite * @return $this / public function setDefaultPathAndDomain($path, $domain, $secure = false, $sameSite = null) { list($this->path, $this->domain, $this->secure, $this->sameSite) = [$path, $domain, $secure, $sameSite]; return $this; }}它只是把这些默认参数保存到CookieJar对象的属性中,等到make生成SymfonyComponentHttpFoundationCookie对象时才会使用它们。生成Cookie上面说了生成Cookie用的是Response对象的cookie方法,Response的是利用Laravel的全局函数cookie来生成Cookie对象然后设置到响应头里的,有点乱我们来看一下源码class Response extends BaseResponse{ /* * Add a cookie to the response. * * @param SymfonyComponentHttpFoundationCookie|mixed $cookie * @return $this / public function cookie($cookie) { return call_user_func_array([$this, ‘withCookie’], func_get_args()); } /* * Add a cookie to the response. * * @param SymfonyComponentHttpFoundationCookie|mixed $cookie * @return $this / public function withCookie($cookie) { if (is_string($cookie) && function_exists(‘cookie’)) { $cookie = call_user_func_array(‘cookie’, func_get_args()); } $this->headers->setCookie($cookie); return $this; }}看一下全局函数cookie的实现:/* * Create a new cookie instance. * * @param string $name * @param string $value * @param int $minutes * @param string $path * @param string $domain * @param bool $secure * @param bool $httpOnly * @param bool $raw * @param string|null $sameSite * @return IlluminateCookieCookieJar|SymfonyComponentHttpFoundationCookie /function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = false, $httpOnly = true, $raw = false, $sameSite = null){ $cookie = app(CookieFactory::class); if (is_null($name)) { return $cookie; } return $cookie->make($name, $value, $minutes, $path, $domain, $secure, $httpOnly, $raw, $sameSite);}通过cookie函数的@return标注我们能知道它返回的是一个IlluminateCookieCookieJar对象或者是SymfonyComponentHttpFoundationCookie对象。既cookie函数在无接受参数时返回一个CookieJar对象,在有Cookie参数时调用了CookieJar的make方法返回一个SymfonyComponentHttpFoundationCookie对象。拿到Cookie对象后程序接着流程往下走把Cookie设置到Response对象的headers属性里,`headers`属性引用了SymfonyComponentHttpFoundationResponseHeaderBag对象class ResponseHeaderBag extends HeaderBag{ public function setCookie(Cookie $cookie) { $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; $this->headerNames[‘set-cookie’] = ‘Set-Cookie’; }}我们可以看到这里只是把Cookie对象暂存到了headers对象里,真正把Cookie发送到浏览器是在Laravel返回响应时发生的,在Laravel的public/index.php里:$response->send();Laravel的Response继承自Symfony的Response,send方法定义在Symfony的Response里namespace SymfonyComponentHttpFoundation;class Response{ /* * Sends HTTP headers and content. * * @return $this / public function send() { $this->sendHeaders(); $this->sendContent(); if (function_exists(‘fastcgi_finish_request’)) { fastcgi_finish_request(); } elseif (!in_array(PHP_SAPI, array(‘cli’, ‘phpdbg’), true)) { static::closeOutputBuffers(0, true); } return $this; } public function sendHeaders() { // headers have already been sent by the developer if (headers_sent()) { return $this; } // headers foreach ($this->headers->allPreserveCase() as $name => $values) { foreach ($values as $value) { header($name.’: ‘.$value, false, $this->statusCode); } } // status header(sprintf(‘HTTP/%s %s %s’, $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); return $this; } /* * Returns the headers, with original capitalizations. * * @return array An array of headers / public function allPreserveCase() { $headers = array(); foreach ($this->all() as $name => $value) { $headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value; } return $headers; } public function all() { $headers = parent::all(); foreach ($this->getCookies() as $cookie) { $headers[‘set-cookie’][] = (string) $cookie; } return $headers; }} 在Response的send方法里发送响应头时将Cookie数据设置到了Http响应首部的Set-Cookie字段里,这样当响应发送给浏览器后浏览器就能保存这些Cookie数据了。至于用门面Cookie::queue以队列的形式设置Cookie其实也是将Cookie暂存到了CookieJar对象的queued属性里namespace IlluminateCookie;class CookieJar implements JarContract{ public function queue(…$parameters) { if (head($parameters) instanceof Cookie) { $cookie = head($parameters); } else { $cookie = call_user_func_array([$this, ‘make’], $parameters); } $this->queued[$cookie->getName()] = $cookie; } public function queued($key, $default = null) { return Arr::get($this->queued, $key, $default); }}然后在web中间件组里边有一个IlluminateCookieMiddlewareAddQueuedCookiesToResponse中间件,它在响应返回给客户端之前将暂存在queued属性里的Cookie设置到了响应的headers对象里:namespace IlluminateCookieMiddleware;use Closure;use IlluminateContractsCookieQueueingFactory as CookieJar;class AddQueuedCookiesToResponse{ /* * The cookie jar instance. * * @var IlluminateContractsCookieQueueingFactory / protected $cookies; /* * Create a new CookieQueue instance. * * @param IlluminateContractsCookieQueueingFactory $cookies * @return void / public function __construct(CookieJar $cookies) { $this->cookies = $cookies; } /* * Handle an incoming request. * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed / public function handle($request, Closure $next) { $response = $next($request); foreach ($this->cookies->getQueuedCookies() as $cookie) { $response->headers->setCookie($cookie); } return $response; }这样在Response对象调用send方法时也会把通过Cookie::queue()设置的Cookie数据设置到Set-Cookie响应首部中去了。读取CookieLaravel读取请求中的Cookie值$value = $request->cookie(’name’); 其实是Laravel的Request对象直接去读取Symfony请求对象的cookies来实现的, 我们在写Laravel Request对象的文章里有提到它依赖于Symfony的Request, Symfony的Request在实例化时会把PHP里那些$_POST、$_COOKIE全局变量抽象成了具体对象存储在了对应的属性中。namespace IlluminateHttp;class Request extends SymfonyRequest implements Arrayable, ArrayAccess{ public function cookie($key = null, $default = null) { return $this->retrieveItem(‘cookies’, $key, $default); } protected function retrieveItem($source, $key, $default) { if (is_null($key)) { return $this->$source->all(); } //从Request的cookies属性中获取数据 return $this->$source->get($key, $default); }}关于通过门面Cookie::get()读取Cookie的实现我们可以看下Cookie门面源码的实现,通过源码我们知道门面Cookie除了通过外观模式代理Cookie服务外自己也定义了两个方法:<?phpnamespace IlluminateSupportFacades;/* * @see IlluminateCookieCookieJar /class Cookie extends Facade{ /* * Determine if a cookie exists on the request. * * @param string $key * @return bool / public static function has($key) { return ! is_null(static::$app[‘request’]->cookie($key, null)); } /* * Retrieve a cookie from the request. * * @param string $key * @param mixed $default * @return string / public static function get($key = null, $default = null) { return static::$app[‘request’]->cookie($key, $default); } /* * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return ‘cookie’; }}Cookie::get()和Cookie::has()是门面直接读取Request对象cookies属性里的Cookie数据。Cookie加密关于对Cookie的加密可以看一下IlluminateCookieMiddlewareEncryptCookies中间件的源码,它的子类AppHttpMiddlewareEncryptCookies是Laravelweb中间件组里的一个中间件,如果想让客户端的Javascript程序能够读Laravel设置的Cookie则需要在AppHttpMiddlewareEncryptCookies的$exception里对Cookie名称进行声明。Laravel中Cookie模块大致的实现原理就梳理完了,希望大家看了我的源码分析后能够清楚Laravel Cookie实现的基本流程这样在遇到困惑或者无法通过文档找到解决方案时可以通过阅读源码看看它的实现机制再相应的设计解决方案。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

September 2, 2018 · 4 min · jiezi