• TOC
    {:toc}

8.3 集合

8.3.1 简介

Eloquent 返回的所有的,包含多条记录的,结果集,都是 Illuminate\Database\Eloquent\Collection 对象的实例,包括通过 get 方法或者,通过访问关联关系获取的结果。Eloquent 集合对象继承自Laravel的集合基类,因此很自然的继承了很多处理 Eloquent 模型底层数组的方法。

当然,所有集合也是迭代器,允许你像数组一样对其进行循环:

1
2
3
4
5
$users = App\User::where('active', 1)->get();

foreach ($users as $user) {
echo $user->name;
}

然而,集合使用直观接口提供各种映射/简化操作,因此比数组更加强大。

例如,我们可以通过以下方式移除所有无效的模型并聚合还存在的用户的名字

1
2
3
4
5
6
7
$users = App\User::where('active', 1)->get();

$names = $users->reject(function ($user) {
return $user->active === false;
})->map(function ($user) {
return $user->name;
});

注意:尽管大多数 Eloquent 集合返回的是一个新的 Eloquent 集合实例,但是pluckkeyszipcollapseflattenflip方法返回的是集合基类实例。


8.3.2 可用方法

关于服务的可用方法,可用查看服务-合集

8.3.3 自定义集合

如果你需要在自己扩展的方法中使用自定义的集合对象,可以重写模型上的 newCollection 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App;

use App\CustomCollection;
use Illuminate\Database\Eloquent\Model;

class User extends Model{
/**
* 创建一个新的Eloquent集合实例
*
* @param array $models
* @return \Illuminate\Database\Eloquent\Collection
*/
public function newCollection(array $models = [])
{
return new CustomCollection($models);
}
}

定义好 newCollection 方法后,无论何时 Eloquent 返回该模型的 Collection 实例你都会获取到自定义的集合。

如果你想要在应用中的每一个模型中使用自定义集合,需要在模型基类中重写 newCollection 方法。

  • TOC
    {:toc}

8.2 关联关系

8.2.1 简介

数据表经常要与其它表做关联,比如一篇博客文章可能有很多评论,或者一个订单会被关联到下单用户,Eloquent 使得组织和处理这些关联关系变得简单,并且支持多种不同类型的关联关系:

  • 一对一
  • 一对多
  • 多对多
  • 远层一对多
  • 多态关联
  • 多对多的多态关联

8.2.2 定义关联关系

Eloquent 关联关系以Eloquent模型类方法的形式被定义。和 Eloquent 模型本身一样,关联关系也是强大的查询构建器,定义关联关系为函数能够提供功能强大的方法链和查询能力。

例如:

$user->posts()->where('active', 1)->get();

但是,在深入使用关联关系之前,让我们先学习如何定义每种关联类型。

8.2.2.1 一对一

一对一关联是一个非常简单的关联关系,例如,一个User模型有一个与之对应的Phone模型。要定义这种模型,我们需要将phone方法置于User模型中,phone方法应该返回 Eloquent 模型基类,上的hasOne
方法:

在User模型中,使用hasOne方法,设置photo方法的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model{
/**
* 获取关联到用户的手机
*/
public function phone()
{
return $this->hasOne('App\Phone');
}
}

传递给hasOne方法的第一个参数是关联模型的名称,关联关系被定义后,我们可以使用 Eloquent 的动态属性获取关联记录。

动态属性允许我们访问关联函数就像它们是定义在模型上的属性一样:

1
$phone = User::find(1)->phone;

Eloquent 默认关联关系的外键基于模型名称,在本例中,Phone模型默认有一个user_id外键,如果你希望重写这种约定,可以传递第二个参数到hasOne方法:

1
return $this->hasOne('App\Phone', 'foreign_key');

此外,Eloquent 假设外键应该在父级上有一个与之匹配的id,换句话说,Eloquent 将会通过user表的id值去phone表中查询user_id与之匹配的Phone记录。

如果你想要关联关系使用其他值而不是id,可以传递第三个参数到hasOne来指定自定义的主键:

1
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
  • 定义相对的关联

    我们可以从User中访问Phone模型,相应的,我们也可以在Phone模型中定义关联关系从而让我们可以拥有该phoneUser。我们可以使用belongsTo方法定义与hasOne关联关系相对的关联:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Phone extends Model{
    /**
    * 获取手机对应的用户
    */
    public function user()
    {
    return $this->belongsTo('App\User');
    }
    }

    在上面的例子中,Eloquent 将会尝试通过Phone模型的user_idUser模型查找与之匹配的记录。Eloquent 通过关联关系方法名并在方法名后加_id后缀来生成默认的外键名。然而,如果Phone模型上的外键不是user_id,也可以将自定义的键名作为第二个参数传递到belongsTo方法:

    1
    2
    3
    4
    5
    6
    /**
    * 获取手机对应的用户
    */
    public function user(){
    return $this->belongsTo('App\User', 'foreign_key');
    }

    如果父模型不使用id作为主键,或者你希望使用别的列来连接子模型,可以将父表自定义键作为第三个参数传递给belongsTo方法:

    1
    2
    3
    4
    5
    6
    /**
    * 获取手机对应的用户
    */
    public function user(){
    return $this->belongsTo('App\User', 'foreign_key', 'other_key');
    }

8.2.2.2 一对多

“一对多”是用于定义单个模型拥有多个其它模型的关联关系。

例如,一篇博客文章拥有无数评论,和其他关联关系一样,一对多关联通过在 Eloquent 模型中定义方法来定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model{
/**
* 获取博客文章的评论
*/
public function comments()
{
return $this->hasMany('App\Comment');
}
}

记住,Eloquent 会自动判断Comment模型的外键,为方便起见,Eloquent 将拥有者模型名称加上id后缀作为外键。因此,在本例中,Eloquent 假设Comment模型上的外键是post_id

关联关系被定义后,我们就可以通过访问comments属性来访问评论集合。记住,由于 Eloquent 提供“动态属性”,我们可以像访问模型的属性一样访问关联方法:

1
2
3
4
5
$comments = App\Post::find(1)->comments;

foreach ($comments as $comment) {
//
}

当然,由于所有关联同时也是查询构建器,我们可以添加更多的条件约束到通过调用comments方法获取到的评论上:

1
$comments = App\Post::find(1)->comments()->where('title', 'foo')->first();

hasOne方法一样,你还可以通过传递额外参数到hasMany方法来重新设置外键和本地主键:

1
2
return $this->hasMany('App\Comment', 'foreign_key');
return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
  • 定义相对的关联

    现在我们可以访问文章的所有评论了,接下来让我们定义一个关联关系允许通过评论访问所属文章。要定义与hasMany相对的关联关系,需要在子模型中定义一个关联方法去调用belongsTo方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Comment extends Model{
    /**
    * 获取评论对应的博客文章
    */
    public function post()
    {
    return $this->belongsTo('App\Post');
    }
    }

    关联关系定义好之后,我们可以通过访问动态属性post来获取一条Comment对应的Post

    1
    2
    $comment = App\Comment::find(1);
    echo $comment->post->title;

    在上面这个例子中,Eloquent 尝试匹配Comment模型的post_idPost模型的id,Eloquent 通过关联方法名加上_id后缀生成默认外键,当然,你也可以通过传递自定义外键名作为第二个参数传递到belongsTo方法,如果你的外键不是post_id,或者你想自定义的话:

    1
    2
    3
    4
    5
    6
    /**
    * 获取评论对应的博客文章
    */
    public function post(){
    return $this->belongsTo('App\Post', 'foreign_key');
    }

    如果你的父模型不使用id作为主键,或者你希望通过其他列来连接子模型,可以将自定义键名作为第三个参数传递给belongsTo方法:

    1
    2
    3
    4
    5
    6
    /**
    * 获取评论对应的博客文章
    */
    public function post(){
    return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
    }

8.2.2.3 多对多

多对多关系比hasOnehasMany关联关系要稍微复杂一些。

这种关联关系的一个例子就是一个用户有多个角色,同时一个角色被多个用户共用。例如,很多用户可能都有一个Admin角色。要定义这样的关联关系,需要三个数据表:usersrolesrole_userrole_user表按照关联模型名的字母顺序命名,并且包含user_idrole_id两个列。

多对多关联通过编写一个调用 Eloquent 基类上的belongsToMany方法的函数来定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model{
/**
* 用户角色
*/
public function roles()
{
return $this->belongsToMany('App\Role');
}
}

关联关系被定义之后,可以使用动态属性roles来访问用户的角色:

1
2
3
4
$user = App\User::find(1);
foreach ($user->roles as $role) {
//
}

当然,和所有其它关联关系类型一样,你可以调用roles方法来添加条件约束到关联查询上:

1
$roles = App\User::find(1)->roles()->orderBy('name')->get();

正如前面所提到的,为了决定关联关系连接表的表名,Eloquent以字母顺序连接两个关联模型的名字。然而,你可以重写这种约定——通过传递第二个参数到belongsToMany方法:

1
return $this->belongsToMany('App\Role', 'user_roles');

除了自定义连接表的表名,你还可以通过传递额外参数到belongsToMany方法来自定义该表中字段的列名。

第三个参数是你定义的关系模型的外键名称,第四个参数你要连接到的模型的外键名称:

1
return $this->belongsToMany('App\Role', 'user_roles', 'user_id', 'role_id');
  • 定义相对的关联关系

    要定义与多对多关联相对的关联关系,只需在关联模型中在调用一下belongsToMany方法即可。让我们在Role模型中定义users方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Role extends Model{
    /**
    * 角色用户
    */
    public function users()
    {
    return $this->belongsToMany('App\User');
    }
    }

    正如你所看到的,定义的关联关系和与其对应的User中定义的一模一样,只是前者引用App\Role,后者引用App\User,由于我们再次使用了belongsToMany方法,所有的常用表和键自定义选项在定义与多对多相对的关联关系时都是可用的。

  • 获取中间表的列

    正如你已经学习到的,处理多对多关联要求一个中间表。Eloquent 提供了一些有用的方法来与其进行交互。

    例如,我们假设User对象有很多与之关联的Role对象,访问这些关联关系之后,我们可以使用模型上的pivot属性访问中间表:

    1
    2
    3
    4
    5
    $user = App\User::find(1);

    foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
    }

    注意我们获取到的每一个Role模型都被自动赋上了pivot属性。该属性包含一个代表中间表的模型,并且可以像其它 Eloquent 模型一样使用。

    默认情况下,只有模型键才能用在pivot对象上,如果你的pivot表包含额外的属性,必须在定义关联关系时进行指定:

    1
    return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');

    如果你想要你的pivot表自动包含created_atupdated_at时间戳,在关联关系定义时使用withTimestamps方法:

    1
    return $this->belongsToMany('App\Role')->withTimestamps();
  • 定义中间表过滤

    你还可以对belongsToMany的返回结果使用wherePivotwherePivotIn过滤:

    1
    2
    3
    return $this->belongsToMany('App\Role')->wherePivot('approved', 1);

    return $this->belongsToMany('App\Role')->wherePivotIn('approved', [1, 2]);

8.2.2.4 远层的一对多

“远层一对多”关联为通过中间关联访问远层的关联关系提供了一个便利之道。

例如:Country模型通过中间的User模型可能拥有多个Post模型。在这个例子中,你可以轻易的找到给定国家的所有文章,让我们看看定义这个关联关系需要哪些表:

1
2
3
4
5
6
7
8
9
10
11
12
13
countries
id - integer
name - string

users
id - integer
country_id - integer
name - string

posts
id - integer
user_id - integer
title - string

尽管posts表不包含country_id列,hasManyThrough关联提供了通过$country->posts来访问一个国家的所有文章。

要执行该查询,Eloquent 在中间表$users上检查country_id,查找到相匹配的用户ID后,通过用户ID来查询posts表。

既然我们已经查看了该关联关系的数据表结构,接下来让我们在Country模型上进行定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Country extends Model{
/**
* 获取指定国家的所有文章
*/
public function posts()
{
return $this->hasManyThrough('App\Post', 'App\User');
}
}

第一个传递到hasManyThrough方法的参数是最终我们希望访问的模型的名称,第二个参数是中间模型名称。

当执行这种关联查询时通常 Eloquent 外键规则会被使用,如果你想要自定义该关联关系的外键,可以将它们作为第三个、第四个参数传递给hasManyThrough方法。第三个参数是中间模型的外键名,第四个参数是最终模型的外键名。

1
2
3
4
5
6
class Country extends Model{
public function posts()
{
return $this->hasManyThrough('App\Post', 'App\User', 'country_id', 'user_id');
}
}

8.2.2.5 多态关联

  • 表结构

多态关联允许一个模型在单个关联下属于多个不同模型。

例如,假如应用用户可以对文章点赞也可以对评论点赞,使用多态关联,你可以在这两种场景下使用单个likes表,首先,让我们看看构建这种关联关系需要的表结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
posts
id - integer
title - string
body - text

comments
id - integer
post_id - integer
body - text

likes
id - integer
likeable_id - integer
likeable_type - string

两个重要的需要注意的列是 likes 表上的 likeable_idlikeable_type

likeable_id列对应 PostCommentID 值,而 likeable_type 列对应所属模型的类名。当访问 likeable 关联时,ORM 根据 likeable_type 列来判断所属模型的类型并返回相应模型实例。

  • 模型结构

    接下来,让我们看看构建这种关联关系需要在模型中定义什么:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Like extends Model{
    /**
    * 获取所属的likeable模型
    */
    public function likeable()
    {
    return $this->morphTo();
    }
    }

    class Post extends Model{
    /**
    * 获取该文章的所有点赞
    */
    public function likes()
    {
    return $this->morphMany('App\Like', 'likeable');
    }
    }

    class Comment extends Model{
    /**
    * 获取该评论的所有点赞
    */
    public function likes()
    {
    return $this->morphMany('App\Like', 'likeable');
    }
    }
  • 获取多态关联

    数据表和模型定义好以后,可以通过模型访问关联关系。

    例如,要访问一篇文章的所有点赞,可以通过使用动态属性likes :

    1
    2
    3
    4
    5
    $post = App\Post::find(1);

    foreach ($post->likes as $like) {
    //
    }

    你还可以通过调用 morphTo 方法从多态模型中获取多态关联的所属对象。在本例中,就是 Like 模型中的 likeable 方法。因此,我们可以用动态属性的方式访问该方法:

    1
    2
    $like = App\Like::find(1);
    $likeable = $like->likeable;

    Like 模型的 likeable 关联返回 PostComment 实例,这取决于哪个类型的模型拥有该点赞。

8.2.2.6 多对多的多态关联

  • 表结构

    除了传统的多态关联,还可以定义“多对多”的多态关联,例如,一个博客的 PostVideo 模型可能共享一个 Tag 模型的多态关联。

    使用多对多的多态关联允许你在博客文章和视频之间有唯一的标签列表。

    首先,让我们看看表结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    posts
    id - integer
    name - string

    videos
    id - integer
    name - string

    tags
    id - integer
    name - string

    taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string
  • 模型结构

    接下来,我们准备在模型中定义该关联关系。PostVideo 模型都有一个 tags 方法调用 Eloquent 基类的 morphToMany 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Post extends Model{
    /**
    * 获取指定文章所有标签
    */
    public function tags()
    {
    return $this->morphToMany('App\Tag', 'taggable');
    }
    }
  • 定义相对的关联关系

    接下来,在Tag模型中,应该为每一个关联模型定义一个方法。

    例如,我们定义一个posts方法和videos方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Tag extends Model{
    /**
    * 获取所有分配该标签的文章
    */
    public function posts()
    {
    return $this->morphedByMany('App\Post', 'taggable');
    }

    /**
    * 获取分配该标签的所有视频
    */
    public function videos()
    {
    return $this->morphedByMany('App\Video', 'taggable');
    }
    }
  • 获取关联关系

    定义好数据库和模型后可以通过模型访问关联关系。

    例如,要访问一篇文章的所有标签,可以使用动态属性tags:

    1
    2
    3
    4
    5
    $post = App\Post::find(1);

    foreach ($post->tags as $tag) {
    //
    }

    还可以通过访问调用morphedByMany的方法名从多态模型中获取多态关联的所属对象。

    在本例中,就是Tag模型中的posts或者videos方法:

    1
    2
    3
    4
    5
    $tag = App\Tag::find(1);

    foreach ($tag->videos as $video) {
    //
    }

8.2.3 关联查询

由于 Eloquent 所有关联关系都是通过函数定义,你可以调用这些方法来获取关联关系的实例而不需要再去手动执行关联查询。此外,所有 Eloquent 关联关系类型同时也是查询构建器,允许你在最终在数据库执行 SQL 之前继续添加条件约束到关联查询上。

例如,假定在一个博客系统中一个 User 模型有很多相关的 Post 模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model{
/**
* 获取指定用户的所有文章
*/
public function posts()
{
return $this->hasMany('App\Post');
}
}

你可以像这样查询posts关联并添加额外的条件约束到该关联关系上:

1
2
$user = App\User::find(1);
$user->posts()->where('active', 1)->get();

8.2.3.1 关联关系方法 VS 动态属性

如果你不需要添加额外的条件约束到 Eloquent 关联查询,你可以简单通过动态属性来访问关联对象。

例如,还是拿UserPost模型作为例子,你可以像这样访问所有用户的文章:

1
2
3
4
5
$user = App\User::find(1);

foreach ($user->posts as $post) {
//
}

动态属性就是“懒惰式加载”,意味着当你真正访问它们的时候才会加载关联数据。

正因为如此,开发者经常使用渴求式加载来预加载他们知道在加载模型时要被访问的关联关系。

渴求式加载有效减少了必须要被执以加载模型关联的 SQL 查询。

8.2.3.2 查询已存在的关联关系

访问一个模型的记录的时候,你可能希望基于关联关系是否存在来限制查询结果的数目。

例如,假设你想要获取所有至少有一个评论的博客文章,要实现这个,可以传递关联关系的名称到has方法:

1
2
// 获取所有至少有一条评论的文章...
$posts = App\Post::has('comments')->get();

你还可以指定操作符和大小来自定义查询:

1
2
// 获取所有至少有三条评论的文章...
$posts = Post::has('comments', '>=', 3)->get();

还可以使用.来构造嵌套has语句。

例如,你要获取所有至少有一条评论及投票的所有文章:

1
2
// 获取所有至少有一条评论获得投票的文章...
$posts = Post::has('comments.votes')->get();

如果你需要更强大的功能,可以使用whereHasorWhereHas方法将where条件放到has查询上,这些方法允许你添加自定义条件约束到关联关系条件约束,例如检查一条评论的内容:

1
2
3
4
// 获取所有至少有一条评论包含foo字样的文章
$posts = Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();

8.2.3.3 渴求式加载

当以属性方式访问数据库关联关系的时候,关联关系数据是懒惰式加载的,这意味着关联关系数据直到第一次访问的时候才被加载。

然而,Eloquent 可以在查询父级模型的同时,渴求式加载关联关系。渴求式加载缓解了N+1查询问题,要阐明N+1查询问题,考虑下关联到AuthorBook模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model{
/**
* 获取写这本书的作者
*/
public function author()
{
return $this->belongsTo('App\Author');
}
}

现在,让我们获取所有书及其作者:

1
2
3
4
5
$books = App\Book::all();

foreach ($books as $book) {
echo $book->author->name;
}

该循环先执行1次查询获取表中的所有书,然后另一个查询获取每一本书的作者,因此,如果有25本书,要执行26次查询:1次是获取书本身,剩下的25次查询是为每一本书获取其作者。

谢天谢地,我们可以使用渴求式加载来减少该操作到2次查询。当查询的时候,可以使用with方法指定应该被渴求式加载的关联关系:

1
2
3
4
5
$books = App\Book::with('author')->get();

foreach ($books as $book) {
echo $book->author->name;
}

在该操作中,只执行两次查询即可:

1
2
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
  • 渴求式加载多个关联关系

    有时候你需要在单个操作中渴求式加载几个不同的关联关系。要实现这个,只需要添加额外的参数到with方法即可:

      $books = App\Book::with('author', 'publisher')->get();
    
  • 嵌套的渴求式加载

    要渴求式加载嵌套的关联关系,可以使用.语法。例如,让我们在一个Eloquent语句中渴求式加载所有书的作者及所有作者的个人联系方式:

      $books = App\Book::with('author.contacts')->get();
    

8.2.3.4 带条件约束的渴求式加载

有时候我们希望渴求式加载一个关联关系,但还想为渴求式加载指定更多的查询条件:

1
2
3
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();

在这个例子中,Eloquent 只渴求式加载 title 包含 first 的文章。当然,你可以调用其它查询构建器来自定义渴求式加载操作:

1
2
3
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();

8.2.3.5 懒惰渴求式加载

有时候你需要在父模型已经被获取后,渴求式加载一个关联关系。

例如,这在你需要动态决定是否加载关联模型时可能很有用:

1
2
3
4
5
$books = App\Book::all();

if ($someCondition) {
$books->load('author', 'publisher');
}

如果你需要设置更多的查询条件到渴求式加载查询上,可以传递一个闭包到load方法:

1
2
3
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);

8.2.4 插入关联模型

8.2.4.1 基本使用

  • save方法

    Eloquent 提供了便利的方法来添加新模型到关联关系。

    例如,也许你需要插入新的CommentPost模型,你可以从关联关系的save方法直接插入Comment而不是手动设置Commentpost_id属性:

    1
    2
    3
    $comment = new App\Comment(['message' => 'A new comment.']);
    $post = App\Post::find(1);
    $post->comments()->save($comment);

    注意我们没有用动态属性方式访问comments,而是调用comments方法获取关联关系实例。save方法会自动添加post_id值到新的Comment模型。

    如果你需要保存多个关联模型,可以使用saveMany方法:

    1
    2
    3
    4
    5
    6
    $post = App\Post::find(1);

    $post->comments()->saveMany([
    new App\Comment(['message' => 'A new comment.']),
    new App\Comment(['message' => 'Another comment.']),
    ]);
  • save & 多对多关联

    当处理多对多关联的时候,save方法以数组形式接收额外的中间表属性作为第二个参数:

      App\User::find(1)->roles()->save($role, ['expires' => $expires]);
    
  • create方法

    除了savesaveMany方法外,还可以使用create方法,该方法接收属性数组、创建模型、然后插入数据库。savecreate的不同之处在于save接收整个Eloquent模型实例而create接收原生PHP数组:

    1
    2
    3
    4
    5
    $post = App\Post::find(1);

    $comment = $post->comments()->create([
    'message' => 'A new comment.',
    ]);

    使用create方法之前确保先浏览属性[批量赋值]文档(http://laravelacademy.org/post/2995.html)。

8.2.4.2 更新”属于“关联

更新belongsTo关联的时候,可以使用associate方法,该方法会在子模型设置外键:

1
2
3
$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();

移除belongsTo关联的时候,可以使用dissociate方法。该方法在子模型上取消外键和关联:

1
2
$user->account()->dissociate();
$user->save();

8.2.4.3 多对多关联

  • 附加/分离

    处理多对多关联的时候,Eloquent提供了一些额外的帮助函数来使得处理关联模型变得更加方便。

    例如,让我们假定一个用户可能有多个角色同时一个角色属于多个用户,要通过在连接模型的中间表中插入记录附加角色到用户上,可以使用attach方法:

    1
    2
    $user = App\User::find(1);
    $user->roles()->attach($roleId);

    附加关联关系到模型,还可以以数组形式传递额外被插入数据到中间表:

    1
    $user->roles()->attach($roleId, ['expires' => $expires]);

    当然,有时候有必要从用户中移除角色,要移除一个多对多关联记录,使用detach方法。detach方法将会从中间表中移除相应的记录;然而,两个模型在数据库中都保持不变:

    1
    2
    3
    4
    // 从指定用户中移除角色...
    $user->roles()->detach($roleId);
    // 从指定用户移除所有角色...
    $user->roles()->detach();

    为了方便,attachdetach还接收数组形式的 ID 作为输入:

    1
    2
    3
    $user = App\User::find(1);
    $user->roles()->detach([1, 2, 3]);
    $user->roles()->attach([1 => ['expires' => $expires], 2, 3]);
  • 同步

    你还可以使用sync方法构建多对多关联。sync方法接收数组形式的ID并将其放置到中间表。任何不在该数组中的ID对应记录将会从中间表中移除。因此,该操作完成后,只有在数组中的ID对应记录还存在于中间表:

    1
    $user->roles()->sync([1, 2, 3]);

    你还可以和ID一起传递额外的中间表值:

    1
    $user->roles()->sync([1 => ['expires' => true], 2, 3]);

8.2.4.4 触发父级时间戳

当一个模型属于另外一个时,例如Comment属于Post,子模型更新时父模型的时间戳也被更新将很有用。

例如,当Comment模型被更新时,你可能想要触发创建其所属模型Postupdated_at时间戳。

Eloquent使得这项操作变得简单,只需要添加包含关联关系名称的touches属性到子模型中即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model{
/**
* 要触发的所有关联关系
*
* @var array
*/
protected $touches = ['post'];

/**
* 评论所属文章
*/
public function post()
{
return $this->belongsTo('App\Post');
}
}

现在,当你更新Comment时,所属模型Post将也会更新其updated_at值:

1
2
3
$comment = App\Comment::find(1);
$comment->text = 'Edit to this comment!';
$comment->save();

  • TOC
    {:toc}

8. Eloquent ORM

8.1 起步

8.1.1 简介

Laravel 自带的 Eloquent ORM 提供了一个美观、简单的与数据库打交道的 ActiveRecord 实现方法,每张数据表都对应一个与该表进行交互的“模型”,模型允许你在表中进行数据查询,以及插入、更新、删除等操作。

在开始之前,确保在config/database.php文件中配置好了数据库连接。更多关于数据库配置的信息,请查看文档。


8.1.2 定义模型

作为开始,让我们创建一个 Eloquent 模型,模型通常位于app目录下,你也可以将其放在其他可以被composer.json文件自动加载的地方。所有Eloquent模型都继承自 Illuminate\Database\Eloquent\Model类。

创建模型实例最简单的办法就是使用 Artisan 命令make:model

php artisan make:model User

如果你想要在生成模型时生成数据库迁移,可以使用--migration-m选项:

php artisan make:model User --migration
php artisan make:model User -m

8.1.2.1 Eloquent 模型约定

现在,让我们来看一个 Flight 模型类例子,我们将用该类获取和存取数据表flights中的信息:

1
2
3
4
5
6
7
8
9
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model{
//
}
  • 表名

    注意我们并没有告诉 Eloquent 我们的Flight模型使用哪张表。默认规则是模型类名的复数作为与其对应的表名,除非在模型类中明确指定了其它名称。

    所以,在本例中,Eloquent 认为Flight模型存储记录在flights表中。你也可以在模型中定义table属性来指定自定义的表名:

    下面例子中表明被定义为my_flights

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Flight extends Model{
    /**
    * 关联到模型的数据表
    *
    * @var string
    */
    protected $table = 'my_flights';
    }
  • 主键

    Eloquent 默认每张表的主键名为id,你可以在模型类中定义一个$primaryKey属性来覆盖该设置。

  • 时间戳

    默认情况下,Eloquent 期望created_atupdated_at已经存在于数据表中,如果你不想要这些 Laravel 自动管理的列,在模型类中设置$timestamps属性为false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Flight extends Model{
    /**
    * 表明模型是否应该被打上时间戳
    *
    * @var bool
    */
    public $timestamps = false;
    }

    如果你需要自定义时间戳格式,设置模型中的$dateFormat属性。该属性决定日期被如何存储到数据库中,以及模型被序列化为数组或 JSON 时日期的格式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Flight extends Model{
    /**
    * 模型日期列的存储格式
    *
    * @var string
    */
    protected $dateFormat = 'U';
    }
  • 数据库连接

    默认情况下,所有的 Eloquent 模型使用应用配置中的默认数据库连接,如果你想要为模型指定不同的连接,可以通过 $connection属性来设置。

    这里的Flight模型,默认连接到Flights。设置后将Flight模型连接到connection_name,这里的connection_name可以随意命名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Flight extends Model{
    /**
    * The connection name for the model.
    *
    * @var string
    */
    protected $connection = 'connection_name';
    }

8.1.3 获取多个模型

创建完模型及其关联的数据表后,就要准备从数据库中获取数据。将Eloquent模型看作功能强大的查询构建器,你可以使用它来流畅的查询与其关联的数据表。

例如:例子中将Flights表中全部数据,注入到flight.index页面中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Http\Controllers;

use App\Flight;
use App\Http\Controllers\Controller;

class FlightController extends Controller{
/**
* 显示所有有效航班列表
*
* @return Response
*/
public function index()
{
$flights = Flight::all();
return view('flight.index', ['flights' => $flights]);
}
}

8.1.3.1 访问列值

如果你有一个 Eloquent 模型实例,可以通过访问其相应的属性来访问模型的列值。

例如,让我们循环查询返回的每一个Flight实例并输出name的值:

1
2
3
foreach ($flights as $flight) {
echo $flight->name;
}

8.1.3.2 添加额外约束

Eloquent 的all方法返回模型表的所有结果,由于每一个Eloquent模型都是一个查询构建器,你还可以添加约束条件到查询,然后使用get方法获取对应结果:

1
2
3
4
$flights = App\Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();

注意:由于 Eloquent 模型本质上就是查询构建器,你可以在Eloquent查询中使用查询构建器的所有方法

8.1.3.3 集合

对 Eloquent 中获取多个结果的方法(比如allget)而言,其返回值是Illuminate\Database\Eloquent\Collection的一个实例,Collection类提供了多个有用的函数来处理Eloquent结果。

当然,你可以像操作数组一样简单循环这个集合:

1
2
3
foreach ($flights as $flight) {
echo $flight->name;
}

8.1.3.4 组块结果集

如果你需要处理成千上万个 Eloquent 结果,可以使用chunk命令。chunk方法会获取一个“组块”的 Eloquent 模型,并将其填充到给定闭包进行处理。使用chunk方法能够在处理大量数据集合时有效减少内存消耗:

1
2
3
4
5
Flight::chunk(200, function ($flights) {
foreach ($flights as $flight) {
//
}
});

传递给该方法的第一个参数是你想要获取的组块数目,闭包作为第二个参数被调用用于处理每个从数据库获取的区块数据。


8.1.4 获取单个模型/聚合

当然,除了从给定表中获取所有记录之外,还可以使用findfirst获取单个记录。这些方法返回单个模型实例而不是返回模型集合:

1
2
3
4
// 通过主键获取模型...
$flight = App\Flight::find(1);
// 获取匹配查询条件的第一个模型...
$flight = App\Flight::where('active', 1)->first();
  • Not Found 异常

有时候你可能想要在模型找不到的时候抛出异常,这在路由或控制器中非常有用,findOrFailfirstOrFail方法会获取查询到的第一个结果。

然而,如果没有任何查询结果,Illuminate\Database\Eloquent\ModelNotFoundException异常将会被抛出:

1
2
$model = App\Flight::findOrFail(1);
$model = App\Flight::where('legs', '>', 100)->firstOrFail();

如果异常没有被捕获,那么HTTP 404 响应将会被发送给用户,所以在使用这些方法的时候没有必要对返回404响应编写明确的检查:

1
2
3
Route::get('/api/flights/{id}', function ($id) {
return App\Flight::findOrFail($id);
});

8.1.4.1 获取聚合

当然,你还可以使用查询构建器聚合方法,例如countsummax,以及其它查询构建器提供的聚合方法。这些方法返回计算后的结果而不是整个模型实例:

1
2
$count = App\Flight::where('active', 1)->count();
$max = App\Flight::where('active', 1)->max('price');

8.1.5 插入/更新模型

8.1.5.1 基本插入

想要在数据库中插入新的记录,只需创建一个新的模型实例,设置模型的属性,然后调用save方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Http\Controllers;

use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class FlightController extends Controller{
/**
* 创建一个新的航班实例
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
// Validate the request...

$flight = new Flight;

$flight->name = $request->name;

$flight->save();
}
}

在这个例子中,我们只是简单分配HTTP请求中的name参数值给App\Flight模型实例的那么属性,当我们调用save方法时,一条记录将会被插入数据库。created_atupdated_at时间戳在save方法被调用时会自动被设置,所以没必要手动设置它们。

8.1.5.2 基本更新

save方法还可以用于更新数据库中已存在的模型。要更新一个模型,应该先获取它,设置你想要更新的属性,然后调用save方法。同样,updated_at时间戳会被自动更新,所以没必要手动设置其值:

1
2
3
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();

更新操作还可以同时修改给定查询提供的多个模型实例,在本例中,所有有效且destination=San Diego的航班都被标记为延迟:

1
2
3
App\Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);

update方法要求以数组形式传递键值对参数,代表着数据表中应该被更新的列。

8.1.5.3 批量赋值

还可以使用create方法保存一个新的模型。该方法返回被插入的模型实例。

但是,在此之前,你需要指定模型的fillableguarded属性,因为所有Eloquent模型都通过批量赋值(Mass Assignment)进行保护。

当用户通过 HTTP 请求传递一个不被期望的参数值时就会出现安全隐患,然后该参数以不被期望的方式修改数据库中的列值。

例如,恶意用户通过 HTTP 请求发送一个is_admin参数,然后该参数映射到模型的create方法,从而允许用户将自己变成管理员。

所以,你应该在模型中定义哪些属性是可以进行赋值的,使用模型上的$fillable属性即可实现。

例如,我们设置Flight模型上的name属性可以被赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model{
/**
* 可以被批量赋值的属性.
*
* @var array
*/
protected $fillable = ['name'];
}

设置完可以被赋值的属性之后,我们就可以使用create方法在数据库中插入一条新的记录。create方法返回保存后的模型实例:

$flight = App\Flight::create(['name' => 'Flight 10']);

$fillable就像是可以被赋值属性的“白名单”,还可以选择使用$guarded$guarded属性包含你不想被赋值的属性数组。所以不被包含在其中的属性都是可以被赋值的,因此,$guarded方法就像“黑名单”。

当然,你只能同时使用其中一个——而不是一起使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model{
/**
* 不能被批量赋值的属性
*
* @var array
*/
protected $guarded = ['price'];
}

在这个例子中,除了$price之外的所有属性都是可以被赋值的。

  • 其它创建方法

    还有其它两种可以用来创建模型的方法:firstOrCreatefirstOrNew

    firstOrCreate方法先尝试通过给定列/值对在数据库中查找记录,如果没有找到的话则通过给定属性创建一个新的记录。

    firstOrNew方法和firstOrCreate方法一样先尝试在数据库中查找匹配的记录,如果没有找到,则返回一个的模型实例。

    注意通过firstOrNew方法返回的模型实例并没有持久化到数据库中,你还需要调用save方法手动持久化:

    1
    2
    3
    4
    // 通过属性获取航班, 如果不存在则创建...
    $flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
    // 通过属性获取航班, 如果不存在初始化一个新的实例...
    $flight = App\Flight::firstOrNew(['name' => 'Flight 10']);

8.1.6 删除模型

要删除一个模型,调用模型实例上的delete方法:

1
2
$flight = App\Flight::find(1);
$flight->delete();

8.1.6.1 通过主键删除模型

在上面的例子中,我们在调用delete方法之前从数据库中获取该模型,然而,如果你知道模型的主键的话,可以调用destroy方法直接删除而不需要获取它:

下方三种方式相同。

1
2
3
App\Flight::destroy(1);
App\Flight::destroy([1, 2, 3]);
App\Flight::destroy(1, 2, 3);

8.1.6.2 通过查询删除模型

当然,你还可以通过查询删除多个模型,在本例中,我们删除所有被标记为无效的航班:

1
$deletedRows = App\Flight::where('active', 0)->delete();

8.1.6.3 软删除

除了从数据库删除记录外,Eloquent还可以对模型进行“软删除”。当模型被软删除后,它们并没有真的从数据库删除,而是在模型上设置一个deleted_at属性并插入数据库,如果模型有一个非空deleted_at值,那么该模型已经被软删除了。

要启用模型的软删除功能,可以使用模型上的Illuminate\Database\Eloquent\SoftDeletes方法并添加deleted_at列到$dates属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model{
use SoftDeletes;

/**
* 应该被调整为日期的属性
*
* @var array
*/
protected $dates = ['deleted_at'];
}

当然,应该添加deleted_at列到数据表。Laravel Schema构建器包含一个帮助函数来创建该列:

1
2
3
Schema::table('flights', function ($table) {
$table->softDeletes();
});

现在,当调用模型的delete方法时,deleted_at列将被设置为当前日期和时间,并且,当查询一个使用软删除的模型时,被软删除的模型将会自动从查询结果中排除。

判断给定模型实例是否被软删除,可以使用trashed方法:

1
2
3
if ($flight->trashed()) {
//
}

8.1.6.4 查询被软删除的模型

  • 包含软删除模型

    正如上面提到的,软删除模型将会自动从查询结果中排除,但是,如果你想要软删除模型出现在查询结果中,可以使用withTrashed方法:

    1
    2
    3
    $flights = App\Flight::withTrashed()
    ->where('account_id', 1)
    ->get();

    withTrashed方法也可以用于关联查询中:

    1
    $flight->history()->withTrashed()->get();
  • 只获取软删除模型

    onlyTrashed方法之获取软删除模型:

    1
    2
    3
    $flights = App\Flight::onlyTrashed()
    ->where('airline_id', 1)
    ->get();
  • 恢复软删除模型

    有时候你希望恢复一个被软删除的模型,可以使用restore方法:

      $flight->restore();
    

    你还可以在查询中使用restore方法来快速恢复多个模型:

    1
    2
    3
    App\Flight::withTrashed()
    ->where('airline_id', 1)
    ->restore();

    withTrashed方法一样,restore方法也可以用于关联查询:

      $flight->history()->restore();
    
  • 永久删除模型

    有时候你真的需要从数据库中删除一个模型,可以使用forceDelete方法:

    1
    2
    3
    4
    // 强制删除单个模型实例...
    $flight->forceDelete();
    // 强制删除所有关联模型...
    $flight->history()->forceDelete();

8.1.7 查询作用域

8.1.7.1 全局作用域

全局作用域允许我们为给定模型的所有查询添加条件约束。Laravel 自带的软删除功能就使用了全局作用域来从数据库中拉出所有没有被删除的模型。编写自定义的全局作用域可以提供一种方便的、简单的方式来确保给定模型的每个查询都有特定的条件约束。

  • 编写全局作用域

    自定义全局作用域很简单,首先定义一个实现 Illuminate\Database\Eloquent\Scope 接口的类,该接口要求你实现一个方法:apply。需要的话可以在 apply 方法中添加 where 条件到查询:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?php

    namespace App\Scopes;

    use Illuminate\Database\Eloquent\Scope;
    use Illuminate\Database\Eloquent\Model;
    use Illuminate\Database\Eloquent\Builder;

    class AgeScope implements Scope{
    /**
    * Apply the scope to a given Eloquent query builder.
    *
    * @param \Illuminate\Database\Eloquent\Builder $builder
    * @param \Illuminate\Database\Eloquent\Model $model
    * @return void
    */
    public function apply(Builder $builder, Model $model)
    {
    return $builder->where('age', '>', 200);
    }
    }
  • 应用全局作用域

    要将全局作用域分配给模型,需要重写给定模型的 boot 方法并使用 addGlobalScope 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?php

    namespace App;

    use App\Scopes\AgeScope;
    use Illuminate\Database\Eloquent\Model;

    class User extends Model{
    /**
    * The "booting" method of the model.
    *
    * @return void
    */
    protected static function boot()
    {
    parent::boot();

    static::addGlobalScope(new AgeScope);
    }
    }

    添加作用域后,如果使用 User::all() 查询则会生成如下SQL语句:

      select * from `users` where `age` > 200
    
  • 匿名的全局作用域

Eloquent还允许我们使用闭包定义全局作用域,这在实现简单作用域的时候特别有用,这样的话,我们就没必要定义一个单独的类了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class User extends Model{
/**
* The "booting" method of the model.
*
* @return void
*/
protected static function boot()
{
parent::boot();

static::addGlobalScope('age', function(Builder $builder) {
$builder->where('age', '>', 200);
});
}
}

我们还可以通过以下方式移除全局作用:

1
User::withoutGlobalScope('age')->get();
  • 移除全局作用域

如果想要在给定查询中移除指定全局作用域,可以使用 withoutGlobalScope

1
User::withoutGlobalScope(AgeScope::class)->get();

如果你想要移除某几个或全部全局作用域,可以使用 withoutGlobalScopes 方法:

1
2
User::withoutGlobalScopes()->get();
User::withoutGlobalScopes([FirstScope::class, SecondScope::class])->get();

8.1.7.2 本地作用域

本地作用域允许我们定义通用的约束集合以便在应用中复用。

例如,你可能经常需要获取最受欢迎的用户,要定义这样的一个作用域,只需简单在对应Eloquent模型方法前加上一个scope前缀,作用域总是返回查询构建器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model{
/**
* 只包含活跃用户的查询作用域
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}

/**
* 只包含激活用户的查询作用域
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('active', 1);
}
}
  • 使用查询作用域

    作用域被定义好了之后,就可以在查询模型的时候调用作用域方法,但调用时不需要加上scope前缀,你甚至可以在同时调用多个作用域,例如:

      $users = App\User::popular()->active()->orderBy('created_at')->get();
    
  • 动态作用域

    有时候你可能想要定义一个可以接收参数的作用域,你只需要将额外的参数添加到你的作用域即可。作用域参数应该被定义在$query参数之后:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class User extends Model{
    /**
    * 只包含给用类型用户的查询作用域
    *
    * @return \Illuminate\Database\Eloquent\Builder
    */
    public function scopeOfType($query, $type)
    {
    return $query->where('type', $type);
    }
    }

    现在,你可以在调用作用域时传递参数了:

      $users = App\User::ofType('admin')->get();
    

8.1.8 事件

Eloquent模型可以触发事件,允许你在模型生命周期中的多个时间点调用如下这些方法:creating, created, updating, updated, saving, saved,deleting, deleted, restoring, restored。事件允许你在一个指定模型类每次保存或更新的时候执行代码。

8.1.8.1 基本使用

一个新模型被首次保存的时候,creatingcreated事件会被触发。

如果一个模型已经在数据库中存在并调用save方法,updating / updated事件会被触发,无论是创建还是更新,saving / saved事件都会被调用。

举个例子,我们在服务提供者中定义一个Eloquent事件监听器,在事件监听器中,我们会调用给定模型的isValid方法,如果模型无效会返回false。如果从Eloquent事件监听器中返回false则取消save/update操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace App\Providers;

use App\User;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider{
/**
* 启动所有应用服务
*
* @return void
*/
public function boot()
{
User::creating(function ($user) {
if ( ! $user->isValid()) {
return false;
}
});
}

/**
* 注册服务提供者.
*
* @return void
*/
public function register()
{
//
}
}

  • TOC
    {:toc}

7.4 填充数据

7.4.1 简介

Laravel 包含了一个简单方法来填充数据库——使用填充类和测试数据。

所有的填充类都位于database/seeds目录。

填充类的类名完全由你自定义,但最好还是遵循一定的规则,比如可读性,例如UserTableSeeder等等。

安装完 Laravel 后,会默认提供一个DatabaseSeeder类。

从这个类中,你可以使用call方法来运行其他填充类,从而允许你控制填充顺序。


7.4.2 编写填充器

要生成一个填充器,可以通过 Artisan 命令make:seeder。所有框架生成的填充器都位于database/seeders目录:

php artisan make:seeder UserTableSeeder

一个填充器类默认只包含一个方法:run。当Artisan命令db:seed运行时该方法被调用。在run方法中,可以插入任何你想插入数据库的数据,你可以使用查询构建器手动插入数据,也可以使用 Eloquent 模型工厂。

举个例子,让我们修改 Laravel 安装时自带的DatabaseSeeder类,添加一个数据库插入语句到run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder{
/**
* 运行数据库填充
*
* @return void
*/
public function run()
{
DB::table('users')->insert([
'name' => str_random(10),
'email' => str_random(10).'@gmail.com',
'password' => bcrypt('secret'),
]);
}
}

7.4.2.1 使用模型工厂

当然,手动指定每一个模型填充的属性是很笨重累赘的,取而代之的,我们可以使用模型工厂来方便的生成大量的数据库记录。首先,查看模型工厂文档来学习如何定义工厂,定义工厂后,可以使用帮助函数factory来插入记录到数据库。

举个例子,让我们创建50个用户并添加关联关系到每个用户:

1
2
3
4
5
6
7
8
9
10
/**
* 运行数据库填充
*
* @return void
*/
public function run(){
factory('App\User', 50)->create()->each(function($u) {
$u->posts()->save(factory('App\Post')->make());
});
}

7.4.2.2 调用额外的填充器

DatabaseSeeder类中,你可以使用call方法执行额外的填充类,使用call方法允许你将数据库填充分解成多个文件,这样单个填充器类就不会变得无比巨大,只需简单将你想要运行的填充器类名传递过去即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 运行数据库填充
*
* @return void
*/
public function run(){
Model::unguard();

$this->call(UserTableSeeder::class);
$this->call(PostsTableSeeder::class);
$this->call(CommentsTableSeeder::class);

Model::reguard();
}

7.4.3 运行填充器

编写好填充器类之后,可以使用 Artisan 命令db:seed来填充数据库。默认情况下,db:seed命令运行可以用来运行其它填充器类的DatabaseSeeder类,但是,你也可以使用--class 选项来指定你想要运行的独立的填充器类:

1
2
php artisan db:seed
php artisan db:seed --class=UserTableSeeder

你还可以使用migrate:refresh命令来填充数据库,该命令还可以回滚并重新运行迁移,这在需要完全重建数据库时很有用:

php artisan migrate:refresh --seed

  • TOC
    {:toc}

7.3 迁移

7.3.1 简介

迁移就像数据库的版本控制,允许团队简单轻松的编辑并共享应用的数据库表结构,迁移通常搭配 Laravel 结构构建器,可以很容易地构建应用的数据库表结构。

Laravel的Schema门面提供了与数据库系统无关的创建和操纵表的支持,在 Laravel 所支持的所有数据库系统中提供一致的、优雅的、平滑的API。


7.3.2 生成迁移

使用 Artisan 命令make:migration来创建一个新的迁移:

php artisan make:migration create_users_table

新的迁移位于database/migrations目录下,每个迁移文件名都包含时间戳从而允许 Laravel 判断其顺序。

--table--create选项可以用于指定表名以及该迁移是否要创建一个新的数据表。这些选项只需要简单放在上述迁移命令后面并指定表名:

php artisan make:migration add_votes_to_users_table --table=users
php artisan make:migration create_users_table --create=users

如果你想要指定生成迁移的自定义输出路径,在执行make:migration命令时可以使用--path选项,提供的路径应该是相对于应用根目录的。


7.3.3 迁移结构

迁移类包含了两个方法:updown

up方法用于新增表,列或者索引到数据库,而down方法就是up方法的反操作,和up里的操作相反。

在这两个方法中你都要用到 Laravel 的表结构构建器来创建和修改表.

例如:让我们先看看创建flights表的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateFlightsTable extends Migration{
/**
* 运行迁移
*
* @return void
*/
public function up()
{
Schema::create('flights', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('airline');
$table->timestamps();
});
}

/**
* 撤销迁移
*
* @return void
*/
public function down()
{
Schema::drop('flights');
}
}

7.3.4 运行迁移

要运行应用中所有未执行的迁移,可以使用 Artisan 命令的migrate方法。

如果你正在使用Homestead虚拟机,应该在你的虚拟机中运行如下这条命令:

php artisan migrate

如果再运行时遇到“class not found”的错误提示,尝试运行composer dump-autoload命令然后重新运行迁移命令。

  • 在生产环境中强制运行迁移

    有些迁移操作是毁灭性的,这意味着它们可能造成数据的丢失,为了避免在生产环境数据库中运行这些命令,你将会在运行这些命令之前被提示并确认。想要强制运行这些命令而不被提示,可以使用--force

      php artisan migrate --force
    

7.3.4.1 回滚迁移

想要回滚最新的一次迁移操作,可以使用rollback命令,注意这将会回滚最后一批运行的迁移,可能包含多个迁移文件:

php artisan migrate:rollback

migrate:reset命令将会回滚所有的应用迁移:

php artisan migrate:reset
  • 在单个命令中回滚/迁移 migrate:refresh命令将会先回滚所有数据库迁移,然后运行migrate命令。这个命令可以有效的重建整个数据库:
      php artisan migrate:refresh
      php artisan migrate:refresh --seed
    

7.3.5 编写迁移

7.3.5.1 创建表

使用Schema门面上的create方法来创建新的数据表。create方法接收两个参数,第一个是表名,第二个是获取用于定义新表的Blueprint对象的闭包:

1
2
3
Schema::create('users', function ($table) {
$table->increments('id');
});

当然,创建新表的时候,可以使用表结构构建器中的任意列方法来定义数据表的列。

  • 检查表/列是否存在

    你可以轻松地使用 hasTablehasColumn 方法检查表或列是否存在:

    1
    2
    3
    4
    5
    6
    7
    if (Schema::hasTable('users')) {
    //
    }

    if (Schema::hasColumn('users', 'email')) {
    //
    }
  • 连接&存储引擎

    如果你想要在一个数据库连接上执行表结构操作,该数据库连接并不是默认数据库连接,使用connection方法:

    1
    2
    3
    Schema::connection('foo')->create('users', function ($table) {
    $table->increments('id');
    });

    要设置表的存储引擎,在表结构构建器上设置engine属性:

    1
    2
    3
    4
    Schema::create('users', function ($table) {
    $table->engine = 'InnoDB';
    $table->increments('id');
    });

7.3.5.2 重命名/删除表

要重命名一个已存在的数据表,使用rename方法:

Schema::rename($from, $to);

要删除一个已存在的数据表,可以使用dropdropIfExists方法:

1
2
Schema::drop('users');
Schema::dropIfExists('users');

7.3.5.3 创建列

要更新一个已存在的表,使用Schema门面上的table方法,和create方法一样,table方法接收两个参数:表名和获取用于添加列到表的Blueprint实例的闭包:

1
2
3
Schema::table('users', function ($table) {
$table->string('email');
});
  • 可用的列类型

当然,表结构构建器包含一系列你可以用来构建表的列类型:

命令 描述
$table->bigIncrements(‘id’); 自增ID,类型为bigint
$table->bigInteger(‘votes’); 等同于数据库中的BIGINT类型
$table->binary(‘data’); 等同于数据库中的BLOB类型
$table->boolean(‘confirmed’); 等同于数据库中的BOOLEAN类型
$table->char(‘name’, 4); 等同于数据库中的CHAR类型
$table->date(‘created_at’); 等同于数据库中的DATE类型
$table->dateTime(‘created_at’); 等同于数据库中的DATETIME类型
$table->decimal(‘amount’, 5, 2); 等同于数据库中的DECIMAL类型,带一个精度和范围
$table->double(‘column’, 15, 8); 等同于数据库中的DOUBLE类型,带精度, 总共15位数字,小数点后8位.
$table->enum(‘choices’, [‘foo’, ‘bar’]); 等同于数据库中的 ENUM类型
$table->float(‘amount’); 等同于数据库中的 FLOAT 类型
$table->increments(‘id’); 数据库主键自增ID
$table->integer(‘votes’); 等同于数据库中的 INTEGER 类型
$table->json(‘options’); 等同于数据库中的 JSON 类型
$table->jsonb(‘options’); 等同于数据库中的 JSONB 类型
$table->longText(‘description’); 等同于数据库中的 LONGTEXT 类型
$table->mediumInteger(‘numbers’); 等同于数据库中的 MEDIUMINT类型
$table->mediumText(‘description’); 等同于数据库中的 MEDIUMTEXT类型
$table->morphs(‘taggable’); 添加一个 INTEGER类型的 taggable_id 列和一个 STRING类型的 taggable_type
$table->nullableTimestamps(); timestamps()一样但允许 NULL值.
$table->rememberToken(); 添加一个 remember_token 列: VARCHAR(100) NULL.
$table->smallInteger(‘votes’); 等同于数据库中的 SMALLINT 类型
$table->softDeletes(); 新增一个 deleted_at 列 用于软删除.
$table->string(‘email’); 等同于数据库中的 VARCHAR 列 .
$table->string(‘name’, 100); 等同于数据库中的 VARCHAR,带一个长度
$table->text(‘description’); 等同于数据库中的 TEXT 类型
$table->time(‘sunrise’); 等同于数据库中的 TIME类型
$table->tinyInteger(‘numbers’); 等同于数据库中的 TINYINT 类型
$table->timestamp(‘added_on’); 等同于数据库中的 TIMESTAMP 类型
$table->timestamps(); 添加 created_atupdated_at列.
$table->uuid(‘id’); 等同于数据库的UUID

7.3.5.4 列修改器

除了上面列出的列类型之外,在添加列的时候还可以使用一些其它列“修改器”。

例如:要使列默认为null,可以使用nullable方法:

1
2
3
Schema::table('users', function ($table) {
$table->string('email')->nullable();
});

下面是所有可用的列修改器列表,该列表不包含索引修改器:

修改器 描述
->first() 将该列置为表中第一个列 (仅适用于MySQL)
->after(‘column’) 将该列置于另一个列之后 (仅适用于MySQL)
->nullable() 允许该列的值为NULL
->default($value) 指定列的默认值
->unsigned() 设置 integer 列为 UNSIGNED

7.3.5.5 修改列

  • 先决条件

    在修改列之前,确保已经将doctrine/dbal依赖添加到composer.json文件,Doctrine DBAL 库用于判断列的当前状态并在需要时创建SQL查询来对列进行指定的调整。

  • 更新列属性

    change方法允许你修改已存在的列为新的类型,或者修改列的属性。例如,你可能想要增加 string 类型列的尺寸,让我们将name列的尺寸从 25 增加到 50:

    1
    2
    3
    Schema::table('users', function ($table) {
    $table->string('name', 50)->change();
    });

    我们还可以修改该列允许 NULL 值:

    1
    2
    3
    Schema::table('users', function ($table) {
    $table->string('name', 50)->nullable()->change();
    });
  • 重命名列

要重命名一个列,可以使用表结构构建器上的renameColumn方法,在重命名一个列之前,确保doctrine/dbal依赖已经添加到composer.json文件:

1
2
3
Schema::table('users', function ($table) {
$table->renameColumn('from', 'to');
});

注意:暂不支持 enum类型的列的重命名。

7.3.5.6 删除列

要删除一个列,使用表结构构建器上的dropColumn方法:

1
2
3
Schema::table('users', function ($table) {
$table->dropColumn('votes');
});

你可以传递列名数组到dropColumn方法从表中删除多个列:

1
2
3
Schema::table('users', function ($table) {
$table->dropColumn(['votes', 'avatar', 'location']);
});

注意:在从SQLite数据库删除列之前,需要添加doctrine/dbal依赖到composer.json文件并在终端中运行composer update命令来安装该库。此外,SQLite数据库暂不支持在单个迁移中删除或修改多个列。

7.3.5.7 创建索引

表结构构建器支持多种类型的索引,首先,让我们看一个指定列值为唯一索引的例子。要创建索引,可以使用unique方法:

$table->string('email')->unique();

此外,你可以在定义列之后创建索引,例如:

$table->unique('email');

你甚至可以传递列名数组到索引方法来创建组合索引:

$table->index(['account_id', 'created_at']);

Laravel 会自动生成合理的索引名称,但是你可以传递第二个参数到该方法用于指定索引名称:

$table->index('email', 'my_index_name');
  • 可用索引类型

    命令 描述
    $table->primary(‘id’); 添加主键索引
    $table->primary([‘first’, ‘last’]); 添加混合索引
    $table->unique(‘email’); 添加唯一索引
    $table->unique(‘state’, ‘my_index_name’); 指定自定义索引名称
    $table->index(‘state’); 添加普通索引

7.3.5.8 删除索引

要删除索引,必须指定索引名。默认情况下,Laravel 自动分配适当的名称给索引——简单连接表名、列名和索引类型。下面是一些例子:

命令 描述
$table->dropPrimary(‘users_id_primary’); 从 “users”表中删除主键索引
$table->dropUnique(‘users_email_unique’); 从 “users”表中删除唯一索引
$table->dropIndex(‘geo_state_index’); 从 “geo”表中删除普通索引

7.3.5.9 外键约束

Laravel 还提供了创建外键约束的支持,用于在数据库层面强制引用完整性。例如,我们在posts表中定义了一个引用users表的id列的user_id列:

1
2
3
4
Schema::table('posts', function ($table) {
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users');
});

你还可以为约束的on deleteon update属性指定期望的动作:

1
2
3
$table->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');

要删除一个外键,可以使用dropForeign方法。外键约束和索引使用同样的命名规则——连接表名、外键名然后加上_foreign后缀:

$table->dropForeign('posts_user_id_foreign');

  • TOC
    {:toc}

7.2 查询构建器

7.2.1 简介

数据库查询构建器提供了一个方便的、平滑的接口来创建和运行数据库查询查询构建器可以用于执行应用中大部分数据库操作,并且能够在支持的所有数据库系统上工作。

注意:Laravel 查询构建器使用 PDO 参数绑定来避免 SQL 注入攻击,不再需要过滤传递到绑定的字符串。


7.2.2 获取结果集

7.2.2.1 从一张表中取出所有行

在查询之前,使用DB门面table方法,table方法为给定表返回一个查询构建器,允许你在查询上链接更多约束条件并最终返回查询结果。在本例中,我们使用get方法获取表中所有记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Http\Controllers;

use DB;
use App\Http\Controllers\Controller;

class UserController extends Controller{
/**
* 显示用户列表
*
* @return Response
*/
public function index()
{
$users = DB::table('users')->get();

return view('user.index', ['users' => $users]);
}
}

和原生查询一样,get方法返回结果集的数组,其中每一个结果都是PHP对象的StdClass实例。你可以像访问对象的属性一样访问列的值:

1
2
3
foreach ($users as $user) {
echo $user->name;
}

7.2.2.2 从一张表中获取一行/一列

如果你只是想要从数据表中获取一行数据,可以使用first方法,该方法将会返回单个StdClass对象:

1
2
$user = DB::table('users')->where('name', 'John')->first();
echo $user->name;

如果你不需要完整的一行,可以使用value方法从结果中获取单个值,该方法会直接返回指定列的值:

$email = DB::table('users')->where('name', 'John')->value('email');

7.2.2.3 从一张表中获取组块结果集

如果你需要处理成千上百条数据库记录,可以考虑使用chunk方法,该方法一次获取结果集的一小块,然后填充每一小块数据到要处理的闭包,该方法在编写处理大量数据库记录的 Artisan 命令的时候非常有用。

例如:我们可以将处理全部 users 表数据,处理成一次处理 100 条记录的小组块:

1
2
3
4
5
DB::table('users')->chunk(100, function($users) {
foreach ($users as $user) {
//
}
});

你可以通过从闭包函数中返回false来中止组块的运行:

1
2
3
4
DB::table('users')->chunk(100, function($users) {
// 处理结果集...
return false;
});

7.2.2.4 获取数据列值列表

7.2.2.5 聚合函数

队列构建器还提供了很多聚合方法,比如count, max, min, avg, 和 sum,你可以在构造查询之后调用这些方法:

1
2
$users = DB::table('users')->count();
$price = DB::table('orders')->max('price');

当然,你可以联合其它查询子句和聚合函数来构建查询:

1
2
3
$price = DB::table('orders')
->where('finalized', 1)
->avg('price');

7.2.3 查询(Select)

7.2.3.1 指定查询子句

当然,我们并不总是想要获取数据表的所有列,使用select方法,你可以为查询指定自定义的select子句:

1
$users = DB::table('users')->select('name', 'email as user_email')->get();

distinct方法允许你强制查询返回不重复的结果集:

1
$users = DB::table('users')->distinct()->get();

如果你已经有了一个查询构建器实例并且希望添加一个查询列到已存在的select子句,可以使用addSelect方法:

1
2
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();

7.2.3.2 原生表达式

有时候你希望在查询中使用原生表达式,这些表达式将会以字符串的形式注入到查询中,所以要格外小心避免被 SQL 注入。想要创建一个原生表达式,可以使用DB::raw方法:

1
2
3
4
5
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();

7.2.4 连接(Join)

7.2.4.1 内连接(等值连接)

查询构建器还可以用于编写基本的SQL“内连接”,你可以使用查询构建器实例上的join方法,传递给join方法的第一次参数是你需要连接到的表名,剩余的其它参数则是为连接指定的列约束,当然,正如你所看到的,你可以在单个查询中连接多张表:

1
2
3
4
5
$users = DB::table('users')
->join('contacts', 'users.id', '=', 'contacts.user_id')
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.*', 'contacts.phone', 'orders.price')
->get();

7.2.4.2 左连接

如果你是想要执行“左连接”而不是“内连接”,可以使用leftJoin方法。该方法和join方法的使用方法一样:

1
2
3
$users = DB::table('users')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->get();

7.2.4.3 高级连接语句

你还可以指定更多的高级连接子句,传递一个闭包到join方法作为该方法的第2个参数,该闭包将会返回允许你指定join子句约束的JoinClause对象:

1
2
3
4
5
DB::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(...);
})
->get();

如果你想要在连接中使用“where”风格的子句,可以在查询中使用whereorWhere方法。这些方法将会将列和值进行比较而不是列和列进行比较:

1
2
3
4
5
6
DB::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')
->where('contacts.user_id', '>', 5);
})
->get();

7.2.5 联合(Union)

查询构建器还提供了一条“联合”两个查询的快捷方式.

例如:你要创建一个独立的查询,然后使用union方法将其和第二个查询进行联合:

1
2
3
4
5
6
7
$first = DB::table('users')
->whereNull('first_name');

$users = DB::table('users')
->whereNull('last_name')
->union($first)
->get();

unionAll方法也是有效的,并且和union有同样的使用方法。


7.2.6 Where子句

7.2.6.1 简单where子句

使用查询构建器上的where方法可以添加where子句到查询中,调用where最基本的方法需要三个参数,第一个参数是列名,第二个参数是一个数据库系统支持的任意操作符,第三个参数是该列要比较的值。

例如,下面是一个验证votes列的值是否等于100的查询:

1
$users = DB::table('users')->where('votes', '=', 100)->get();

为了方便,如果你只是简单比较列值和给定数值是否相等,可以将数值直接作为where方法的第二个参数:

1
$users = DB::table('users')->where('votes', 100)->get();

当然,你可以使用其它操作符来编写where子句:

1
2
3
4
5
6
7
8
9
10
11
$users = DB::table('users')
->where('votes', '>=', 100)
->get();

$users = DB::table('users')
->where('votes', '<>', 100)
->get();

$users = DB::table('users')
->where('name', 'like', 'T%')
->get();

7.2.6.2 or

你可以通过方法链将多个where约束链接到一起,也可以添加or子句到查询,orWhere方法和where方法接收参数一样:

1
2
3
4
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'John')
->get();

7.2.6.3 更多Where子句

  • whereBetween

    whereBetween方法验证列值是否在给定值之间:

    1
    2
    $users = DB::table('users')
    ->whereBetween('votes', [1, 100])->get();
  • whereNotBetween

    whereNotBetween方法验证列值不在给定值之间:

    1
    2
    3
    $users = DB::table('users')
    ->whereNotBetween('votes', [1, 100])
    ->get();
  • whereIn/whereNotIn

    whereIn方法验证给定列的值是否在给定数组中:

    1
    2
    3
    $users = DB::table('users')
    ->whereIn('id', [1, 2, 3])
    ->get();

    whereNotIn方法验证给定列的值不在给定数组中:

    1
    2
    3
    $users = DB::table('users')
    ->whereNotIn('id', [1, 2, 3])
    ->get();
  • whereNull/whereNotNull

    whereNull方法验证给定列的值为NULL:

    1
    2
    3
    $users = DB::table('users')
    ->whereNull('updated_at')
    ->get();

    whereNotNull方法验证给定列的值不是NULL:

    1
    2
    3
    $users = DB::table('users')
    ->whereNotNull('updated_at')
    ->get();

7.2.6.4 高级Where子句

  • 参数分组

    有时候你需要创建更加高级的where子句,比如where exists或者嵌套的参数分组。Laravel查询构建器也可以处理这些。作为开始,让我们看一个在括号中进行分组约束的例子:

    1
    2
    3
    4
    5
    6
    7
    DB::table('users')
    ->where('name', '=', 'John')
    ->orWhere(function ($query) {
    $query->where('votes', '>', 100)
    ->where('title', '<>', 'Admin');
    })
    ->get();

    正如你所看到的,传递闭包到orWhere方法构造查询构建器来开始一个约束分组,该闭包将会获取一个用于设置括号中包含的约束的查询构建器实例。上述语句等价于下面的SQL:

    1
    select * from users where name = 'John' or (votes > 100 and title <> 'Admin')
  • exists语句

    whereExists方法允许你编写where existSQL子句,whereExists方法接收一个闭包参数,该闭包获取一个查询构建器实例从而允许你定义放置在exists子句中的查询:

    1
    2
    3
    4
    5
    6
    7
    DB::table('users')
    ->whereExists(function ($query) {
    $query->select(DB::raw(1))
    ->from('orders')
    ->whereRaw('orders.user_id = users.id');
    })
    ->get();

    上述查询等价于下面的SQL语句:

    1
    2
    3
    4
    select * from users
    where exists (
    select 1 from orders where orders.user_id = users.id
    )

7.2.7 排序、分组、限定

7.2.7.1 orderBy

orderBy方法允许你通过给定列对结果集进行排序,orderBy的第一个参数应该是你希望排序的列,第二个参数控制着排序的方向——asc(正序)desc(倒序)

1
2
3
$users = DB::table('users')
->orderBy('name', 'desc')
->get();

7.2.7.2 groupBy / having / havingRaw

groupByhaving方法用于对结果集进行分组,having方法和where方法的用法类似:

1
2
3
4
$users = DB::table('users')
->groupBy('account_id')
->having('account_id', '>', 100)
->get();

havingRaw方法可以用于设置原生字符串作为having子句的值,例如,我们要找到所有售价大于$2500的部分:

1
2
3
4
5
$users = DB::table('orders')
->select('department', DB::raw('SUM(price) as total_sales'))
->groupBy('department')
->havingRaw('SUM(price) > 2500')
->get();

7.2.7.3 skip / take

想要限定查询返回的结果集的数目,或者在查询中跳过给定数目的结果,可以使用skiptake方法:

1
$users = DB::table('users')->skip(10)->take(5)->get();

7.2.8 插入(Insert)

查询构建器还提供了insert方法来插入记录到数据表。insert方法接收数组形式的列名和值进行插入操作:

1
2
DB::table('users')->insert(
['email' => '[email protected]', 'votes' => 0]);

你甚至可以一次性通过传入多个数组来插入多条记录,每个数组代表要插入数据表的记录:

1
2
3
4
DB::table('users')->insert([
['email' => '[email protected]', 'votes' => 0],
['email' => '[email protected]', 'votes' => 0]
]);

7.2.8.1 自增ID

如果数据表有自增ID,使用insertGetId方法来插入记录将会返回ID值:

1
2
3
$id = DB::table('users')->insertGetId(
['email' => '[email protected]', 'votes' => 0]
);

注意:当使用PostgresSQL时,insertGetId方法默认自增列被命名为id,如果你想要从其他序列获取ID,可以将序列名作为第二个参数传递到insertGetId方法。


7.2.9 更新(Update)

当然,除了插入记录到数据库,查询构建器还可以通过使用update方法更新已有记录。update方法和insert方法一样,接收列和值的键值对数组包含要更新的列,你可以通过where子句来对update查询进行约束:

1
2
3
DB::table('users')
->where('id', 1)
->update(['votes' => 1]);

7.2.9.1 增加/减少

查询构建器还提供了方便增减给定列名数值的方法。相较于编写update语句,这是一条捷径,提供了更好的体验和测试接口。

这两个方法都至少接收一个参数:需要修改的列。第二个参数是可选的,用于控制列值增加/减少的数目。

1
2
3
4
DB::table('users')->increment('votes');
DB::table('users')->increment('votes', 5);
DB::table('users')->decrement('votes');
DB::table('users')->decrement('votes', 5);

在操作过程中你还可以指定额外的列进行更新:

1
DB::table('users')->increment('votes', 1, ['name' => 'John']);

7.2.10 删除(Delete)

当然,查询构建器还可以通过delete方法从表中删除记录:

1
DB::table('users')->delete();

在调用delete方法之前可以通过添加where子句对delete语句进行约束:

1
DB::table('users')->where('votes', '<', 100)->delete();

如果你希望清除整张表,也就是删除所有列并将自增ID置为0,可以使用truncate方法:

1
DB::table('users')->truncate();

7.2.11 悲观锁

查询构建器还包含一些方法帮助你在select语句中实现悲观锁。可以在查询中使用sharedLock方法从而在运行语句时带一把共享锁。共享锁可以避免,被选择的行被修改直到事务提交:

1
DB::table('users')->where('votes', '>', 100)->sharedLock()->get();

此外你还可以使用lockForUpdate方法。for update锁避免选择行被其它共享锁修改或删除:

1
DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();

  • TOC
    {:toc}

7 数据库

7.1 起步

7.1.1 简介

Laravel 让连接多种数据库以及对数据库进行查询变得非常简单,不论使用原生 SQL、还是查询构建器,还是 Eloquent ORM。目前,Laravel 支持四种类型的数据库系统:

  • MySQL
  • Postgres
  • SQLite
  • SQL Server

7.1.1.1 配置

Laravel 让连接数据库和运行查询都变得非常简单。应用的数据库配置位于config/database.php。在该文件中你可以定义所有的数据库连接,并指定哪个连接是默认连接。该文件中提供了所有支持数据库系统的配置示例。

默认情况下,Laravel 示例环境配置已经为 Laravel Homestead 做好了设置,当然,你也可以按照需要为本地的数据库修改该配置。

  • 读/写连接

有时候你希望使用一个数据库连接做查询,另一个数据库连接做插入、更新和删除,Laravel 使得这件事情轻而易举,不管你用的是原生 SQL,还是查询构建器,还是 Eloquent ORM,合适的连接总是会被使用。

想要知道如何配置读/写连接,让我们看看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'mysql' => [
'read' => [
'host' => '192.168.1.1',
],
'write' => [
'host' => '196.168.1.2'
],
'driver' => 'mysql',
'database' => 'database',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
],

注意我们在配置数组中新增了两个键:read和write,这两个键都对应一个包含单个键“host”的数组,读/写连接的其它数据库配置选项都共用 mysql 的主数组配置。

如果我们想要覆盖主数组中的配置,只需要将相应配置项放到readwrite数组中即可。在本例中,192.168.1.1将被用作“读”连接,而192.168.1.2将被用作“写”连接。两个数据库连接的凭证(用户名/密码)、前缀、字符集以及其它配置将会共享mysql数组中的设置。


7.1.2 运行原生 SQL 查询

配置好数据库连接后,就可以使用DB门面来运行查询。DB门面为每种查询提供了相应方法:select, update, insert, delete, 和statement

7.1.2.1 运行 Select 查询

运行一个最基本的查询,可以使用DB门面select方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Http\Controllers;

use DB;
use App\Http\Controllers\Controller;

class UserController extends Controller{
/**
* 显示用户列表
*
* @return Response
*/
public function index()
{
$users = DB::select('select * from users where active = ?', [1]);
return view('user.index', ['users' => $users]);
}
}

传递给select方法的第一个参数是原生的SQL语句,第二个参数需要绑定到查询的参数绑定,通常,这些都是where字句约束中的值。参数绑定可以避免SQL注入攻击。

select方法以数组的形式返回结果,数组中的每一个结果都是一个PHP StdClass对象,从而允许你像下面这样访问结果值:

1
2
3
foreach ($users as $user) {
echo $user->name;
}

7.1.2.2 使用命名绑定

除了使用?占位符来代表参数绑定外,还可以使用命名绑定来执行查询:

1
$results = DB::select('select * from users where id = :id', ['id' => 1]);

7.1.2.3 运行插入语句

使用DB门面insert方法执行插入语句。和select一样,改方法将原生SQL语句作为第一个参数,将绑定作为第二个参数:

1
$results = DB::insert('insert into users (id, name) values (?, ?)', [1, 'Dayle']);

7.1.2.4 运行更新语句

update方法用于更新数据库中已存在的记录,该方法返回受更新语句影响的行数:

1
$affected = DB::update('update users set votes = 100 where name = ?', ['John']);

7.1.2.5 运行删除语句

delete方法用于删除数据库中已存在的记录,和update一样,该语句返回被删除的行数:

1
$deleted = DB::delete('delete from users');

7.1.2.6 运行一个通用语句

有些数据库语句不返回任何值,对于这种类型的操作,可以使用DB门面statement方法:

1
DB::statement('drop table users');

7.1.2.7 监听查询事件

如果你想要获取应用中每次 SQL 语句的执行,可以使用listen方法,该方法对查询日志和调试非常有用,你可以在服务提供者中注册查询监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace App\Providers;

use DB;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider{
/**
* 启动所有应用服务
*
* @return void
*/
public function boot()
{
DB::listen(function($query) {
// $query->sql
// $query->bindings
// $query->time
});
}

/**
* 注册服务提供者
*
* @return void
*/
public function register()
{
//
}
}

7.1.3 数据库事务

想要在一个数据库事务中运行一连串操作,可以使用DB门面transaction方法,如果事务闭包中抛出异常,事务将会自动回滚。

如果闭包执行成功,事务将会自动提交。使用transaction方法时不需要担心手动回滚或提交:

1
2
3
4
DB::transaction(function () {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
});

7.1.3.1 手动使用事务

如果你想要手动开始事务从而对回滚和提交有一个完整的控制,可以使用DB门面beginTransaction方法:

DB::beginTransaction();

你可以通过rollBack方法回滚事务:

DB::rollBack();

最后,可以通过commit方法提交事务:

DB::commit();

注意:使用DB门面的事务方法还可以用于控制查询构建器和 Eloquent ORM 的事务。


7.1.4 使用多个数据库连接

使用多个数据库连接的时候,可以使用DB门面connection方法访问每个连接。传递给connection方法的连接名对应配置文件config/database.php中相应的连接:

$users = DB::connection('foo')->select(...);

你还可以通过连接实例上的getPdo方法底层原生的 PDO 实例:

$pdo = DB::connection()->getPdo();

  • TOC
    {:toc}

6.6 合约

6.6.1 简介

Laravel的合约是一组由框架(framework)提供的核心服务接口。

例如:

Illuminate\Contracts\Queue\Queue合约规定,列队工作需要的方法。

Illuminate\Contracts\Mail\Mailer合约规定,发送电子邮件的方法。

每个合约都有框架提供的,相应的实现方法。

例如:

Laravel提供一个列队实现方法与各种驱动,和一个由SwiftMailer提供的邮件接收服务。

所有的Laravel合约都在各自的github仓库中更新。这为所有的合约,提供了一个快速参考点,以及一个单一的分离包,用于包开发。

Github参考点

6.6.1.1 合约Vs.门面

Laravel的门面提供一种简单的方法,利用Laravel服务无须输入和提示。这好于使用需要服务容器的合约方法。

但是,使用合约运行你定义依赖类。多余大多数应用,使用门面就好了。

但是,如果你真有的需要额外的松耦合,合约就可以提供,阅读下面的了解更多。

6.6.2 为什么用合约?

关于合约你可能有几个问题要问。为什么全部使用接口?不使用接口会更复杂?

下方是使用接口的重要原因:松耦合简洁性

6.6.2.1 松耦合

首先,让我们来看一个使用紧耦合缓存实现方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace App\Orders;

class Repository
{
/**
* The cache instance.
*/
protected $cache;

/**
* Create a new repository instance.
*
* @param \SomePackage\Cache\Memcached $cache
* @return void
*/
public function __construct(\SomePackage\Cache\Memcached $cache)
{
$this->cache = $cache;
}

/**
* Retrieve an Order by ID.
*
* @param int $id
* @return Order
*/
public function find($id)
{
if ($this->cache->has($id)) {
//
}
}
}

这这个类中,代码是使用紧耦合,到给定的缓存实现方法。

因为是从一个包中取得缓存,所以这是一个紧耦合。

如果这个包的API发生了改变,我们的代码也要随之改变。

同样的,如果我们要使用其他技术(例如:redis),取代正在使用的技术(Memcached),我们还是要修改代码。

我们的类不应该用这些设定,例如,谁为他们提供数据,它是如何提供的等等。

使用下方松耦合方法,不需要绑定注入,进而优化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Orders;

use Illuminate\Contracts\Cache\Repository as Cache;

class Repository
{
/**
* The cache instance.
*/
protected $cache;

/**
* Create a new repository instance.
*
* @param Cache $cache
* @return void
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}

现在代码没有连接任何的服务提供者,甚至Laravel。由于合约包中没有实现方法也没有依赖关系,你可以很容易的替换合约,更换缓存技术,而无需修改代码。

6.6.2.2 简洁性

当所有的Laravel的服务,在简单接口中整洁的定义,就可以很容易的给定服务提供的功能。

合约服务为框架功能提了简洁的文档。

当你使用简单接口时,你的代码更容易理解和维护。

6.6.3 合约参考

合约 参考的门面
Illuminate\Contracts\Auth\Factory Auth
Illuminate\Contracts\Auth\PasswordBroker Password
Illuminate\Contracts\Bus\Dispatcher Bus
Illuminate\Contracts\Broadcasting\Broadcaster
Illuminate\Contracts\Cache\Repository Cache
Illuminate\Contracts\Cache\Factory Cache::driver()
Illuminate\Contracts\Config\Repository Config
Illuminate\Contracts\Container\Container App
Illuminate\Contracts\Cookie\Factory Cookie
Illuminate\Contracts\Cookie\QueueingFactory Cookie::queue()
Illuminate\Contracts\Encryption\Encrypter Crypt
Illuminate\Contracts\Events\Dispatcher Event
Illuminate\Contracts\Filesystem\Cloud
Illuminate\Contracts\Filesystem\Factory File
Illuminate\Contracts\Filesystem\Filesystem File
Illuminate\Contracts\Foundation\Application App
Illuminate\Contracts\Hashing\Hasher Hash
Illuminate\Contracts\Logging\Log Log
Illuminate\Contracts\Mail\MailQueue Mail::queue()
Illuminate\Contracts\Mail\Mailer Mail
Illuminate\Contracts\Queue\Factory Queue::driver()
Illuminate\Contracts\Queue\Queue Queue
Illuminate\Contracts\Redis\Database Redis
Illuminate\Contracts\Routing\Registrar Route
Illuminate\Contracts\Routing\ResponseFactory Response
Illuminate\Contracts\Routing\UrlGenerator URL
Illuminate\Contracts\Support\Arrayable
Illuminate\Contracts\Support\Jsonable
Illuminate\Contracts\Support\Renderable
Illuminate\Contracts\Validation\Factory Validator::make()
Illuminate\Contracts\Validation\Validator
Illuminate\Contracts\View\Factory View::make()
Illuminate\Contracts\View\View

6.6.4 如何使用合约

那么,如果使用一个合约的实现方法?这其实很简单。

许多类型的Laravel类,是通过服务容器解决的,包括控制器,事件监听,中间件,列队甚至是路由。

要使用合约,只需要进行类型提示,注册类的接口函数。

例如:下面的事件监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace App\Listeners;

use App\User;
use App\Events\NewUserRegistered;
use Illuminate\Contracts\Redis\Database;

class CacheUserInformation
{
/**
* The Redis database implementation.
*/
protected $redis;

/**
* Create a new event handler instance.
*
* @param Database $redis
* @return void
*/
public function __construct(Database $redis)
{
$this->redis = $redis;
}

/**
* Handle the event.
*
* @param NewUserRegistered $event
* @return void
*/
public function handle(NewUserRegistered $event)
{
//
}
}

当事件监听器运行完成,服务容器将读取构造函数的类型提示,并注入相应的值。

  • TOC
    {:toc}

6.5 门面

6.5.1 简介

Facades为应用在服务容器中的绑定类,提供一个静态接口。

Laravel附带许多facades,你可以在不知道的情况下正在使用它们。

Laravel的门面作为服务容器中底层类的静态代理,相比于传统静态方法,在维护时能够提供更加易于测试,简明的语法。


6.5.2 使用门面

在Laravel应用的上下文中,门面是一个提供访问容器中对象的类。该机制原理由Facade类实现,Laravel自带的门面,以及创建的自定义门面,都会继承自Illuminate\Support\Facades\Facade基类。

门面类只需要实现一个方法:getFacadeAccessorgetFacadeAccessor 方法定义了从容器中解析什么,然后 Facade 基类使用魔术方法 __callStatic() 从你的门面中调用解析对象。

下面的例子中,将会调用Laravel的缓存系统,浏览代码后,也许你会觉得我们调用了Cache的静态方法get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Http\Controllers;

use Cache;
use App\Http\Controllers\Controller;

class UserController extends Controller{
/**
* 为指定用户显示属性
*
* @param int $id
* @return Response
*/
public function showProfile($id)
{
$user = Cache::get('user:'.$id);

return view('profile', ['user' => $user]);
}
}

注意我们在顶部位置引入Cache门面。该门面作为代理访问底层Illuminate\Contracts\Cache\Factory接口的实现方法。我们对门面的所有调用都会被传递给Laravel缓存服务的底层实例。

如果我们查看Illuminate\Support\Facades\Cache类的源码,将会发现其中并没有静态方法get:

1
2
3
4
5
6
7
8
9
10
class Cache extends Facade{
/**
* 获取组件注册名称
*
* @return string
*/
protected static function getFacadeAccessor() {
return 'cache';
}
}

Cache门面继承Facade基类并定义了getFacadeAccessor方法,该方法的工作就是返回服务容器绑定类的别名,当用户引用Cache类的任何静态方法时,Laravel从服务容器中解析Cache绑定,然后在解析出的对象上调用所有请求方法(本例中是get)。


6.5.3 门面类列表

下面列出了每个门面及其对应的底层类,这对深入给定门面的API文档而言是个很有用的工具。服务容器绑定键也被包含进来:

门面 服务器绑定别名
App Illuminate\Foundation\Application app
Artisan Illuminate\Console\Application artisan
Auth Illuminate\Auth\AuthManager auth
Blade Illuminate\View\Compilers\BladeCompiler blade.compiler
Bus Illuminate\Contracts\Bus\Dispatcher
Cache Illuminate\Cache\Repository cache
Config Illuminate\Config\Repository config
Cookie Illuminate\Cookie\CookieJar cookie
Crypt Illuminate\Encryption\Encrypter encrypter
DB Illuminate\Database\DatabaseManager db
DB (Instance) Illuminate\Database\Connection
Event Illuminate\Events\Dispatcher events
File Illuminate\Filesystem\Filesystem files
Gate Illuminate\Contracts\Auth\Access\Gate
Hash Illuminate\Contracts\Hashing\Hasher hash
Lang Illuminate\Translation\Translator translator
Log Illuminate\Log\Writer log
Mail Illuminate\Mail\Mailer mailer
Password Illuminate\Auth\Passwords\PasswordBroker auth.password
Queue Illuminate\Queue\QueueManager queue
Queue (Instance) Illuminate\Contracts\Queue\Queue queue
Queue (Base Class) Illuminate\Queue\Queue
Redirect Illuminate\Routing\Redirector redirect
Redis Illuminate\Redis\Database redis
Request Illuminate\Http\Request request
Response Illuminate\Contracts\Routing\ResponseFactory
Route Illuminate\Routing\Router router
Schema Illuminate\Database\Schema\Blueprint
Session Illuminate\Session\SessionManager session
Session (Instance) Illuminate\Session\Store
Storage Illuminate\Contracts\Filesystem\Factory filesystem
URL Illuminate\Routing\UrlGenerator url
Validator Illuminate\Validation\Factory validator
Validator (Instance) Illuminate\Validation\Validator
View Illuminate\View\Factory view
View (Instance) Illuminate\View\View

  • TOC
    {:toc}

6.4 服务容器

6.4.1 简介

Laravel 服务容器是一个用于管理类依赖和执行依赖注入的强大工具。依赖注入听上去很花哨,其实质是通过构造函数或者某些情况下通过 setter 方法将类依赖注入到类中。

让我们看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace App\Jobs;

use App\User;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Contracts\Bus\SelfHandling;

class PurchasePodcast implements SelfHandling
{
/**
* mailer 的實作。
*/
protected $mailer;

/**
* 建立一個新實例。
*
* @param Mailer $mailer
* @return void
*/
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}

/**
* 購買 podcast。
*
* @return void
*/
public function handle()
{
//
}
}

在这个例子中,购买podcast时,PurchasePodcast任务会需要寄送e-mails。因此,将注入能寄送e-mails的服务。由于服务被注入,我们能容易地切换成其它实例。当测试应用程序时,我们能很容易的测试,或者建立假的实例。

深入了解laravel服务容器对构建大型laravel应用很重要,对于贡献代码到laravel核心也很有帮助(很少需要这样做。)


6.4.2 绑定

几乎所有的服务容器绑定都是在服务提供者中注册。所以下方所有的例子将示范在该情况中使用容器。不过,如果类别没有任何依赖,那么就没有将类绑定到容器的必要。并不是需要告诉容器如果构建这些物件,因为它会透PHP的reflection自动解析对象

在一个服务提供者中,可以通过 $this->app 变量访问容器,然后使用 bind方法注册一个绑定,该方法需要两个参数,第一个参数是我们想要注册的类名或接口名称,第二个参数是返回类的实例的闭包:

1
2
3
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app['HttpClient']);
});

注意,我们接受容器本身作为解析器的一个参数,然后我们可以使用该容器,来解析我们正在构建的对象的子依赖。

  • 绑定一个单例

    singleton 方法绑定,一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:

    1
    2
    3
    $this->app->singleton('FooBar', function ($app) {
    return new FooBar($app['SomethingElse']);
    });
  • 绑定实例

    你还可以使用 instance 方法绑定一个已存在的对象实例到容器,随后对容器的调用将总是返回给定的实例:

    1
    2
    3
    $fooBar = new FooBar(new SomethingElse);

    $this->app->instance('FooBar', $fooBar);

6.4.2.1 绑定接口到实现方法

服务容器的一个非常强大的特性是,其绑定接口到实现方法的能力。

我们假设有一个 EventPusher 接口及其 RedisEventPusher 实现方法,编写完该接口的 RedisEventPusher 实现方法后,就可以将其注册到服务容器:

1
$this->app->bind('App\Contracts\EventPusher', 'App\Services\RedisEventPusher');

这段代码告诉容器当一个类需要 EventPusher 的实现方法将会注入 RedisEventPusher,现在我们可以在构造器或者任何其它通过服务容器注入依赖的地方进行 EventPusher 接口的类型提示:

1
2
3
4
5
6
7
8
9
10
11
use App\Contracts\EventPusher;

/**
* 创建一个新的类实例
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher){
$this->pusher = $pusher;
}

6.4.2.2 上下文绑定

有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现方法。

例如,当系统接到一个新的订单的时候,我们想要通过PubNub而不是 Pusher 发送一个事件。Laravel 定义了一个简单、平滑的方式来定义这种行为:

1
2
3
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
->needs('App\Contracts\EventPusher')
->give('App\Services\PubNubEventPusher');

你甚至还可以传递一个闭包到 give 方法:

1
2
3
4
5
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
->needs('App\Contracts\EventPusher')
->give(function () {
// Resolve dependency...
});
  • 绑定原始值

    有时候你可能有一个获取若干注入类的类,但还需要一个注入的原始值,比如整型数据,你可以轻松使用上下文绑定来注入指定类所需要的任何值:

    1
    2
    3
    $this->app->when('App\Handlers\Commands\CreateOrderHandler')
    ->needs('$maxOrderCount')
    ->give(10);

6.4.2.2 标签

有些情况下,需要解析特定分类下的所有绑定。

例如:你正在构建一个报表规整器,要通过接收有多个不同Report接口数组实现,在注册完 Report 实现方法之后,可以通过tag方法给它们分配一个标签:

SpeedReportMemoryReport打上名为reports的标签。

1
2
3
4
5
6
7
8
9
$this->app->bind('SpeedReport', function () {
//
});

$this->app->bind('MemoryReport', function () {
//
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

这些服务被打上标签后,可以通过 tagged 方法来轻松解析它们:

1
2
3
	$this->app->bind('ReportAggregator', function ($app) {
return new ReportAggregator($app->tagged('reports'));
});

6.4.3 解析

有很多方式可以从容器中解析对象,首先,你可以使用 make 方法,该方法接收你想要解析的类名或接口名作为参数:

$fooBar = $this->app->make('FooBar');

其次,你可以以数组方式访问容器,因为其实现了 PHP 的 ArrayAccess 接口:

$fooBar = $this->app['FooBar'];

最后,也是最常用的,你可以简单的通过,在类的构造函数中,对依赖进行类型提示,来从容器中解析对象.包括控制器、事件监听器、队列任务、中间件等都是通过这种方式。在实践中,这是大多数对象从容器中解析的方式。

容器会自动为其解析类注入依赖.

例如:你可以在控制器的构造函数中为应用定义的仓库进行类型提示,该仓库会自动解析并注入该类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Controller;
use App\Users\Repository as UserRepository;

class UserController extends Controller{
/**
* 用户仓库实例
*/
protected $users;

/**
* 创建一个控制器实例
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}

/**
* 通过指定ID显示用户
*
* @param int $id
* @return Response
*/
public function show($id)
{
//
}
}

6.4.4 容器事件

服务容器在每一次解析对象时都会触发一个事件,可以使用 resolving 方法监听该事件:

1
2
3
4
5
6
7
$this->app->resolving(function ($object, $app) {
// 容器解析所有类型对象时调用
});

$this->app->resolving(function (FooBar $fooBar, $app) {
// 容器解析“FooBar”对象时调用
});

正如你所看到的,被解析的对象将会传递给回调,从而允许你在对象被传递给消费者之前为其设置额外属性。


0%