Laravel作为在国内国外都颇为流行的PHP框架,风格优雅,其拥有自己的一些特点,且也发布长期支持版(LTS)。
一. 请求周期
Laravel 采用了单一入口模式,应用的所有请求入口都是 public/index.php 文件。
注册类文件自动加载器:Laravel通过composer进行依赖管理,并在bootstrap/autoload.php中注册了Composer Auto Loader (PSR-4),应用中类的命名空间将被映射到类文件实际路径,不再需要开发者手动导入各种类文件,而由自动加载器自行导入。因此,Laravel允许你在应用中定义的类可以自由放置在Composer Auto Loader能自动加载的任何目录下,但大多数时候还是建议放置在app目录下或app的某个子目录下。
创建服务容器:从 bootstrap/app.php 文件中取得 Laravel 应用实例 $app(服务容器)。
创建 HTTP / Console 内核:传入的请求在HTTP / Console 内核中进行预处理。HTTP 内核继承自 Illuminate\Foundation\Http\Kernel 类,其中注入了 $app 和 $router 两个实例,内核是完成应用引导、请求处理(包括通过Router转发请求等)的场所。HTTP Kernel 定义了一个 bootstrappers 数组,配置了环境变量加载、应用配置加载、错误处理,以及其他在请求被处理前需要完成的工作。
载入服务提供者至容器:在内核引导启动的过程中最重要的动作之一就是载入服务提供者到你的 $app (即所有的服务提供者都要挂载到服务容器下去执行),服务提供者负责引导启动框架的全部各种组件,例如数据库、队列、验证器以及路由组件。因为这些组件引导和配置了框架的各种功能,所以服务提供者是整个 Laravel 启动过程中最为重要的部分,所有的服务提供者都配置在 config/app.php 文件中的 providers 数组中。首先,所有提供者的 register 方法会被调用;一旦所有提供者注册完成,接下来,boot 方法将会被调用。
分发请求:一旦应用完成引导和所有服务提供者都注册完成,Request 将会移交给Router进行分发。在通过Router转发请求时,所有请求都必须先经过全局HTTP中间件栈的处理,再调度到Router并获得其回调,然后执行该回调。
关于中间件:
(1)中间件好比一个过滤层,多个中间件就是多个过滤层,且它们有先后顺序。
(2)全局中间件可分发前执行,也可分发后执行;中间件组使用组key(如'web'、'api')来调用middleware(key)执行,中间件组仅仅是为了使一次将多个中间件指定给路由变得更加方便;路由中间件在(自定义)路由分发中或分发后执行,也是通过key(如'auth')来调用middleware(key)执行的。
(3)你也可以自定义前置或后置中间件,它们的差别在于在请求执行前还是执行后执行自定义动作。
复制代码
namespace App\Http\Middleware;
use Closure;
class AfterMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
// Perform action
return $response;
}
}
复制代码
(4)如果你想在内核 handle 和 内核 terminate 时使用同一个中间件实例,可使用容器的 singleton 方法向容器注册中间件。
发送响应并结束:由Response发送响应,然后由内核发出terminate,包括调用可终止的中间件(定义了terminate方法的全局HTTP中间件和路由中间件)、 $app 服务容器终止。
二. 服务容器和服务提供者
服务容器是 Laravel 管理类依赖和运行依赖注入的有力工具,在类中可通过 $this->app 来访问容器,在类之外通过 $app 来访问容器;服务提供者是 Laravel 应用程序引导启动的中心,关系到服务提供者自身、事件监听器、路由的启动运行。因为应用程序中注册的路由通过RouteServiceProvider实例来加载,而事件监听器在EventServiceProvider类中进行注册。在新创建的应用中,AppServiceProvider 文件中方法实现都是空的,这个提供者是你添加应用专属的引导和服务的最佳位置,当然,对于大型应用你可能希望创建几个服务提供者,每个都具有粒度更精细的引导。服务提供者在 config/app.php 配置文件中的providers数组中进行注册
复制代码
17
18 //makeVisible()用来临时修改可见性
19 return $user->makeVisible('attribute')->toArray();
复制代码
访问器和修改器:访问器(getFooAttribute)和修改器(setFooAttribute)可以让你修改 Eloquent 模型中的属性或者设置它们的值,比如你想要使用 Laravel 加密器来加密一个被保存在数据库中的值,当你从 Eloquent 模型访问该属性时该值将被自动解密。访问器和修改器要遵循cameCase命名规范,修改器会设置值到 Eloquent 模型内部的 $attributes 属性上
复制代码
1 <?php
2
3 namespace App;
4
5 use Illuminate\Database\Eloquent\Model;
6
7 class User extends Model
8 {
9 /**
10 * 获取用户的名字。
11 *
12 * @param string $value
13 * @return string
14 */
15 public function getFirstNameAttribute($value)
16 {
17 return ucfirst($value);
18 }
19
20 /**
21 * 设定用户的名字。
22 *
23 * @param string $value
24 * @return void
25 */
26 public function setFirstNameAttribute($value)
27 {
28 $this->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 <?php
2
3 namespace App;
4
5 use Illuminate\Database\Eloquent\Model;
6
7 class User extends Model
8 {
9 /**
10 * 访问器被附加到模型数组的形式。
11 *
12 * @var array
13 */
14 protected $appends = ['is_admin'];
15
16 /**
17 * 为用户获取管理者的标记。
18 *
19 * @return bool
20 */
21 public function getIsAdminAttribute()
22 {
23 return $this->attributes['admin'] == 'yes';
24 }
25 }
复制代码
属性类型转换:$casts 属性数组在模型中提供了将属性转换为常见的数据类型的方法,且键是那些需要被转换的属性名称,值则是代表字段要转换的类型。支持的转换的类型有:integer、real、float、double、string、boolean、object、array、collection、date、datetime、timestamp
复制代码
1 <?php
2
3 namespace App;
4
5 use Illuminate\Database\Eloquent\Model;
6
7 class User extends Model
8 {
9 /**
10 * 应该被转换成原生类型的属性。
11 *
12 * @var array
13 */
14 protected $casts = [
15 'is_admin' => '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转换会将模型或集合序列化成JSON
12 $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 <?php
2
3 namespace App;
4
5 use Illuminate\Database\Eloquent\Model;
6
7 class User extends Model
8 {
9 /**
10 * 获取与用户关联的电话号码
11 */
12 public function phone()
13 {
14 return $this->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 <?php
2
3 namespace App;
4
5 use Illuminate\Database\Eloquent\Model;
6
7 class Phone extends Model
8 {
9 /**
10 * 获取拥有该电话的用户模型。
11 */
12 public function user()
13 {
14 return $this->belongsTo('App\User');
15 }
16 }
复制代码
模型事件: Laravel为模型定义的事件包括creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored。 模型上定义一个 $events 属性
复制代码
1 <?php
2
3 namespace App;
4
5 use App\Events\UserSaved;
6 use App\Events\UserDeleted;
7 use Illuminate\Notifications\Notifiable;
8 use Illuminate\Foundation\Auth\User as Authenticatable;
9
10 class User extends Authenticatable
11 {
12 use Notifiable;
13
14 /**
15 * 模型的时间映射。
16 *
17 * @var array
18 */
19 protected $events = [
20 'saved' => UserSaved::class,
21 'deleted' => UserDeleted::class,
22 ];
23 }
复制代码
如果你在一个给定的模型中监听许多事件,也可使用观察者将所有监听器变成一个类,类的一个方法就是一个事件监听器
复制代码
1 定义观察者:
2 <?php
3
4 namespace App\Observers;
5
6 use App\User;
7
8 class UserObserver
9 {
10 /**
11 * 监听用户创建的事件。
12 *
13 * @param User $user
14 * @return void
15 */
16 public function created(User $user)
17 {
18 //
19 }
20
21 /**
22 * 监听用户删除事件。
23 *
24 * @param User $user
25 * @return void
26 */
27 public function deleting(User $user)
28 {
29 //
30 }
31 }
32
33 注册观察者:
34 <?php
35
36 namespace App\Providers;
37
38 use App\User;
39 use App\Observers\UserObserver;
40 use Illuminate\Support\ServiceProvider;
41
42 class AppServiceProvider extends ServiceProvider
43 {
44 /**
45 * 运行所有应用.
46 *
47 * @return void
48 */
49 public function boot()
50 {
51 User::observe(UserObserver::class);
52 }
53
54 /**
55 * 注册服务提供.
56 *
57 * @return void
58 */
59 public function register()
60 {
61 //
62 }
63 }
复制代码
八. Laravel的Restful风格
一般认为Restful风格的资源定义不包含操作,但是在Laravel中操作(动词)也可作为一种资源来定义。下图是对Laravel中资源控制器操作原理的描述,可以看到,create、edit就直接出现在了URI中,它们是一种合法的资源。对于create和edit这两种资源的访问都采用GET方法来实现,第一眼看到顿感奇怪,后来尝试通过artisan console生成资源控制器,并注意到其对create、edit给出注释“ Show the form for ”字样,方知它们只是用来展现表单而非提交表单的。
关于POST与PUT方法的差异的讨论,多认为它们都可用于创建和修改数据,主要在于POST是非幂等操作而PUT是幂等操作。
九. 扩展开发
我们知道,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 void
5 */
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 void
23 */
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 void
5 */
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 --force
复制代码
1 /**
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部分