一. 请求周期
Laravel 采用了单一入口模式,应用的所有请求入口都是 public/index.php 文件。
二. 服务容器和服务提供者
服务容器是 Laravel 管理类依赖和运行依赖注入的有力工具,在类中可通过 $this->app 来访问容器,在类之外通过 $app 来访问容器;服务提供者是 Laravel 应用程序引导启动的中心,关系到服务提供者自身、事件监听器、路由以及中间件的启动运行。应用程序中注册的路由通过RouteServiceProvider实例来加载;事件监听器在EventServiceProvider类中进行注册;中间件又称路由中间件,在app/Http/Kernel.php类文件中注册,调用时与路由进行绑定。在新创建的应用中,AppServiceProvider 文件中方法实现都是空的,这个提供者是你添加应用专属的引导和服务的最佳位置,当然,对于大型应用你可能希望创建几个服务提供者,每个都具有粒度更精细的引导。服务提供者在 config/app.php 配置文件中的providers数组中进行注册
php namespace App\Providers; use Riak\Connection; use Illuminate\Support\ServiceProvider; class RiakServiceProvider extends ServiceProvider { /** * 在容器中注册绑定 * * @return void */ public function register() { $this->app->singleton(Connection::class, function ($app) { return new Connection(config('riak')); }); } }
三. 依赖注入
Laravel 实现依赖注入方式有两种:自动注入和主动注册。自动注入通过参数类型提示由服务容器自动注入实现;主动注册则需开发人员通过绑定机制来实现,即绑定服务提供者或类(参考: http://d.laravel-china.org/docs/5.4/container )。
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'); });
php namespace App\Http\Controllers; use App\Users\Repository as UserRepository; class UserController extends Controller { /** * user repository 实例。 */ protected $users; /** * 控制器构造方法。 * * @param UserRepository $users * @return void */ public function __construct(UserRepository $users) { $this->users = $users; } /** * 储存一个新用户。 * * @param Request $request * @return Response */ public function store(Request $request) { $name = $request->input('name'); // } }
你的路由可能是这样定义的: Route::put('user/{id}', 'UserController@update'); 而控制器对路由参数id的依赖却可能是这样实现的: php namespace App\Http\Controllers; use Illuminate\Http\Request; class UserController extends Controller { /** * 更新指定的用户。 * * @param Request $request * @param string $id * @return Response */ public function update(Request $request, $id) { // } }
四. Artisan Console
Laravel利用PHP的CLI构建了强大的Console工具artisan,artisan几乎能够创建任何你想要的模板类以及管理配置你的应用,在开发和运维管理中扮演着极其重要的角色,artisan是Laravel开发不可或缺的工具。在Laravel根目录下运行:PHP artisan list可查看所有命令列表。用好artisan能极大地简化开发工作,并减少错误发生的可能;另外,还可以编写自己的命令。下面列举部分比较常用的命令:
五. 表单验证机制
表单验证在web开发中是不可或缺的,其重要性也不言而喻,也算是每个web框架的标配部件了。Laravel表单验证拥有标准且庞大的规则集,通过规则调用来完成数据验证,多个规则组合调用须以“|”符号连接,一旦验证失败将自动回退并可自动绑定视图。
下例中,附加bail规则至title属性,在第一次验证required失败后将立即停止验证;“.”语法符号在Laravel中通常表示嵌套包含关系,这个在其他语言或框架语法中也比较常见
$this->validate($request, [ 'title' => 'bail|required|unique:posts|max:255', 'author.name' => 'required', 'author.description' => 'required', ]);
Laravel验证规则参考 http://d.laravel-china.org/docs/5.4/validation#可用的验证规则 ;另外,在Laravel开发中还可采用如下扩展规则:
六. 事件机制
Laravel事件机制是一种很好的应用解耦方式,因为一个事件可以拥有多个互不依赖的监听器。事件类 (Event) 类通常保存在 app/Events
目录下,而它们的监听类 (Listener) 类被保存在 app/Listeners
目录下,使用 Artisan 命令来生成事件和监听器时他们会被自动创建。
1 php 2 3 namespace App\Events; 4 5 use App\Order; 6 use Illuminate\Queue\SerializesModels; 7 8 class OrderShipped 9 { 10 use SerializesModels; 11 12 public $order; 13 14 /** 15 * 创建一个事件实例。 16 * 17 * @param Order $order 18 * @return void 19 */ 20 public function __construct(Order $order) 21 { 22 $this->order = $order; 23 } 24 }
1 php 2 3 namespace App\Listeners; 4 5 use App\Events\OrderShipped; 6 7 class SendShipmentNotification 8 { 9 /** 10 * 创建事件监听器。 11 * 12 * @return void 13 */ 14 public function __construct() 15 { 16 // 17 } 18 19 /** 20 * 处理事件 21 * 22 * @param OrderShipped $event 23 * @return void 24 */ 25 public function handle(OrderShipped $event) 26 { 27 // 使用 $event->order 来访问 order ... 28 } 29 }
handle
方法中返回 false
来停止事件传播到其他的监听器1 php 2 3 namespace App\Http\Controllers; 4 5 use App\Order; 6 use App\Events\OrderShipped; 7 use App\Http\Controllers\Controller; 8 9 class OrderController extends Controller 10 { 11 /** 12 * 将传递过来的订单发货。 13 * 14 * @param int $orderId 15 * @return Response 16 */ 17 public function ship($orderId) 18 { 19 $order = Order::findOrFail($orderId); 20 21 // 订单的发货逻辑... 22 23 event(new OrderShipped($order)); 24 } 25 }
$connection
和 $queue
属性;如果队列监听器任务执行次数超过在工作队列中定义的最大尝试次数,监听器的 failed 方法将会被自动调用
1 php 2 3 namespace App\Listeners; 4 5 use App\Events\OrderShipped; 6 use Illuminate\Contracts\Queue\ShouldQueue; 7 8 class SendShipmentNotification implements ShouldQueue 9 { 10 /** 11 * 队列化任务使用的连接名称。 12 * 13 * @var string|null 14 */ 15 public $connection = 'sqs'; 16 17 /** 18 * 队列化任务使用的队列名称。 19 * 20 * @var string|null 21 */ 22 public $queue = 'listeners'; 23 24 public function failed(OrderShipped $event, $exception) 25 { 26 // 27 } 28 }
事件订阅者:事件订阅者允许在单个类中定义多个事件处理器,还应该定义一个 subscribe 方法,这个方法接受一个事件分发器的实例,通过调用事件分发器的 listen 方法来注册事件监听器,然后在 EventServiceProvider 类的 $subscribe 属性中注册订阅者
1 php 2 3 namespace App\Listeners; 4 5 class UserEventSubscriber 6 { 7 /** 8 * 处理用户登录事件。 9 */ 10 public function onUserLogin($event) {} 11 12 /** 13 * 处理用户注销事件。 14 */ 15 public function onUserLogout($event) {} 16 17 /** 18 * 为订阅者注册监听器。 19 * 20 * @param Illuminate\Events\Dispatcher $events 21 */ 22 public function subscribe($events) 23 { 24 $events->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 模型约定:
1 // 用属性取回航班,当结果不存在时创建它... 2 $flight = App\Flight::firstOrCreate(['name' => 'Flight 10']); 3 4 // 用属性取回航班,当结果不存在时实例化一个新实例... 5 $flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
1 php 2 3 namespace App; 4 5 use Illuminate\Database\Eloquent\Model; 6 use Illuminate\Database\Eloquent\SoftDeletes; 7 8 class Flight extends Model 9 { 10 use SoftDeletes; 11 12 /** 13 * 需要被转换成日期的属性。 14 * 15 * @var array 16 */ 17 protected $dates = ['deleted_at']; 18 }
scope
前缀)。作用域总是返回查询构建器
1 全局作用域定义: 2 php 3 4 namespace App\Scopes; 5 6 use Illuminate\Database\Eloquent\Scope; 7 use Illuminate\Database\Eloquent\Model; 8 use Illuminate\Database\Eloquent\Builder; 9 10 class AgeScope implements Scope 11 { 12 /** 13 * 应用作用域 14 * 15 * @param \Illuminate\Database\Eloquent\Builder $builder 16 * @param \Illuminate\Database\Eloquent\Model $model 17 * @return void 18 */ 19 public function apply(Builder $builder, Model $model) 20 { 21 return $builder->where('age', '>', 200); 22 } 23 } 24 25 本地作用域: 26 php 27 28 namespace App; 29 30 use Illuminate\Database\Eloquent\Model; 31 32 class User extends Model 33 { 34 /** 35 * 限制查询只包括受欢迎的用户。 36 * 37 * @return \Illuminate\Database\Eloquent\Builder 38 */ 39 public function scopePopular($query) 40 { 41 return $query->where('votes', '>', 100); 42 } 43 44 /** 45 * 限制查询只包括活跃的用户。 46 * 47 * @return \Illuminate\Database\Eloquent\Builder 48 */ 49 public function scopeActive($query) 50 { 51 return $query->where('active', 1); 52 } 53 } 54 55 动态范围: 56 php 57 58 namespace App; 59 60 use Illuminate\Database\Eloquent\Model; 61 62 class User extends Model 63 { 64 /** 65 * 限制查询只包括指定类型的用户。 66 * 67 * @return \Illuminate\Database\Eloquent\Builder 68 */ 69 public function scopeOfType($query, $type) 70 { 71 return $query->where('type', $type); 72 } 73 }
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 $visible = ['first_name', 'last_name']; 15 } 16 ?> 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;//将自动调用相应的访问器
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 }
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 });
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 ”字样,方知它们只是用来展现表单而非提交表单的。
九. 扩展开发
我们知道,Laravel本身是基于Composer管理的一个包,遵循Composer的相关规范,可以通过Composer来添加所依赖的其他Composer包,因此在做应用的扩展开发时,可以开发Composer包然后引入项目中即可;另外也可开发基于Laravel的专属扩展包。下面所讲的就是Laravel的专属扩展开发,最好的方式是使用 contracts ,而不是 facades,因为你开发的包并不能访问所有 Laravel 提供的测试辅助函数,模拟 contracts 要比模拟 facade 简单很多。
1 /** 2 * 在注册后进行服务的启动。 3 * 4 * @return void 5 */ 6 public function boot() 7 { 8 $this->loadRoutesFrom(__DIR__.'/path/to/routes.php'); 9 }
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 }
1 /** 2 * 在注册后进行服务的启动。 3 * 4 * @return void 5 */ 6 public function boot() 7 { 8 $this->loadMigrationsFrom(__DIR__.'/path/to/migrations'); 9 }
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部分