附录1:Yii2.0 对比 Yii1.1 的重大改进

这部分内容是专门为已经有Yii1.1基础的读者朋友写的。将Yii2.0与Yii1.1的不同点着重写出来,对比学起来会快得多。 而对于从未接触过Yii的读者朋友,这部分内容扫一扫就可以了,作为对过往历史的一个了解就够了。 如果有的内容你一时没看明白,也不要紧,本书的正文部分会讲清楚的。 另外,没有Yii1.1的经验,并不妨碍对Yii2.0的学习。

Yii官方有专门的文档归纳总结1.1版本和2.0版本的不同。以下内容,主要来自于官方的文档,我做了下精简, 选择比较重要的变化,并加入了一些个人的经验。

PHP新特性

从对PHP新特性的使用上,两者就存在很大不同。Yii2.0大量使用了PHP的新特性,这在Yii1.1中是没有的。因此,Yii2.0对于PHP的版本要求更高,要求PHP5.4及以上。Yii2.0中使用到的PHP新特性,主要有:

  • 命名空间(Namespace)
  • 匿名函数
  • 数组短语法形式: [1,2,3] 取代 array(1,2,3) 。这在多维数组、嵌套数组中,代码更清晰、简短。
  • 在视图文件中使用PHP的 <?= 标签,取代 echo 语句。
  • 标准PHP库(SPL) 类和接口,具体可以查看 SPL Class and Interface
  • 延迟静态绑定, 具体可以查看 Late Static Bindings
  • PHP标准日期时间
  • 特性(Traits)
  • 使用PHP intl 扩展实现国际化支持, 具体可以查看 PECL init

了解Yii2.0使用了PHP的新特性,可以避免开发时由于环境不当,特别是开发生产环境切换时,产生莫名其妙的错误。 同时,也是让读者朋友借机学习PHP新知识的意思。

命名空间(Namespace)

Yii2.0与Yii1.1之间最显著的不同是对于PHP命名空间的使用。Yii1.1中没有命名空间一说, 为避免Yii核心类与用户自定义类的命名冲突,所有的Yii核心类的命名,均冠以 C 前缀,以示区别。

而Yii2.0中所有核心类都使用了命名空间,因此, C 前缀也就人老珠黄,退出历史舞台了。

命名空间与实际路径相关联,比如 yii\base\Object 对应Yii目录下的 base/Object.php 文件。

基础类

Yii1.1中使用了一个基础类 CComponent ,提供了属性支持等基本功能,因此几乎所有的Yii核心类都派生自该类。 到了Yii2.0,将一家独大的 CComponent 进行了拆分。拆分成了 yii\base\Objectyii\base\Component 。 拆分的考虑主要是 CComponent 尾大不掉,有影响性能之嫌。 于是,Yii2.0中,把 yii\base\Object 定位于只需要属性支持,无需事件、行为。 而 yii\base\Component 则在前者的基础上,加入对于事件和行为的支持。 这样,开发者可以根据需要,选择继承自哪基础类。

这一功能上的明确划分,带来了效率上的提升。在仅表示基础数据结构,而非反映客观事物的情况下, yii\base\Object 比较适用。

值得一提的是, yii\base\Objectyii\base\Component 两者并不是同一层级的,前者是后者他爹。

事件(Event)

在Yii1.1中,通过一个 on 前缀的方法来创建事件,比如 CActiveRecord 中的 onBeforeSave() 。 在Yii2.0中,可以任意定义事件的名称,并自己触发它:

1
2
3
4
5
6
7
8
$event = new \yii\base\Event;
// 使用 trigger() 触发事件
$component->trigger($eventName, $event);

// 使用 on() 前事件handler与对象绑定
$component->on($eventName, $handler);
// 使用 off() 解除绑定
$component->off($eventName, $handler);

别名(Alias)

Yii2.0中改变了Yii1.1中别名的使用形式,并扩大了别名的范畴。 Yii1.1中,别名以 . 的形式使用:

RootAlias.path.to.target

而在Yii2.0中,别名以 @ 前缀的方式使用:

@yii/jui

另外,Yii2.0中,不仅有路径别名,还有URL别名:

1
2
3
4
5
// 路径别名
Yii::setAlias('@foo', '/path/to/foo');

// URL别名
Yii::setAlias('@bar', 'http://www.example.com');

别名与命名空间是紧密相关的,Yii建议为所有根命名空间都定义一个别名,比如上面提到的 yii\base\Object , 事实上是定义了 @yii 的别名,表示Yii在系统中的安装路径。 这样一来,Yii就能根据命名空间找到实际的类文件所在路径,并自动加载。这一点上,Yii2.0与Yii1.1并没有本质区别。

而如果没有为根命名空间定义别名,则需要进行额外的配置。将命名空间与实际路径的映射关系,告知Yii。

关于别名的更详细内容请看 别名(Alias)

视图(View)

Yii1.1中,MVC(model-view-controller)中的视图一直是依赖于Controller的,并非是真正意义上独立的View。 Yii2.0引入了 yii\web\View 类,使得View完全独立。这也是一个相当重要变化。

首先,Yii2.0中,View作为Application的一个组件,可以在全局中代码中进行访问。 因此,视图渲染代码不必再局限于Controller中或Widget中。

其次,Yii1.1中视图文件中的 $this 指的是当前控制器,而在 Yii2.0中,指的是视图本身。 要在视图中访问控制器,可以使用 $this->context 。这个 $this->context 是指谁调用了 yii\base\View::renderFile() 来渲染这个视图。 一般是某个控制器,也可以是其他实现了 yii\base\ViewContextInterface 接口的对象。

同时,Yii1.1中的 CClientScript 也被淘汰了,相关的前端资源及其依赖关系的管理,交由Assert Bundle yii\web\AssertBundle 来专职处理。 一个Assert Bundle代表一系列的前端资源,这些前端资源以目录形式进行管理,这样显得更有序。 更为重要的是,Yii1.1中需要你格外注意资源在HTML中的顺序,比如CSS文件的顺序(后面的会覆盖前面的), JavaScript文件的顺序(前后顺序出错会导致有的库未加载)等。 而在Yii2.0中,使用一个Assert Bundle可以定义依赖于另外的一个或多个Assert Bundle的关系, 这样在向HTML页面注册这些CSS或者JavaScript时,Yii2.0会自动把所依赖的文件先注册上。

在视图模版引擎方面,Yii2.0仍然使用PHP作为主要的模版语言。 同时官方提供了两个扩展以支持当前两大主流PHP模版引擎:Smarty和Twig,而对于Pardo引擎官方不再提供支持了。 当然,开发者可以通过设置 yii\web\View::$renderers 来使用其他模版。

另外,Yii1.1中,调用 $this->render('viewFile', ...) 是不需要使用 echo 命令的。 而Yii2.0中,记得 render() 只是返回视图渲染结果,并不对直接显示出来,需要自己调用 echo

echo $this->render('_item', ['item' => $item]);

如果有一天你发现怎么Yii输出了个空白页给你,就要注意是不是忘记使用 echo 了。 还别说,这个错误很常见,特别是在对Ajax请求作出响应时,会更难发现这一错误。请你们编程时留意。

在视图的主题(Theme)化方面,Yii2.0的运作机理采用了完全不同的方式。 在Yii2.0中,使用路径映射的方式,将一个源视图文件路径,映射成一个主题化后的视图文件路径。 因此, ['/web/views' => '/web/themes/basic'] 定义了一个主题映射关系, 源视图文件 /web/views/site/index.php 主题化后将是 /web/themes/basic/site/index.php 。 因此, Yii1.1中的 CThemeManager 也被淘汰了。

模型(Model)

MVC中的M指的就是模型,Yii1.1中使用 CModel 来表示,而Yii2.0使用 yii\base\Model 来表示。

Yii1.1中, CFormModel 用来表示用户的表单输入,以区别于数据库中的表。 这在Yii2.0中也被淘汰,Yii2.0倾向于使用继承自 yii\base\Model 来表示提交的表单数据。

另外,Yii2.0为Model引入了 yii\base\Model::load()yii\base\Model::loadMutiple() 两个新的方法, 用于简化将用户输入的表单数据赋值给Model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Yii2.0使用load()等同于下面Yii1.1的用法
$model = new Post;
if ($model->load($_POST)) {
    ... ...
}

// Yii1.1中常用的套路
if (isset($_POST['Post'])) {
    $model->attributes = $_POST['Post'];
}

另外一个重要变化就是Yii2.0中改变了Model应用于不同场景的逻辑。通过引入 yii\base\Model::scenarios() 来集中管理场景,使得一个Model所有适用的场景都比较清晰,一目了然。而Yii1.1是没有一个统一管理场景的方法的。

由此带来的一个很容易出现的问题就是,当你声明一个Model处于某一场景时,可能由于拼写错误, 不小心将场景的名称写错了,那么在Yii1.1中,这个错误的场景并没有任何的提示。假设有以下情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class UserForm extends CFormModel
{
    public $username;
    public $email;
    public $password;
    public $password_repeat;
    public $rememberMe=false;

    public function rules()
    {
        return array(
            // username 和 password 在所有场景中都要验证
            array('username, password', 'required'),

            // email 和 password_repeat 只在注册场景中验证
            array('email, password_repeat', 'required', 'on'=>'Registration'),
            array('email', 'email', 'on'=>'Registration'),

            // rememberMe 仅在登陆场景中验证
            array('rememberMe', 'boolean', 'on'=>'Login'),
        );
    }
}

这里针对UserForm的注册和登陆两个场景,设定了不同的验证规则。接下来,你要在注册场景中使用这个UserForm, 但你一不小心将 Registration 场景设定成了 SignUp , 说实在,我不是学英文出身的,这两个单词的意思在我眼里是一样一样的。只是Yii不会智能到把这两个场景等同起来。 那么Yii1.1将不会有任何的提示,并自动地使用第一个验证规则,而用户注册时填写的 emailpassword_repeat 字段就被抛弃了。这在实际编程中,是经常出现的一个低级错误。

从这里可以看到,Yii1.1中对于场景,没有一个集中统一的管理,也就是说一个Model可适用的场景, 是不确定的、任意的。通过 rules() 你很难一眼看出来一个Model可以适用于多少个场景,每个场景下都有哪些字段是有效的、需要验证的。

而在Yii2.0中,由于引入了 yii\base\Model::scenarios() 新的方法, 将本Model所有适用的场景,及不同场景下的有效字段都进行了声明, 这个逻辑就显得清晰了。而且,如果使用了一个未声明的场景,Yii2.0会有相应的提示, 这避免了上面这个低级错误的可能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    public function scenarios()
    {
        return [
            'login' => ['username', 'password', 'rememberMe'],
            'registration' => ['username', 'email', 'password', 'password_repeat'],
        ];
    }
}

这样看来,是不是很清晰?这个User仅有两种场景,每种场景的有效字段也一目了然。 而至于具体场景下每个字段的验证规则,仍然由 yii\base\Model::rules() 来确定。 这也意味着, unsafe 验证在Yii2.0中也没有了立足之地,凡是 unsafe 的字段,就不在特定的场景中列出来。 或者为了更加明显的表示某一字段在特定场景下是无效的,可以给这个字段加上 ! 前缀。

在默认情况下, yii\base\Model::scenarios() 所有适用的场景和对应的字段由 yii\base\Model::rules() 的内容自动生成。也就是说,如果你的 rules() 很完备、很清晰,那么也是不需要重载这个 scenarios() 的。 这种情况下,Yii1.1和Yii2.0在这一点上的表现形式,是一样的。但是,个人经验看, 我更倾向于将 scenarios() 声明清楚,而在 rules() 中,仅指定字段的验证规则,而不涉及场景的内容。 这样的逻辑更加清晰,便于其他团队成员阅读你的代码,也便于后续的维护和开发。

控制器(Controller)

除了上面讲到的控制器中要使用 echo 来显示渲染视图的输出这点区别外, Yii1.1与Yii2.0的控制器还表现出更为明显的区别,那就是动作过滤器(Action Filter) 的不同。

在Yii2.0中,动作过滤器以行为(behavior)的方式出现, 一般继承自 yii\base\ActionFilter ,并注入到一个控制器中,以发生作用。比如,Yii1.1中很常见的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public function behaviors()
{
    return [
        'access' => [
            'class' => 'yii\filters\AccessControl',
            'rules' => [
                ['allow' => true, 'actions' => ['admin'], 'roles' => ['@']],
            ],
        ],
    ];
}

看着是不是有点像,但又确实不一样?

Active Record

还记得么?在Yii1.1中,数据库查询被分散成 CDbCommandCDbCriteriaCDbCommandBuilder 。 所谓天下大势分久必合,到了Yii2.0,采用 yii\db\Query 来表示数据库查询:

1
2
3
4
5
6
7
8
$query = new \yii\db\Query();
$query->select('id, name')
      ->from('user')
      ->limit(10);

$command = $query->createCommand();
$sql = $command->sql;
$rows = $command->queryAll();

最最最爽的是, yii\db\Query 可以在 Active Record中使用,而在Yii1.1中,要结合两者,并不容易。

Active Record在Yii2.0中最大的变化一个是查询的构建,另一个是关联查询的处理。

Yii1.1中的 CDbCriteria 在Yii2.0中被 yii\db\ActiveQuery 所取代, 这个把前辈拍死在沙滩上的家伙,继承自 yii\db\Query ,所以可以进行类似上面代码的查询。 调用 yii\db\ActiveRecord::find() 就可以启动查询的构建了:

$customers = Customer::find()
    ->where(['status' => $active])
    ->orderBy('id')
    ->all();

这在Yii1.1中,是不容易实现的。特别是比较复杂的查询关系。

在关联查询方面,Yii1.1是在一个统一的地方 relations() 定义关联关系。 而Yii2.0改变了这一做法,定义一个关联关系:

  • 定义一个getter方法
  • getter方法的方法名表示关联关系的名称,如 getOrders() 表示关系 orders
  • getter方法中定义关联的依据,通常是外键关系
  • getter返回一个 yii\db\ActiveQuery 对象

比如以下代码就定义了 Customerorders 关联关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Customer extends \yii\db\ActiveRecord
{
    ... ...

    public function getOrders()
    {
        // 关联的依据是 Order.customer_id = Customer.id
        return $this->hasMany('Order', ['customer_id' => 'id']);
    }
}

这样的话,可以通过 Customer 访问关联的 Order

1
2
3
4
5
// 获取所有与当前 $customer 关联的 orders
$orders = $customer->orders;

// 获取所有关联 orders 中,status=1 的 orders
$orders = $customer->getOrders()->andWhere('status=1')->all();

对于关联查询,有积极的方式也有消极的方式。区别在于采用积极方式时,关联的查询会一并执行, 而消极方式时,仅在显示调用关联记录时材会执行关联的查询。

在积极方式的实现上,Yii2.0与Yii1.1也存在不同。Yii1.1使用一个JOIN查询, 来实现同时查询主记录及其关联的记录。 而Yii2.0弃用JOIN查询的方式,而使用两个顺序的SQL语句, 第一个语句查询主记录,第二个语句根据第一个语句的返回结果进行过滤。

同时,Yii2.0为Active Record引入了 asArray() 方法。在返回大量记录时,可以以数组形式保存, 而不再以对象形式保存,这样可以节约大量的空间,提高效率。

另外一个变化是,在Yii1.1中,字段的默认值可以通过为类的public 成员变量赋初始值来指定。 而在Yii2.0中,这样的方式是行不通的,必须通过重载 init() 成员函数的方式实现了。

Yii2.0还淘汰掉了原来的 CActiveRecordBehavior 类。在Yii2.0中,将行为与类进行绑定采用了统一的方式进行, 具体请参考 行为(Behavior) 的有关内容。

Yii2.0中,ActiveRecord得到极大的加强,在相关的章节中我们已经进行专门的讲解。

这里的内容主要是点一点Yii2.0之于Yii1.1的变化。大致了解下就可以了, 主要还是要看正文专门针对每个知识点的深入讲解。

如果觉得《深入理解Yii2.0》对您有所帮助,也请帮助《深入理解Yii2.0》。 谢谢!