Laravel作为在国内国外都颇为流行的PHP框架,风格优雅,其拥有自己的一些特点。
一. 请求周期
Laravel 采用了单一入口模式,应用的所有请求入口都是 public/index.php 文件。
- 注册类文件自动加载器:Laravel通过composer进行依赖管理,并在bootstrap/autoload.php中注册了Composer Auto Loader (),应用中类的命名空间将被映射到类文件实际路径,不再需要开发者手动导入各种类文件,而由自动加载器自行导入。因此,Laravel允许你在应用中定义的类可以自由放置在Composer Auto Loader能自动加载的任何目录下,但大多数时候还是建议放置在app目录下或app的某个子目录下
- 创建服务容器:从 bootstrap/app.php 文件中取得 Laravel 应用实例 $app (服务容器)
- 创建 HTTP / Console 内核:传入的请求会被发送给 HTTP 内核或者 console 内核进行处理,HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类。它定义了一个 bootstrappers 数组,数组中的类在请求真正执行前进行前置执行,这些引导程序配置了错误处理,日志记录,检测应用程序环境,以及其他在请求被处理前需要完成的工作;HTTP 内核同时定义了一个 HTTP 中间件列表,所有的请求必须在处理前通过这些中间件处理 HTTP session 的读写,判断应用是否在维护模式, 验证 CSRF token 等等
- 载入服务提供者至容器:在内核引导启动的过程中最重要的动作之一就是载入服务提供者到你的应用,服务提供者负责引导启动框架的全部各种组件,例如数据库、队列、验证器以及路由组件。因为这些组件引导和配置了框架的各种功能,所以服务提供者是整个 Laravel 启动过程中最为重要的部分,所有的服务提供者都配置在 config/app.php 文件中的 providers 数组中。首先,所有提供者的 register 方法会被调用;一旦所有提供者注册完成,接下来,boot 方法将会被调用
- 分发请求:一旦应用完成引导和所有服务提供者都注册完成,Request 将会移交给路由进行分发。路由将分发请求给一个路由或控制器,同时运行路由指定的中间件
二. 服务容器和服务提供者
服务容器是 Laravel 管理类依赖和运行依赖注入的有力工具,在类中可通过 $this->app 来访问容器,在类之外通过 $app 来访问容器;服务提供者是 Laravel 应用程序引导启动的中心,关系到服务提供者自身、事件监听器、路由以及中间件的启动运行。应用程序中注册的路由通过RouteServiceProvider实例来加载;事件监听器在EventServiceProvider类中进行注册;中间件又称路由中间件,在app/Http/Kernel.php类文件中注册,调用时与路由进行绑定。在新创建的应用中,AppServiceProvider 文件中方法实现都是空的,这个提供者是你添加应用专属的引导和服务的最佳位置,当然,对于大型应用你可能希望创建几个服务提供者,每个都具有粒度更精细的引导。服务提供者在 config/app.php 配置文件中的providers数组中进行注册
app->singleton(Connection::class, function ($app) { return new Connection(config('riak')); }); }}
三. 依赖注入
Laravel 实现依赖注入方式有两种:自动注入和主动注册。自动注入通过参数类型提示由服务容器自动注入实现;主动注册则需开发人员通过绑定机制来实现,即绑定服务提供者或类(参考: )。
- 绑定服务提供者或类:这种方式对依赖注入的实现可以非常灵活多样
use Illuminate\Support\Facades\Storage;use App\Http\Controllers\PhotoController;use App\Http\Controllers\VideoController;use Illuminate\Contracts\Filesystem\Filesystem;$this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); });$this->app->when(VideoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); });
- 参数类型声明:通过对类的构造器参数类型、类的方法参数类型、闭包的参数类型给出提示来实现
users = $users; } /** * 储存一个新用户。 * * @param Request $request * @return Response */ public function store(Request $request) { $name = $request->input('name'); // }}
- 路由参数依赖:下边的示例使用 Illuminate\Http\Request 类型提示的同时还获取到路由参数id
你的路由可能是这样定义的:Route::put('user/{id}', 'UserController@update');而控制器对路由参数id的依赖却可能是这样实现的:
四. Artisan Console
Laravel利用PHP的CLI构建了强大的Console工具artisan,artisan几乎能够创建任何你想要的模板类以及管理配置你的应用,在开发和运维管理中扮演着极其重要的角色,artisan是Laravel开发不可或缺的工具。在Laravel根目录下运行:PHP artisan list可查看所有命令列表。用好artisan能极大地简化开发工作,并减少错误发生的可能;另外,还可以编写自己的命令。下面列举部分比较常用的命令:
- 启用维护模式:php artisan down --message='Upgrading Database' --retry=60
- 关闭维护模式:php artisan up
- 生成路由缓存:php artisan route:cache
- 清除路由缓存:php artisan route:clear
- 数据库迁移 Migrations:php artisan make:migration create_users_table --create=users
- 创建资源控制器:php artisan make:controller PhotoController --resource --model=Photo
- 创建模型及迁移:php artisan make:model User -m
五. 表单验证机制
表单验证在web开发中是不可或缺的,其重要性也不言而喻,也算是每个web框架的标配部件了。Laravel表单验证拥有标准且庞大的规则集,通过规则调用来完成数据验证,多个规则组合调用须以“|”符号连接,一旦验证失败将自动回退并可自动绑定视图。
下例中,附加bail规则至title属性,在第一次验证required失败后将立即停止验证;“.”语法符号在Laravel中通常表示嵌套包含关系,这个在其他语言或框架语法中也比较常见
$this->validate($request, [ 'title' => 'bail|required|unique:posts|max:255', 'author.name' => 'required', 'author.description' => 'required',]);
Laravel验证规则参考 ;另外,在Laravel开发中还可采用如下扩展规则:
- 自定义FormRequest (须继承自 Illuminate\Foundation\Http\FormRequest )
- Validator::make()手动创建validator实例
- 创建validator实例验证后钩子
- 按条件增加规则
- 数组验证
- 自定义验证规则
六. 事件机制
Laravel事件机制是一种很好的应用解耦方式,因为一个事件可以拥有多个互不依赖的监听器。事件类 (Event) 类通常保存在 app/Events
目录下,而它们的监听类 (Listener) 类被保存在 app/Listeners
目录下,使用 Artisan 命令来生成事件和监听器时他们会被自动创建。
- 注册事件和监听器:EventServiceProvider的 listen 属性数组用于事件(键)到对应的监听器(值)的注册,然后运行 php artisan event:generate将自动生成EventServiceProvider中所注册的事件(类)模板和监听器模板,然后在此基础之上进行修改来实现完整事件和监听器定义;另外,你也可以在 EventServiceProvider 类的 boot 方法中通过注册闭包事件来实现
- 定义事件(类):事件(类)就是一个包含与事件相关信息数据的容器,不包含其它逻辑
1 order = $order;23 }24 }
- 定义监听器:事件监听器在 handle 方法中接受了事件实例作为参数
1 order 来访问 order ...28 }29 }
- 停止事件传播:在监听器的
handle
方法中返回false
来停止事件传播到其他的监听器 - 触发事件:调用 event 辅助函数可触发事件,事件将被分发到它所有已经注册的监听器上
1
- 队列化事件监听器:如果监听器中需要实现一些耗时的任务,比如发送邮件或者进行 HTTP 请求,那把它放到队列中处理是非常有用的。在使用队列化监听器,须在服务器或者本地环境中配置队列并开启一个队列监听器,还要增加 ShouldQueue 接口到你的监听器类;如果你想要自定义队列的连接和名称,你可以在监听器类中定义
$connection
和$queue
属性;如果队列监听器任务执行次数超过在工作队列中定义的最大尝试次数,监听器的 failed 方法将会被自动调用1
-
事件订阅者:事件订阅者允许在单个类中定义多个事件处理器,还应该定义一个 subscribe 方法,这个方法接受一个事件分发器的实例,通过调用事件分发器的 listen 方法来注册事件监听器,然后在 EventServiceProvider 类的 $subscribe 属性中注册订阅者
1 listen(25 'Illuminate\Auth\Events\Login',26 'App\Listeners\UserEventSubscriber@onUserLogin'27 );28 29 $events->listen(30 'Illuminate\Auth\Events\Logout',31 'App\Listeners\UserEventSubscriber@onUserLogout'32 );33 }34 35 }
七. Eloquent 模型
Eloquent ORM 以ActiveRecord形式来和数据库进行交互,拥有全部的数据表操作定义,单个模型实例对应数据表中的一行
1 $flights = App\Flight::where('active', 1)2 ->orderBy('name', 'desc')3 ->take(10)4 ->get();
config/database.php中包含了模型的相关配置项。Eloquent 模型约定:
- 数据表名:模型以单数形式命名(CamelCase),对应的数据表为蛇形复数名(snake_cases),模型的$table属性也可用来指定自定义的数据表名称
- 主键:模型默认以id为主键且假定id是一个递增的整数值,也可以通过来自定义;如果主键非递增数字值,应设置primaryKey来自定义;如果主键非递增数字值,应设置incrementing = false
- 时间戳:模型会默认在你的数据库表有 created_at 和 updated_at 字段,设置可关闭模型自动维护这两个字段;timestamps=false可关闭模型自动维护这两个字段;dateFormat 属性用于在模型中设置自己的时间戳格式
- 数据库连接:模型默认会使用应用程序中配置的数据库连接,如果你想为模型指定不同的连接,可以使用 $connection 属性自定义
- 批量赋值:当用户通过 HTTP 请求传入了非预期的参数,并借助这些参数 create 方法更改了数据库中你并不打算要更改的字段,这时就会出现批量赋值(Mass-Assignment)漏洞,所以你需要先在模型上定义一个 白名单,允许批量赋值字段名数组或fillable(白名单,允许批量赋值字段名数组)或guarded(黑名单,禁止批量赋值字段名数组)
1 // 用属性取回航班,当结果不存在时创建它...2 $flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);3 4 // 用属性取回航班,当结果不存在时实例化一个新实例...5 $flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
- 模型软删除:如果模型有一个非空值 deleted_at,代表模型已经被软删除了。要在模型上启动软删除,则必须在模型上使用Illuminate\Database\Eloquent\SoftDeletes trait 并添加 deleted_at 字段到你的模型 $dates 属性上和数据表中,通过调用trashed方法可查询模型是否被软删除
1
- 查询作用域:Laravel允许对模型设定全局作用域和本地作用域(包括动态范围),全局作用域允许我们为模型的所有查询添加条件约束(定义一个实现 Illuminate\Database\Eloquent\Scope 接口的类),而本地作用域允许我们在模型中定义通用的约束集合(模型方法前加上一个
scope
前缀)。作用域总是返回查询构建器1 全局作用域定义: 2 where('age', '>', 200);22 }23 }24 25 本地作用域:26 where('votes', '>', 100);42 }43 44 /**45 * 限制查询只包括活跃的用户。46 *47 * @return \Illuminate\Database\Eloquent\Builder48 */49 public function scopeActive($query)50 {51 return $query->where('active', 1);52 }53 }54 55 动态范围:56 where('type', $type);72 }73 }
- 隐藏和显示属性:模型 属性用于隐藏属性和关联的输出,hidden属性用于隐藏属性和关联的输出,visible 属性用于显示属性和关联的输出,另外makeVisible()还可用来临时修改可见性。当你要对关联进行隐藏时,需使用关联的方法名称,而不是它的动态属性名称
1 17 18 //makeVisible()用来临时修改可见性19 return $user->makeVisible('attribute')->toArray();
-
访问器和修改器:访问器(getFooAttribute)和修改器(setFooAttribute)可以让你修改 Eloquent 模型中的属性或者设置它们的值,比如你想要使用 Laravel 加密器来加密一个被保存在数据库中的值,当你从 Eloquent 模型访问该属性时该值将被自动解密。访问器和修改器要遵循cameCase命名规范,修改器会设置值到 Eloquent 模型内部的
$attributes
属性上1 attributes['first_name'] = strtolower($value);29 }30 }
而对于访问器与修改器的调用将是模型对象自动进行的
1 $user = App\User::find(1);2 $user->first_name = 'Sally';//将自动调用相应的修改器3 $firstName = $user->first_name;//将自动调用相应的访问器
- 追加属性:在转换模型到数组或JSON时,你希望添加一个在数据库中没有对应字段的属性,首先你需要为这个值定义一个 访问器,然后添加该属性到改模型的 appends 属性中
1 attributes['admin'] == 'yes';24 }25 }
- 属性类型转换:$casts 属性数组在模型中提供了将属性转换为常见的数据类型的方法,且键是那些需要被转换的属性名称,值则是代表字段要转换的类型。支持的转换的类型有:integer、real、float、double、string、boolean、object、array、collection、date、datetime、timestamp
1 'boolean',//is_admin 属性以整数(0 或 1)被保存在我们的数据库中,把它转换为布尔值16 ];17 }
-
序列化: Laravel模型及关联可递归序列化成数组或JSON
1 //单个模型实例序列化成数组 2 $user = App\User::with('roles')->first(); 3 return $user->toArray(); 4 //集合序列化成数组 5 $users = App\User::all(); 6 return $users->toArray(); 7 8 //单个模型实例序列化成JSON 9 $user = App\User::find(1);10 return $user->toJson();11 //直接进行string转换会将模型或集合序列化成JSON12 $user = App\User::find(1);13 return (string) $user;14 //因此你可以直接从应用程序的路由或者控制器中返回 Eloquent 对象15 Route::get('users', function () {16 return App\User::all();17 });
- 关联(方法)与动态属性:在 Eloquent 模型中,关联被定义成方法(methods),也可以作为强大的查询语句构造器
1 $user->posts()->where('active', 1)->get();
Eloquent 模型支持多种类型的关联:一对一、一对多、多对多、远层一对多、多态关联、多态多对多关联
举个例子,一个 User 模型会关联一个 Phone 模型,一对一关联(hasOne)1 hasOne('App\Phone');15 }16 }
动态属性允许你访问关联方法,使用 Eloquent 的动态属性来获取关联记录,如同他们是定义在模型中的属性
1 $phone = User::find(1)->phone;
Eloquent 会假设对应关联的外键名称是基于模型名称的。在这个例子里,它会自动假设 Phone 模型拥有 user_id 外键。如果你想要重写这个约定,则可以传入第二个参数到 hasOne 方法里
1 return $this->hasOne('App\Phone', 'foreign_key');
如果你想让关联使用 id 以外的值,则可以传递第三个参数至 hasOne 方法来指定你自定义的键
1 return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
如果我们要在 Phone 模型上定义一个反向关联,此关联能够让我们访问拥有此电话的 User 模型。我们可以定义与 hasOne 关联相对应的 belongsTo 方法
1 belongsTo('App\User');15 }16 }
-
模型事件: Laravel为模型定义的事件包括creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored。 模型上定义一个
$events
属性1 UserSaved::class,21 'deleted' => UserDeleted::class,22 ];23 }
如果你在一个给定的模型中监听许多事件,也可使用观察者将所有监听器变成一个类,类的一个方法就是一个事件监听器
1 定义观察者: 2
八. Laravel的Restful风格
一般认为Restful风格的资源定义不包含操作,但是在Laravel中操作(动词)也可作为一种资源来定义。下图是对Laravel中资源控制器操作原理的描述,可以看到,create、edit就直接出现在了URI中,它们是一种合法的资源。对于create和edit这两种资源的访问都采用GET方法来实现,第一眼看到顿感奇怪,后来尝试通过artisan console生成资源控制器,并注意到其对create、edit给出注释“ Show the form for ”字样,方知它们只是用来展现表单而非提交表单的。
九. 扩展开发
我们知道,Laravel本身是基于Composer管理的一个包,遵循Composer的相关规范,可以通过Composer来添加所依赖的其他Composer包,因此在做应用的扩展开发时,可以开发Composer包然后引入项目中即可;另外也可开发基于Laravel的专属扩展包。下面所讲的就是Laravel的专属扩展开发,最好的方式是使用 contracts ,而不是 facades,因为你开发的包并不能访问所有 Laravel 提供的测试辅助函数,模拟 contracts 要比模拟 facade 简单很多。
- 服务提供者:服务提供者是你的扩展包与 Laravel 连接的重点,须定义自己的服务提供者并继承自 Illuminate\Support\ServiceProvider 基类
- 路由:若要为你的扩展包定义路由,只需在包的服务提供者的 boot 方法中传递 routes 文件路径到 loadRoutesFrom 方法即可
1 /**2 * 在注册后进行服务的启动。3 *4 * @return void5 */6 public function boot()7 {8 $this->loadRoutesFrom(__DIR__.'/path/to/routes.php');9 }
- 配置文件:你可以选择性地将扩展包的配置文件发布(publishes)到应用程序本身的config目录上或者合并(mergeConfigFrom)到应用程序里的副本配置文件中,但不应在配置文件中定义闭包函数,当执行 config:cache Artisan命令时,它们将不能正确地序列化
1 /** 2 * 在注册后进行服务的启动。 3 * 4 * 用户使用 vendor:publish 命令可将扩展包的文件将会被复制到指定的位置上。 5 * 6 * @return void 7 */ 8 public function boot() 9 {10 $this->publishes([11 __DIR__.'/path/to/config/courier.php' => config_path('courier.php'),12 ]);13 }14 15 $value = config('courier.option');//只要你的配置文件被发布,就可以如其它配置文件一样被访问16 17 /**18 * 或者选择性在容器中注册绑定。19 *20 * 此方法仅合并配置数组的第一级。如果您的用户部分定义了多维配置数组,则不会合并缺失的选项21 *22 * @return void23 */24 public function register()25 {26 $this->mergeConfigFrom(27 __DIR__.'/path/to/config/courier.php', 'courier'28 );29 }
- 数据库迁移:如果你的扩展包包含数据库迁移,需要使用 loadMigrationsFrom 方法告知 Laravel 如何去加载它们。在运行 php artisan migrate 命令时,它们就会自动被执行,不需要把它们导出到应用程序的 database/migrations 目录
1 /**2 * 在注册后进行服务的启动。3 *4 * @return void5 */6 public function boot()7 {8 $this->loadMigrationsFrom(__DIR__.'/path/to/migrations');9 }
- 语言包:如果你的扩展包里面包含了本地化,则可以使用 loadTranslationsFrom 方法来告知 Laravel 该如何加载它们。下例假设你的包名称为courier
1 /** 2 * 在注册后进行服务的启动。 3 * 4 * @return void 5 */ 6 public function boot() 7 { 8 $this->loadTranslationsFrom(__DIR__.'/path/to/translations', 'courier'); 9 10 //如果不想发布语言包至应用程序的 resources/lang/vendor 目录,请注销对$this->publishes()调用。运行 Laravel 的 vendor:publish Artisan 命令可将扩展包的语言包复制到指定的位置上11 $this->publishes([12 __DIR__.'/path/to/translations' => resource_path('lang/vendor/courier'),13 ]);14 }15 16 echo trans('courier::messages.welcome');//扩展包翻译参照使用了双分号 package::file.line 语法
-
视图:若要在 Laravel 中注册扩展包 视图,则必须告诉 Laravel 你的视图位置,loadViewsFrom 方法允许传递视图模板路径与扩展包名称两个参数。需要特别指出的是,当你使用 loadViewsFrom 方法时,Laravel 实际上为你的视图注册了两个位置:一个是应用程序的 resources/views/vendor 目录,另一个是你所指定的目录。Laravel会先检查 resources/views/vendor 目录是否存在待加载视图,如果不存在,才会从指定的目录去加载,这个方法可以让用户很方便的自定义或重写扩展包视图。
1 /** 2 * 在注册后进行服务的启动。 3 * 4 * @return void 5 */ 6 public function boot() 7 { 8 $this->loadViewsFrom(__DIR__.'/path/to/views', 'courier'); 9 10 //若要发布扩展包的视图至 resources/views/vendor 目录,则必须使用服务提供者的 publishes 方法。运行 Laravel 的 vendor:publish Artisan 命令时,扩展包的视图将会被复制到指定的位置上 11 $this->publishes([12 __DIR__.'/path/to/views' => resource_path('views/vendor/courier'),13 ]); 14 }15 16 //扩展包视图参照使用了双分号 package::view 语法17 Route::get('admin', function () {18 return view('courier::admin');19 });
-
命令:使用 commands 方法给扩展包注册 Artisan 命令,命令的定义要遵循Laravel Artisan 命令规范
1 /** 2 * 在注册后进行服务的启动。 3 * 4 * @return void 5 */ 6 public function boot() 7 { 8 if ($this->app->runningInConsole()) { 9 $this->commands([10 FooCommand::class,11 BarCommand::class,12 ]);13 }14 }
-
公用 Assets:你可以发布像 JavaScript、CSS 和图片这些资源文件到应用程序的
public
目录上。当用户执行vendor:publish
命令时,您的 Assets 将被复制到指定的发布位置。由于每次更新包时通常都需要覆盖资源,因此您可以使用--force
标志:php artisan vendor:publish --tag=public --force1 /** 2 * 在注册后进行服务的启动。 3 * 4 * @return void 5 */ 6 public function boot() 7 { 8 $this->publishes([ 9 __DIR__.'/path/to/assets' => public_path('vendor/courier'),10 ], 'public');11 }
-
发布群组文件:你可能想让用户不用发布扩展包的所有资源文件,只需要单独发布扩展包的配置文件即可,通过在调用
publishes
方法时使用标签来实现1 /** 2 * 在注册后进行服务的启动。 3 * 4 * @return void 5 */ 6 public function boot() 7 { 8 $this->publishes([ 9 __DIR__.'/../config/package.php' => config_path('package.php')10 ], 'config');11 12 $this->publishes([13 __DIR__.'/../database/migrations/' => database_path('migrations')14 ], 'migrations');15 }
对于上例运行命令 php artisan vendor:publish --tag=config 时将忽略掉migrations部