第八章 扩展Yii

在本章中,我们将会讨论如下话题:

  • 创建帮助类
  • 创建模型行为
  • 创建组件
  • 创建可复用控制器动作
  • 创建可复用控制器
  • 创建一个小组件
  • 创建CLI命令
  • 创建过滤器
  • 创建模块
  • 创建一个自定义视图渲染器
  • 创建一个多语言应用
  • 制作可发布的扩展

介绍

在本章中,我们将会想你展示如何实现自己的Yii扩展,以及如何使自己的扩展可复用,并对社区有用。此外,我们将会关注许多你需要做的事情,使你的扩展尽可能高效。

创建帮助类

有许多内置的框架帮助类,例如yii\helpers命名空间下的StringHelper。这里包含了很多有用的静态方法,用于操纵字符串、文件、数组和其它目标。

在需要情况下,对于额外的行为,你可以创建一个自己的帮助类,并将任何静态函数放在里边。例如,我们在本小节中事先了数字帮助类。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

  1. 在你的项目中创建helpers文件夹,以及NumberHelper类:
  1. <?php
  2. namespace app\helpers;
  3. class NumberHelper
  4. {
  5. public static function format($value, $decimal = 2)
  6. {
  7. return number_format($value, $decimal, '.', ',');
  8. }
  9. }
  1. 添加actionNumbers方法到NumberHelper
  1. <?php
  2. ...
  3. class SiteController extends Controller
  4. {
  5. public function actionNumbers()
  6. {
  7. return $this->render('numbers', ['value' => 18878334526.3]);
  8. }
  9. }
  1. 添加views/site/numbers.php视图:
  1. <?php
  2. use app\helpers\NumberHelper;
  3. use yii\helpers\Html;
  4. /* @var $this yii\web\View */
  5. /* @var $value float */
  6. $this->title = 'Numbers';
  7. $this->params['breadcrumbs'][] = $this->title;
  8. ?>
  9. <div class="site-numbers">
  10. <h1><?= Html::encode($this->title) ?></h1>
  11. <p>
  12. Raw number:<br />
  13. <b><?= $value ?></b>
  14. </p>
  15. <p>
  16. Formatted number:<br />
  17. <b><?= NumberHelper::format($value) ?></b>
  18. </p>
  19. </div>
  1. 打开这个动作。你将会看到如下截图:

第八章 扩展Yii - 图1

在其它例子中,你可以指定其它数量的十进制数。观察如下例子:

  1. NumberHelper::format($value, 3)

工作原理…

Yii2中任何帮助类只是函数的集合,在对应的类中以静态方法实现。

你可以使用一个帮助类用于实现任何类型的输出,操纵任何变量的任何值,以及其它例子。

注意:通常,静态帮助类是轻量干净的函数,有少数几个参数。避免将你的业务逻辑和其它复杂的操纵到帮助类中。在其它情况中,使用小组件或者其它组件,而不是帮助类。

参考

欲了解更多信息,参考:

http://www.yiiframework.com/doc-2.0/guide-helper-overview.html

例如对于内置帮助类,参考框架中helpers文件夹下的源代码。对于框架,参考:

https://github.com/yiisoft/yii2/tree/master/framework/helpers

创建模型行为

现在的web应用中,有许多相似的解决方案。龙头产品例如google的Gmail,有这两个的UI模式。其中一个就是软删除。不需要点击成吨的确认进行永久删除,Gmail允许我们将信息立刻标记为删除,然后可以很容易的撤销它。相同的行为可以应用于任何对象上,例如博客帖子、评论等等。

下边我们来创建一个行为,它将允许我们将模型标记为删除,选择还未删除的模型,删除模型,以及所有的模型。在本小节中,我们将会进行一个测试驱动的开发方法,来计划这个行为,并测试实现是否正确。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
  2. 创建两个数据库分别用于工作和测试。
  3. 在你的主应用config/db.php中配置Yii来使用第一个数据库。确认测试应用使用第二个数据库tests/codeception/config/config.php
  4. 创建一个新的migration:
  1. <?php
  2. use yii\db\Migration;
  3. class m160427_103115_create_post_table extends Migration
  4. {
  5. public function up()
  6. {
  7. $this->createTable('{{%post}}', [
  8. 'id' => $this->primaryKey(),
  9. 'title' => $this->string()->notNull(),
  10. 'content_markdown' => $this->text(),
  11. 'content_html' => $this->text(),
  12. ]);
  13. }
  14. public function down()
  15. {
  16. $this->dropTable('{{%post}}');
  17. }
  18. }
  1. 应用migration到工作和测试数据库上:
  1. ./yii migrate
  2. tests/codeception/bin/yii migrate
  1. 创建Post模型:
  1. <?php
  2. namespace app\models;
  3. use app\behaviors\MarkdownBehavior;
  4. use yii\db\ActiveRecord;
  5. /**
  6. * @property integer $id
  7. * @property string $title
  8. * @property string $content_markdown
  9. * @property string $content_html
  10. */
  11. class Post extends ActiveRecord
  12. {
  13. public static function tableName()
  14. {
  15. return '{{%post}}';
  16. }
  17. public function rules()
  18. {
  19. return [
  20. [['title'], 'required'],
  21. [['content_markdown'], 'string'],
  22. [['title'], 'string', 'max' => 255],
  23. ];
  24. }
  25. }

如何做…

准备一个测试环境,为Post模型定义fixtures。创建文件tests/codeception/unit/fixtures/PostFixture.php

  1. <?php
  2. namespace app\tests\codeception\unit\fixtures;
  3. use yii\test\ActiveFixture;
  4. class PostFixture extends ActiveFixture
  5. {
  6. public $modelClass = 'app\models\Post';
  7. public $dataFile = '@tests/codeception/unit/fixtures/data/post.php';
  8. }
  1. 添加一个fixture数据到tests/codeception/unit/fixtures/data/post.php
  1. <?php
  2. return [
  3. [
  4. 'id' => 1,
  5. 'title' => 'Post 1',
  6. 'content_markdown' => 'Stored *markdown* text 1',
  7. 'content_html' => "<p>Stored <em>markdown</em> text 1</p>\n",
  8. ],
  9. ];
  1. 然后,我们需要创建一个测试用例,tests/codeception/unit/MarkdownBehaviorTest.php
  1. <?php
  2. namespace app\tests\codeception\unit;
  3. use app\models\Post;
  4. use app\tests\codeception\unit\fixtures\PostFixture;
  5. use yii\codeception\DbTestCase;
  6. class MarkdownBehaviorTest extends DbTestCase
  7. {
  8. public function testNewModelSave()
  9. {
  10. $post = new Post();
  11. $post->title = 'Title';
  12. $post->content_markdown = 'New *markdown* text';
  13. $this->assertTrue($post->save());
  14. $this->assertEquals("<p>New <em>markdown</em> text</p>\n", $post->content_html);
  15. }
  16. public function testExistingModelSave()
  17. {
  18. $post = Post::findOne(1);
  19. $post->content_markdown = 'Other *markdown* text';
  20. $this->assertTrue($post->save());
  21. $this->assertEquals("<p>Other <em>markdown</em> text</p>\n", $post->content_html);
  22. }
  23. public function fixtures()
  24. {
  25. return [
  26. 'posts' => [
  27. 'class' => PostFixture::className(),
  28. ]
  29. ];
  30. }
  31. }
  1. 运行单元测试:
  1. codecept run unit MarkdownBehaviorTest
  2. Ensure that tests has not passed:
  3. Codeception PHP Testing Framework v2.0.9
  4. Powered by PHPUnit 4.8.27 by Sebastian Bergmann and
  5. contributors.
  6. Unit Tests (2)
  7. ----------------------------------------------------------------
  8. -----------
  9. Trying to test ...
  10. MarkdownBehaviorTest::testNewModelSave Error
  11. Trying to test ...
  12. MarkdownBehaviorTest::testExistingModelSave Error
  13. ----------------------------------------------------------------
  14. -----------
  15. Time: 289 ms, Memory: 16.75MB
  1. 现在我们需要实现行为,将它附加到模型上,并确保测试通过。创建一个新的文件夹behaviors。在这个文件夹中,创建一个MarkdownBehavior类:
  1. <?php
  2. namespace app\behaviors;
  3. use yii\base\Behavior;
  4. use yii\base\Event;
  5. use yii\base\InvalidConfigException;
  6. use yii\db\ActiveRecord;
  7. use yii\helpers\Markdown;
  8. class MarkdownBehavior extends Behavior
  9. {
  10. public $sourceAttribute;
  11. public $targetAttribute;
  12. public function init()
  13. {
  14. if (empty($this->sourceAttribute) ||
  15. empty($this->targetAttribute)) {
  16. throw new InvalidConfigException('Source and target must be set.');
  17. }
  18. parent::init();
  19. }
  20. public function events()
  21. {
  22. return [
  23. ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave',
  24. ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave',
  25. ];
  26. }
  27. public function onBeforeSave(Event $event)
  28. {
  29. if
  30. ($this->owner->isAttributeChanged($this->sourceAttribute)) {
  31. $this->processContent();
  32. }
  33. }
  34. private function processContent()
  35. {
  36. $model = $this->owner;
  37. $source = $model->{$this->sourceAttribute};
  38. $model->{$this->targetAttribute} =
  39. Markdown::process($source);
  40. }
  41. }
  1. 附加行为到Post模型上:
  1. class Post extends ActiveRecord
  2. {
  3. ...
  4. public function behaviors()
  5. {
  6. return [
  7. 'markdown' => [
  8. 'class' => MarkdownBehavior::className(),
  9. 'sourceAttribute' => 'content_markdown',
  10. 'targetAttribute' => 'content_html',
  11. ],
  12. ];
  13. }
  14. }
  1. 运行测试并确保通过:
  1. Codeception PHP Testing Framework v2.0.9
  2. Powered by PHPUnit 4.8.27 by Sebastian Bergmann and
  3. contributors.
  4. Unit Tests (2)
  5. ----------------------------------------------------------------
  6. -----------
  7. Trying to test ...
  8. MarkdownBehaviorTest::testNewModelSave Ok
  9. Trying to test ...
  10. MarkdownBehaviorTest::testExistingModelSave Ok
  11. ----------------------------------------------------------------
  12. -----------
  13. Time: 329 ms, Memory: 17.00MB
  1. 完成了。我们已经创建了一个可复用的行为,并可以使用它用于所有未来的项目中,只需要将它连接到一个模型上。

工作原理…

首先看下测试用例。因为我们希望使用模型集,我们定义了fixtures。每次测试方法被执行的时候,一个fixture集合被放到了数据库中。

我们准备单元测试用以说明行为是如何工作的:

  • 首先,我们测试一个新的模型内容的处理。这个行为会将source属性中的markdown格式的文本,转换为HTML,并存储在target属性中。
  • 第二,我们对更新已有模型的内容进行测试。在修改了markdown内容以后,保存这个模型,我们可以得到更新后的HTML内容。

现在,我们转到有趣的实现细节上。在行为中,我们可以添加我们自己的方法,它将会被混合到附带有行为的模型中。此外,我们可以订阅拥有者的组件事件。我们使用它添加一个自己的监听:

  1. public function events()
  2. {
  3. return [
  4. ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave',
  5. ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave',
  6. ];
  7. }

现在,我们可以实现这个监听器:

  1. public function onBeforeSave(Event $event)
  2. {
  3. if ($this->owner->isAttributeChanged($this->sourceAttribute))
  4. {
  5. $this->processContent();
  6. }
  7. }

在所有的方法中,我们可以使用owner属性来获取附带有行为的对象。一般情况下,我们可以附加任何行为到我们的模型、控制器、应用,以及其它继承了yii\base\Component类的组件。此外,我们可以重复附加一个行为到模型上,用以处理不同的属性:

  1. class Post extends ActiveRecord
  2. {
  3. ...
  4. public function behaviors()
  5. {
  6. return [
  7. [
  8. 'class' => MarkdownBehavior::className(),
  9. 'sourceAttribute' => 'description_markdown',
  10. 'targetAttribute' => 'description_html',
  11. ],
  12. [
  13. 'class' => MarkdownBehavior::className(),
  14. 'sourceAttribute' => 'content_markdown',
  15. 'targetAttribute' => 'content_html',
  16. ],
  17. ];
  18. }
  19. }

此外,我们可以像yii\behaviors\TimestampBehavior继承yii\base\AttributeBehavior,用以为任何事件更新指定的属性。

参考

为了了解更多关于行为和事件,参考如下页面:

欲了解更多关于markdown语法的信息,参考http://daringfireball.net/projects/markdown/

此外,参考本章中的制作可发布的扩展小节。

创建组件

如果你有一些代码,看上去可以被复用,但是你不知道它是一个行为、小组件还是其它东西,很有可能它是一个组件。一个组件应该是继承了yii\base\Component类。然后,这个组件可以被附加到应用上,并使用配置文件中的components部分进行配置。这就是同只是使用纯PHP类相比最主要的优点。此外,我们可以得到行为、事件、getter、setter的支持。

在我们的例子中,我们将会实现一个简单的交换应用组件,它能从http://fixer.io获取最新的汇率,将它附加在应用上,并使用它。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

为了得到汇率,我们的组件应该发送一个HTTP GET请求到一个服务地址上,例如http://api.fixer.io/2016-05-14?base=USD

这个服务会返回所有支持的汇率在最近一天的情况:

  1. {
  2. "base":"USD",
  3. "date":"2016-05-13",
  4. "rates": {
  5. "AUD":1.3728,
  6. "BGN":1.7235,
  7. ...
  8. "ZAR":15.168,
  9. "EUR":0.88121
  10. }
  11. }

这个组件应该从JSON格式的响应解析出汇率,并返回一个目标汇率:

  1. 在你的应用结构中创建components文件夹。
  2. 使用如下interface创建组件类例子:
  1. <?php
  2. namespace app\components;
  3. use yii\base\Component;
  4. class Exchange extends Component
  5. {
  6. public function getRate($source, $destination, $date = null)
  7. {
  8. }
  9. }
  1. 实现这个组件的功能:
  1. <?php
  2. namespace app\components;
  3. use yii\base\Component;
  4. use yii\base\InvalidConfigException;
  5. use yii\base\InvalidParamException;
  6. use yii\caching\Cache;
  7. use yii\di\Instance;
  8. use yii\helpers\Json;
  9. class Exchange extends Component
  10. {
  11. /**
  12. * @var string remote host
  13. */
  14. public $host = 'http://api.fixer.io';
  15. /**
  16. * @var bool cache results or not
  17. */
  18. public $enableCaching = false;
  19. /**
  20. * @var string|Cache component ID
  21. */
  22. public $cache = 'cache';
  23. public function init()
  24. {
  25. if (empty($this->host)) {
  26. throw new InvalidConfigException('Host must be set.');
  27. }
  28. if ($this->enableCaching) {
  29. $this->cache = Instance::ensure($this->cache,
  30. Cache::className());
  31. }
  32. parent::init();
  33. }
  34. public function getRate($source, $destination, $date = null)
  35. {
  36. $this->validateCurrency($source);
  37. $this->validateCurrency($destination);
  38. $date = $this->validateDate($date);
  39. $cacheKey = $this->generateCacheKey($source,
  40. $destination, $date);
  41. if (!$this->enableCaching || ($result =
  42. $this->cache->get($cacheKey)) === false) {
  43. $result = $this->getRemoteRate($source,
  44. $destination, $date);
  45. if ($this->enableCaching) {
  46. $this->cache->set($cacheKey, $result);
  47. }
  48. }
  49. return $result;
  50. }
  51. private function getRemoteRate($source, $destination, $date)
  52. {
  53. $url = $this->host . '/' . $date . '?base=' . $source;
  54. $response = Json::decode(file_get_contents($url));
  55. if (!isset($response['rates'][$destination])) {
  56. throw new \RuntimeException('Rate not found.');
  57. }
  58. return $response['rates'][$destination];
  59. }
  60. private function validateCurrency($source)
  61. {
  62. if (!preg_match('#^[A-Z]{3}$#s', $source)) {
  63. throw new InvalidParamException('Invalid currency format.');
  64. }
  65. }
  66. private function validateDate($date)
  67. {
  68. if (!empty($date) &&
  69. !preg_match('#\d{4}\-\d{2}-\d{2}#s', $date)) {
  70. throw new InvalidParamException('Invalid date format.');
  71. }
  72. if (empty($date)) {
  73. $date = date('Y-m-d');
  74. }
  75. return $date;
  76. }
  77. private function generateCacheKey($source, $destination,
  78. $date)
  79. {
  80. return [__CLASS__, $source, $destination, $date];
  81. }
  82. }
  1. 附加这个组件到你的config/console.php或者config/web.php配置文件中:
  1. 'components' => [
  2. 'cache' => [
  3. 'class' => 'yii\caching\FileCache',
  4. ],
  5. 'exchange' => [
  6. 'class' => 'app\components\Exchange',
  7. 'enableCaching' => true,
  8. ],
  9. // ...
  10. db' => $db,
  11. ],
  1. 现在,我们可以直接使用一个新的组件,或者使用get方法:
  1. echo \Yii::$app->exchange->getRate('USD', 'EUR');
  2. echo \Yii::$app->get('exchange')->getRate('USD', 'EUR', '2014-04-12');
  1. 创建一个实例控制台控制器:
  1. <?php
  2. namespace app\commands;
  3. use yii\console\Controller;
  4. class ExchangeController extends Controller
  5. {
  6. public function actionTest($currency, $date = null)
  7. {
  8. echo \Yii::$app->exchange->getRate('USD', $currency,
  9. $date) . PHP_EOL;
  10. }
  11. }
  1. 现在尝试运行任何命令:
  1. $ ./yii exchange/test EUR
  2. > 0.90196
  3. $ ./yii exchange/test EUR 2015-11-24
  4. > 0.93888
  5. $ ./yii exchange/test OTHER
  6. > Exception 'yii\base\InvalidParamException' with message 'Invalid currency format.'
  7. $ ./yii exchange/test EUR 2015/24/11
  8. Exception 'yii\base\InvalidParamException' with message 'Invalid date format.'
  9. $ ./yii exchange/test ASD
  10. > Exception 'RuntimeException' with message 'Rate not found.'

作为结果,成功的话,你可以看到汇率值;如果失败你会看到指定的异常错误。此外创建你自己的组件,你可以做的更多。

覆盖已经存在的应用组件

大部分情况下,没有必须创建你自己的应用组件,因为其它类型的扩展,例如小组件或者行为,涵盖了几乎所有类型的可复用代码。但是,复写核心框架组件是一个常用的实践,并且可以被用于自定义框架的行为,用于特殊的需求,而不需要修改核心代码。

例如,为了能够使用Yii::app()->formatter->asNumber($value)格式化数字,而不是在创建帮助类小节中的NumberHelper::format方法,你可以使用如下步骤:

  1. 继承yii\i18n\Formatter组件:
  1. <?php
  2. namespace app\components;
  3. class Formatter extends \yii\i18n\Formatter
  4. {
  5. public function asNumber($value, $decimal = 2)
  6. {
  7. return number_format($value, $decimal, '.', ',');
  8. }
  9. }
  1. 复写内置formatter组件的类:
  1. 'components' => [
  2. // ...
  3. formatter => [
  4. 'class' => 'app\components\Formatter,
  5. ],
  6. // ...
  7. ],
  1. 现在,我们可以直接使用这个方法:
  1. echo Yii::app()->formatter->asNumber(1534635.2, 3);

或者,它可以被用做一个新的格式,用在GridViewDetailView小组件中:

  1. <?= \yii\grid\GridView::widget([
  2. 'dataProvider' => $dataProvider,
  3. 'columns' => [
  4. 'id',
  5. 'created_at:datetime',
  6. 'title',
  7. 'value:number',
  8. ],
  9. ]) ?>
  1. 此外,你可以扩展任何已有的组件,而不需要修改源代码。

工作原理…

为了能附加一个组件到一个应用中,它可以从yii\base\Component进行继承。附加非常简单,只需要添加一个新的数组到配置的组件部分。这里class的值指定了组件的类,其它值用于设置这个组件的公共属性和setter方法。

继承它自己非常直接:我们包裹了http://api.fixer.io调用到一个非常舒适的API中,并可以进行校验和缓存。我们可以通过Yii::$app和组件的名称访问我们的类。在我们的例子中,它会是Yii::$app->exchange

参考

欲了解关于组件的官方信息,参考http://www.yiiframework.com/doc-2.0/guideconcept-components.html

对于NumberHelper类的源代码,参考创建帮助类小节。

创建可重用控制器动作

常用的动作,例如通过主键删除AR模型,或者从AJAX autocomplete获取数据,可以移到可复用控制器动作中,然后附加到需要的控制器上。

在这个小节中,我们将会创建一个可复用删除动作,它会通过主键删除指定的AR模型。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
  2. 创建一个新的数据库并配置它。
  3. 创建并应用如下migration:
  1. <?php
  2. use yii\db\Migration;
  3. class m160308_093233_create_post_table extends Migration
  4. {
  5. public function up()
  6. {
  7. $this->createTable('{{%post}}', [
  8. 'id' => $this->primaryKey(),
  9. 'title' => $this->string()->notNull(),
  10. 'text' => $this->text()->notNull(),
  11. ]);
  12. }
  13. public function down()
  14. {
  15. $this->dropTable('{{%post}}');
  16. }
  17. }
  1. 使用Gii为帖子和评论生成模型。
  2. 在Gii中生成标准的CRUD控制器app\controllers\PostController
  3. 确保CRUD正常工作:

第八章 扩展Yii - 图2

  1. 在一个成功的例子中,添加一些帖子示例。

如何做…

执行如下步骤:

  1. 创建动作文件夹,添加DeleteAction独立动作:
  1. <?php
  2. namespace app\actions;
  3. use yii\base\Action;
  4. use yii\base\InvalidConfigException;
  5. use yii\web\MethodNotAllowedHttpException;
  6. use yii\web\NotFoundHttpException;
  7. class DeleteAction extends Action
  8. {
  9. public $modelClass;
  10. public $redirectTo = ['index'];
  11. public function init()
  12. {
  13. if (empty($this->modelClass)) {
  14. throw new InvalidConfigException('Empty model class.');
  15. }
  16. parent::init();
  17. }
  18. public function run($id)
  19. {
  20. if (!\Yii::$app->getRequest()->getIsPost()) {
  21. throw new MethodNotAllowedHttpException('Method not allowed.');
  22. }
  23. $model = $this->findModel($id);
  24. $model->delete();
  25. return $this->controller->redirect($this->redirectTo);
  26. }
  27. /**
  28. * @param $id
  29. * @return \yii\db\ActiveRecord
  30. * @throws NotFoundHttpException
  31. */
  32. private function findModel($id)
  33. {
  34. $class = $this->modelClass;
  35. if (($model = $class::findOne($id)) !== null) {
  36. return $model;
  37. } else {
  38. throw new NotFoundHttpException('Page does not exist.');
  39. }
  40. }
  41. }
  1. 现在我们需要将它附加到controllers/PostController.php控制器中。移除控制器的actionDeletebehaviors方法,并在actions方法中附加你自己的动作:
  1. <?php
  2. namespace app\controllers;
  3. use app\actions\DeleteAction;
  4. use Yii;
  5. use app\models\Post;
  6. use app\models\PostSearch;
  7. use yii\web\Controller;
  8. use yii\web\NotFoundHttpException;
  9. class PostController extends Controller
  10. {
  11. public function actions()
  12. {
  13. return [
  14. 'delete' => [
  15. 'class' => DeleteAction::className(),
  16. 'modelClass' => Post::className(),
  17. ],
  18. ];
  19. }
  20. public function actionIndex() { ... }
  21. public function actionView($id) { ... }
  22. public function actionCreate() { ... }
  23. public function actionUpdate($id) { ... }
  24. protected function findModel($id)
  25. {
  26. if (($model = Post::findOne($id)) !== null) {
  27. return $model;
  28. } else {
  29. throw new NotFoundHttpException('The requested page does not exist.');
  30. }
  31. }
  32. }
  1. 完成了。确保删除操作可以正常工作,并且在删除之后,你将会被重定向到一个对应的index动作中。

工作原理…

为了创建一个额外的控制器动作,你需要从yii\base\Action中继承你的类。唯一需要强制实现的方法是run。在我们的例子中,它使用Yii的自动参数绑定特性,从$_GET接收名叫$id的参数,并尝试删除一个对应的模型。

为了是它可配置,我们创建了两个可配置的公共属性。modelName保存了模型的名称,以及redirectTo指定了用户会被重定向的路由。

这个配置本身是通过在你的控制器中实现动作方法来完成的。这里,你可以附加这个动作一次或者多次,并配置它的公共属性。

如果你需要重定向到别的动作,或者渲染一个指定的视图,你可以通过控制器属性,访问原始的控制器对象。

参考

创建可重用控制器

在Yii中,你可以创建可复用的控制器。如果你创建许多应用或者控制器,他们有相同的类型,将所有常用的代码移动到一个可复用的控制器中将会节省很多时间。

在本小节中,我们将会尝试创建一个常用的CleanController,它会清理临时文件夹以及flush缓存数据。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

执行如下步骤,创建可复用的控制器:

  1. 创建cleaner目录,并添加独立CleanController控制器:
  1. <?php
  2. namespace app\cleaner;
  3. use Yii;
  4. use yii\filters\VerbFilter;
  5. use yii\helpers\FileHelper;
  6. use yii\web\Controller;
  7. class CleanController extends Controller
  8. {
  9. public $assetPaths = ['@app/web/assets'];
  10. public $runtimePaths = ['@runtime'];
  11. public $caches = ['cache'];
  12. public function behaviors()
  13. {
  14. return [
  15. 'verbs' => [
  16. 'class' => VerbFilter::className(),
  17. 'actions' => [
  18. 'assets' => ['post'],
  19. 'runtime' => ['post'],
  20. 'cache' => ['post'],
  21. ],
  22. ],
  23. ];
  24. }
  25. public function actionIndex()
  26. {
  27. return $this->render('@app/cleaner/views/index');
  28. }
  29. public function actionAssets()
  30. {
  31. foreach ((array)$this->assetPaths as $path) {
  32. $this->cleanDir($path);
  33. Yii::$app->session->addFlash(
  34. 'cleaner',
  35. 'Assets path "' . $path . '" is cleaned.'
  36. );
  37. }
  38. return $this->redirect(['index']);
  39. }
  40. public function actionRuntime()
  41. {
  42. foreach ((array)$this->runtimePaths as $path) {
  43. $this->cleanDir($path);
  44. Yii::$app->session->addFlash(
  45. 'cleaner',
  46. 'Runtime path "' . $path . '" is cleaned.'
  47. );
  48. }
  49. return $this->redirect(['index']);
  50. }
  51. public function actionCache()
  52. {
  53. foreach ((array)$this->caches as $cache) {
  54. Yii::$app->get($cache)->flush();
  55. Yii::$app->session->addFlash(
  56. 'cleaner',
  57. 'Cache "' . $cache . '" is cleaned.'
  58. );
  59. }
  60. return $this->redirect(['index']);
  61. }
  62. private function cleanDir($dir)
  63. {
  64. $iterator = new \DirectoryIterator(Yii::getAlias($dir));
  65. foreach($iterator as $sub) {
  66. if(!$sub->isDot() && $sub->isDir()) {
  67. FileHelper::removeDirectory($sub->getPathname());
  68. }
  69. }
  70. }
  71. }
  1. actionIndex方法创建cleaner/views/index.php视图文件:actionIndex
  1. <?php
  2. use yii\helpers\Html;
  3. /* @var $this yii\web\View */
  4. $this->title = 'Cleaner';
  5. $this->params['breadcrumbs'][] = $this->title;
  6. ?>
  7. <div class="clean-index">
  8. <h1><?= Html::encode($this->title) ?></h1>
  9. <?php if (Yii::$app->session->hasFlash('cleaner')): ?>
  10. <?php foreach
  11. ((array)Yii::$app->session->getFlash('cleaner', []) as
  12. $message): ?>
  13. <div class="alert alert-success">
  14. <?= $message ?>
  15. </div>
  16. <?php endforeach; ?>
  17. <?php endif; ?>
  18. <p>
  19. <?= Html::a('Clear Caches', ['cache'], [
  20. 'class' => 'btn btn-primary',
  21. 'data' => [
  22. 'confirm' => 'Are you sure you want to clear all cache data?',
  23. 'method' => 'post',
  24. ],
  25. ]) ?>
  26. <?= Html::a('Clear Assets', ['assets'],
  27. ['class' => 'btn btn-primary',
  28. 'data' => [
  29. 'confirm' => 'Are you sure you want to clear all temporary assets?',
  30. 'method' => 'post',
  31. ],
  32. ]) ?>
  33. <?= Html::a('Clear Runtime', ['runtime'],
  34. ['class' => 'btn btn-primary',
  35. 'data' => [
  36. 'confirm' => 'Are you sure you want to clear all runtime files?',
  37. 'method' => 'post',
  38. ],
  39. ]) ?>
  40. </p>
  41. </div>
  1. 配置config/web.phpcontrollerMap部分,附加控制器到应用中:
  1. $config = [
  2. 'id' => 'basic',
  3. 'basePath' => dirname(__DIR__),
  4. 'bootstrap' => ['log'],
  5. 'controllerMap' => [
  6. 'clean' => 'app\cleaner\CleanController',
  7. ],
  8. 'components' => [
  9. ...
  10. ]
  11. ...
  12. ];
  1. 添加一个新的条目到主菜单中:
  1. echo Nav::widget([
  2. 'options' => ['class' => 'navbar-nav navbar-right'],
  3. 'items' => [
  4. ['label' => 'Home', 'url' => ['/site/index']],
  5. ['label' => 'Cleaner', 'url' => ['/clean/index']],
  6. ['label' => 'About', 'url' => ['/site/about']],
  7. ...
  8. ],
  9. ]);
  1. 打开控制器,并清理assets:

第八章 扩展Yii - 图3

  1. 如果你用的是yii2高级应用模板,只需要在配置中指定正确的路径:
  1. 'controllerMap' => [
  2. 'clean' => 'app\cleaner\CleanController',
  3. 'assetPaths' => [
  4. '@backend/web/assets',
  5. '@frontend/web/assets',
  6. ],
  7. 'runtimePaths' => [
  8. '@backend/runtime',
  9. '@frontend/runtime',
  10. '@console/runtime',
  11. ],
  12. ],

现在我们可以附加这个控制器到任何应用中。

工作原理…

当你运行一个应用时,假如路由是clean/index,指向CleanController::actionIndex,Yii检查controllerMap是否定义了。因为这里我们有一个干净的控制器,Yii会执行它,而不是像常用的方式。

在这个控制器中,我们定义了assetPathsruntimePathscaches属性,这能够连接这个控制器到应用的不同路径和缓存结构。当附加这个控制器的时候设置它。

参考

创建小部件

小部件是视图中可复用的部分,它不仅会渲染一些数据,而且还能依赖于一些逻辑。它甚至可以从模型中获取数据,并使用它自己的视图,所以它就像一个简化的可复用的模块。

下面我们来创建一个小部件,它会使用Google API画一个饼状图。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

  1. 创建widgets目录,并添加ChartWidget类:
  1. <?php
  2. namespace app\widgets;
  3. use yii\base\Widget;
  4. class ChartWidget extends Widget
  5. {
  6. public $title;
  7. public $width = 300;
  8. public $height = 200;
  9. public $data = [];
  10. public $labels = [];
  11. public function run()
  12. {
  13. $path = 'http://chart.apis.google.com/chart';
  14. $query = http_build_query([
  15. 'chtt' => $this->title,
  16. 'cht' => 'pc',
  17. 'chs' => $this->width . 'x' . $this->height,
  18. 'chd' => 't:' . implode(',', $this->data),
  19. 'chds' => 'a',
  20. 'chl' => implode('|', $this->labels),
  21. 'chxt' => 'y',
  22. 'chxl' => '0:|0|' . max($this->data)
  23. ]);
  24. $url = $path . '?' . $query;
  25. return $this->render('chart', [
  26. 'url' => $url,
  27. ]);
  28. }
  29. }
  1. 创建widgets/views/chart.php视图:
  1. <?php
  2. use yii\helpers\Html;
  3. /* @var $this yii\web\View */
  4. /* @var $url string */
  5. ?>
  6. <div class="chart">
  7. <?= Html::img($url) ?>
  8. </div>
  1. 创建一个ChartController控制器:
  1. <?php
  2. namespace app\controllers;
  3. use yii\base\Controller;
  4. class ChartController extends Controller
  5. {
  6. public function actionIndex()
  7. {
  8. return $this->render('index');
  9. }
  10. }
  1. 添加views/chart/index.php视图:
  1. <?php
  2. use app\widgets\ChartWidget;
  3. use yii\helpers\Html;
  4. /* @var $this yii\web\View */
  5. $this->title = 'Chart';
  6. $this->params['breadcrumbs'][] = $this->title;
  7. ?>
  8. <div class="site-about">
  9. <h1><?= Html::encode($this->title) ?></h1>
  10. <?= ChartWidget::widget([
  11. 'title' => 'My Chart Diagram',
  12. 'data' => [
  13. 100 - 32,
  14. 32,
  15. ],
  16. 'labels' => [
  17. 'Big',
  18. 'Small',
  19. ],
  20. ]) ?>
  21. </div>
  1. 现在尝试运行这个动作。你应该能看到一个饼状图,如下所示:

第八章 扩展Yii - 图4

  1. 你可以展示不同尺寸和数据集的图。

工作原理…

和其它类型的扩展一样,我们创建一些可以配置的公共属性,在调用一个小部件时使用它的widget方法。在这个例子中,我们配置了标题、数据集和数据标签。

小部件的主方法是run()。在我们的小部件中,我们生成一个URL,并渲染小部件视图,它使用Google charting API来打印<img>标签。

参考

创建CLI命令

Yii有一个好的命令行支持,允许创建可复用的控制台命令。控制台命令比创建web GUI更快。如果你需要为你的应用创建一些工具,可以被开发者或者管理使用,那么控制台命令就是很好的工具。

为了展示如何创建一个控制台命令,我们将会创建一个简单的命令,它会清理一些东西,例如assets和临时文件夹。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

执行如下过程来创建CLI命令:

  1. 使用如下代码创建commands/CleanController.php
  1. <?php
  2. namespace app\commands;
  3. use yii\console\Controller;
  4. use yii\helpers\FileHelper;
  5. /**
  6. * Removes content of assets and runtime directories.
  7. */
  8. class CleanController extends Controller
  9. {
  10. public $assetPaths = ['@app/web/assets'];
  11. public $runtimePaths = ['@runtime'];
  12. /**
  13. * Removes temporary assets.
  14. */
  15. public function actionAssets()
  16. {
  17. foreach ((array)$this->assetPaths as $path) {
  18. $this->cleanDir($path);
  19. }
  20. $this->stdout('Done' . PHP_EOL);
  21. }
  22. /**
  23. * Removes runtime content.
  24. */
  25. public function actionRuntime()
  26. {
  27. foreach ((array)$this->runtimePaths as $path) {
  28. $this->cleanDir($path);
  29. }
  30. $this->stdout('Done' . PHP_EOL);
  31. }
  32. private function cleanDir($dir)
  33. {
  34. $iterator = new
  35. \DirectoryIterator(\Yii::getAlias($dir));
  36. foreach($iterator as $sub) {
  37. if(!$sub->isDot() && $sub->isDir()) {
  38. $this->stdout('Removed ' . $sub->getPathname()
  39. . PHP_EOL);
  40. FileHelper::removeDirectory($sub->getPathname());
  41. }
  42. }
  43. }
  44. }
  1. 现在我们可以使用我们自己的控制台命令。只需要运行yii shell脚本。
  1. ./yii
  1. 查找自己的clean命令:
  1. This is Yii version 2.0.7.
  2. The following commands are available:
  3. - asset Allows you to combine...
  4. asset/compress Combines and compresses the asset...
  5. asset/template Creates template of configuration
  6. file...
  7. ...
  8. - clean Removes content of assets and
  9. runtime directories.
  10. clean/assets Removes temporary assets.
  11. clean/runtime Removes runtime content.
  12. - fixture Manages fixture data loading and
  13. unloading.
  14. fixture/load (default) Loads the specified fixture data.
  15. fixture/unload Unloads the specified fixtures.
  16. ...
  1. 运行asset清理:
  1. .yii clean/assets
  1. 查看处理报告:
  1. Removed /yii-book.app/web/assets/25f82b8a
  2. Removed /yii-book.app/web/assets/9b3b2888
  3. Removed /yii-book.app/web/assets/f4307424
  4. Done
  1. 如果你想在yii2-app-advanced应用中使用这个控制器,只需要指定自定义工作路径:
  1. return [
  2. 'id' => 'app-console',
  3. 'basePath' => dirname(__DIR__),
  4. 'bootstrap' => ['log'],
  5. 'controllerNamespace' => 'console\controllers',
  6. 'controllerMap' => [
  7. 'clean' => [
  8. 'class' => 'console\controllers\CleanController',
  9. 'assetPaths' => [
  10. '@backend/web/assets',
  11. '@frontend/web/assets',
  12. ],
  13. 'runtimePaths' => [
  14. '@backend/runtime',
  15. '@frontend/runtime',
  16. '@console/runtime',
  17. ],
  18. ],
  19. ],
  20. // ...
  21. ];

工作原理…

所有的控制台命令应该继承yii\console\Controller类。因为所有的控制器命令在yii\console\Application运行,而不是yii\web\Application,我们没有办法来决定@webroot的值。此外,在yii2-app-advanced模板中,默认情况下,我们有前端、后端和控制子目录。对于这个目的,我们创建可配置的公共数据,叫做assetPathsruntimePaths

控制台命令结构本身类似一个典型的控制器。我们定义几个动作,然后可以通过yii <console command>/<command action>来执行。

正如你所看到的,没有使用视图,所以我们可以把精力集中在编程任务上,而不需要设计、标记等等。此外,你需要提供一些有用的输出,这样用户就知道现在是什么情况。这可以通过简单的PHP echo语句来完成。

如果你的命令相对复杂,例如使用Yii构建的消息或者migrate,提供额外的描述是一个好的决定,说明可用的选项和动作。它可以通过复写getHelp方法完成:

  1. public function getHelp()
  2. {
  3. $out = "Clean command allows you to clean up various temporary data Yii and an application are generating.\n\n";
  4. return $out . parent::getHelp();
  5. }

运行如下命令:

  1. ./yii help clean

你会看到如下全部输出:

  1. DESCRIPTION
  2. Clean command allows you to clean up various temporary data Yii and
  3. an application are generating.
  4. Removes content of assets and runtime directories.
  5. SUB-COMMANDS
  6. - clean/assets Removes temporary assets.
  7. - clean/runtime Removes runtime content.

默认情况下,当我们运行shell命令时:

  1. ./yii

我们在输出列表中看到了所有命令的简化描述:

  1. - clean Removes content of assets and runtime directories.
  2. clean/assets Removes temporary assets.
  3. clean/runtime Removes runtime content.

这个描述将会从类和动作前边的注释中获取:

  1. /**
  2. * Removes content of assets and runtime directories.
  3. */
  4. class CleanController extends Controller
  5. {
  6. /**
  7. * Removes temporary assets.
  8. */
  9. public function actionAssets() { }
  10. * Removes runtime content.
  11. */
  12. public function actionRuntime() { }
  13. }

为你的类添加描述是可选的。你不用非得为你的CLI命令做这件事。

参考

  • 本章中的创建可复用控制器小节
  • 本章中制作可发布扩展小节

创建过滤器

过滤器是一个类,它可以在动作之前或者之后执行。它可以被用于修改执行上下文,或者装饰输出。在我们的例子中,我们将会实现一个简单的访问过滤器,它允许用户只能在接受了用户协议之后才能看到私有的内容。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

  1. 创建协议表单模型:
  1. <?php
  2. namespace app\models;
  3. use yii\base\Model;
  4. class AgreementForm extends Model
  5. {
  6. public $accept;
  7. public function rules()
  8. {
  9. return [
  10. ['accept', 'required'],
  11. ['accept', 'compare', 'compareValue' => 1,
  12. 'message' => 'You must agree the rules.'],
  13. ];
  14. }
  15. public function attributeLabels()
  16. {
  17. return [
  18. 'accept' => 'I completely accept the rules.'
  19. ];
  20. }
  21. }
  1. 创建协议检查服务:
  1. <?php
  2. namespace app\services;
  3. use Yii;
  4. use yii\web\Cookie;
  5. class AgreementChecker
  6. {
  7. public function isAllowed()
  8. {
  9. return Yii::$app->request->cookies->has('agree');
  10. }
  11. public function allowAccess()
  12. {
  13. Yii::$app->response->cookies->add(new Cookie([
  14. 'name' => 'agree',
  15. 'value' => 'on',
  16. 'expire' => time() + 3600 * 24 * 90, // 90 days
  17. ]));
  18. }
  19. }

它使用了协议cookies进行了封装。

  1. 创建filter类:
  1. <?php
  2. namespace app\filters;
  3. use app\services\AgreementChecker;
  4. use Yii;
  5. use yii\base\ActionFilter;
  6. class AgreementFilter extends ActionFilter
  7. {
  8. public function beforeAction($action)
  9. {
  10. $checker = new AgreementChecker();
  11. if (!$checker->isAllowed()) {
  12. Yii::$app->response->redirect(['/content/agreement'])->send();
  13. return false;
  14. }
  15. return true;
  16. }
  17. }
  1. 创建内容控制器,并将过滤器附加到行为上:
  1. <?php
  2. namespace app\controllers;
  3. use app\filters\AgreementFilter;
  4. use app\models\AgreementForm;
  5. use app\services\AgreementChecker;
  6. use Yii;
  7. use yii\web\Controller;
  8. class ContentController extends Controller
  9. {
  10. public function behaviors()
  11. {
  12. return [
  13. [
  14. 'class' => AgreementFilter::className(),
  15. 'only' => ['index'],
  16. ],
  17. ];
  18. }
  19. public function actionIndex()
  20. {
  21. return $this->render('index');
  22. }
  23. public function actionAgreement()
  24. {
  25. $model = new AgreementForm();
  26. if ($model->load(Yii::$app->request->post()) &&
  27. $model->validate()) {
  28. $checker = new AgreementChecker();
  29. $checker->allowAccess();
  30. return $this->redirect(['index']);
  31. } else {
  32. return $this->render('agreement', [
  33. 'model' => $model,
  34. ]);
  35. }
  36. }
  37. }
  1. 添加私有内容到views/content/index.php
  1. <?php
  2. use yii\helpers\Html;
  3. /* @var $this yii\web\View */
  4. $this->title = 'Content';
  5. $this->params['breadcrumbs'][] = $this->title;
  6. ?>
  7. <div class="site-about">
  8. <h1><?= Html::encode($this->title) ?></h1>
  9. <div class="well">
  10. This is our private page.
  11. </div>
  12. </div>
  1. 给表单添加views/content/agreement.php视图:
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\bootstrap\ActiveForm;
  4. /* @var $this yii\web\View */
  5. /* @var $form yii\bootstrap\ActiveForm */
  6. /* @var $model app\models\AgreementForm */
  7. $this->title = 'User agreement';
  8. $this->params['breadcrumbs'][] = $this->title;
  9. ?>
  10. <div class="site-login">
  11. <h1><?= Html::encode($this->title) ?></h1>
  12. <p>Please agree with our rules:</p>
  13. <?php $form = ActiveForm::begin(); ?>
  14. <?= $form->field($model, 'accept')->checkbox() ?>
  15. <div class="form-group">
  16. <?= Html::submitButton('Accept', ['class' => 'btn btn-success']) ?>
  17. <?= Html::a('Cancel', ['/site/index'], ['class' => 'btn btn-danger']) ?>
  18. </div>
  19. <?php ActiveForm::end(); ?>
  20. </div>
  1. 添加主菜单项到views/layouts/main.php
  1. echo Nav::widget([
  2. 'options' => ['class' => 'navbar-nav navbar-right'],
  3. 'items' => [
  4. ['label' => 'Home', 'url' => ['/site/index']],
  5. ['label' => 'Content', 'url' => ['/content/index']],
  6. ['label' => 'About', 'url' => ['/site/about']],
  7. //...
  8. ],
  9. ]);
  1. 尝试打开内容页。过滤器会将你重定向到协议页上:

第八章 扩展Yii - 图5

  1. 只有在接受协议之后,你才可以看到私有内容:

第八章 扩展Yii - 图6

  1. 此外,你可以附加这个过滤器到其他控制器或者模块上。

工作原理…

过滤器应该继承了yii\base\ActionFilter类,它继承了yii\base\Behavior。如果我们想做前过滤或者后过滤,我们可以复写beforeAction或者afterAction方法。

例如,我们可以检查用户访问,并在遇到失败情况时,抛出HTTP异常。在这个小节中,如果指定的cookie的值不存在,我们将用户重定向到协议页上。

  1. class AgreementFilter extends ActionFilter
  2. {
  3. public function beforeAction($action)
  4. {
  5. $checker = new AgreementChecker();
  6. if (!$checker->isAllowed()) {
  7. Yii::$app->response->redirect(['/content/agreement'])->send();
  8. return false;
  9. }
  10. return true;
  11. }
  12. }

你可以附加过滤器到任何控制器或者模块上。为了指定必要路由的列表,只需要使用only或者except选项。例如,我们只为控制器的index动作应用我们的过滤器:

  1. public function behaviors()
  2. {
  3. return [
  4. [
  5. 'class' => AgreementFilter::className(),
  6. 'only' => ['index'],
  7. ],
  8. ];
  9. }

注意:不要忘记,对于beforeAction方法,成功的时候返回一个true。否则,这个控制器动作将不会被执行。

参考

欲了解更多关于过滤器的信息,参考http://www.yiiframework.com/doc-2.0/guide-structurefilters.html

对于内置的缓存和访问控制过滤器,参考:

创建模块

如果你创建了一个复杂的应用部分,并希望他有一些可自定义的自由度,并用于下一个项目中,很可能你需要创建一个模块。在这个小节中,我们将会看到如何创建一个应用日志查看模块。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

首先我们来做一些计划。

yii2-app-basic中使用缺省配置,所有的日志被存放在runtime/logs/app.log文件中。我们可以使用正则表达式将所有的消息从文件中提取出来,然后将他们展示在GridView小部件上。此外,我们允许用户为日志文件配置自定义路径。

执行如下步骤:

  1. 创建modules/log文件夹,并创建Module类文件:
  1. <?php
  2. namespace app\modules\log;
  3. class Module extends \yii\base\Module
  4. {
  5. public $file = '@runtime/logs/app.log';
  6. }
  1. 创建一个简单的模型,用于从日志文件中转换每行内容:
  1. <?php
  2. namespace app\modules\log\models;
  3. use yii\base\Object;
  4. class LogRow extends Object
  5. {
  6. public $time;
  7. public $ip;
  8. public $userId;
  9. public $sessionId;
  10. public $level;
  11. public $category;
  12. public $text;
  13. }
  1. 写一个日志文件读取类,它会解析文件每行内容,逆序排列,返回LogRow模型的实例向量:
  1. <?php
  2. namespace app\modules\log\services;
  3. use app\modules\log\models\LogRow;
  4. class LogReader
  5. {
  6. public function getRows($file)
  7. {
  8. $result = [];
  9. $handle = @fopen($file, "r");
  10. if ($handle) {
  11. while (($row = fgets($handle)) !== false) {
  12. $pattern =
  13. '#^' .
  14. '(?P<time>\d{4}\-\d{2}\-\d{2}\d{2}:\d{2}:\d{2}) ' .
  15. '\[(?P<ip>[^\]]+)\]' .
  16. '\[(?P<userId>[^\]]+)\]' .
  17. '\[(?P<sessionId>[^\]]+)\]' .
  18. '\[(?P<level>[^\]]+)\]' .
  19. '\[(?P<category>[^\]]+)\]' .
  20. ' (?P<text>.*?)' .
  21. '(\$\_(GET|POST|REQUEST|COOKIE|SERVER) = \[)?' .
  22. '$#i';
  23. if (preg_match($pattern, $row, $matches)) {
  24. if ($matches['text']) {
  25. $result[] = new LogRow([
  26. 'time' => $matches['time'],
  27. 'ip' => $matches['ip'],
  28. 'userId' => $matches['userId'],
  29. 'sessionId' =>
  30. $matches['sessionId'],
  31. 'level' => $matches['level'],
  32. 'category' => $matches['category'],
  33. 'text' => $matches['text'],
  34. ]);
  35. }
  36. }
  37. }
  38. fclose($handle);
  39. }
  40. return array_reverse($result);
  41. }
  42. }
  1. 添加一个帮助类,用于为日志等级展示美化的HTML-badges:
  1. <?php
  2. namespace app\modules\log\helpers;
  3. use yii\helpers\ArrayHelper;
  4. use yii\helpers\Html;
  5. class LogHelper
  6. {
  7. public static function levelLabel($level)
  8. {
  9. $classes = [
  10. 'error' => 'danger',
  11. 'warning' => 'warning',
  12. 'info' => 'primary',
  13. 'trace' => 'default',
  14. 'profile' => 'success',
  15. 'profile begin' => 'info',
  16. 'profile end' => 'info',
  17. ];
  18. $class = ArrayHelper::getValue($classes, $level,
  19. 'default');
  20. return Html::tag('span', Html::encode($level), ['class' => 'label-' . $class]);
  21. }
  22. }
  1. 创建一个模块控制器,它会从读取器中获取行的数组,并将他们传递给ArrayDataProvider
  1. <?php
  2. namespace app\modules\log\controllers;
  3. use app\modules\log\services\LogReader;
  4. use yii\data\ArrayDataProvider;
  5. use yii\web\Controller;
  6. class DefaultController extends Controller
  7. {
  8. public function actionIndex()
  9. {
  10. $reader = new LogReader();
  11. $dataProvider = new ArrayDataProvider([
  12. 'allModels' => $reader->getRows($this->getFile()),
  13. ]);
  14. return $this->render('index', [
  15. 'dataProvider' => $dataProvider,
  16. ]);
  17. }
  18. private function getFile()
  19. {
  20. return \Yii::getAlias($this->module->file);
  21. }
  22. }
  1. 现在,创建modules/log/default/index.php视图文件:
  1. <?php
  2. use app\modules\log\helpers\LogHelper;
  3. use app\modules\log\models\LogRow;
  4. use yii\grid\GridView;
  5. use yii\helpers\Html;
  6. /* @var $this yii\web\View */
  7. /* @var $dataProvider yii\data\ArrayDataProvider */
  8. $this->title = 'Application log';
  9. $this->params['breadcrumbs'][] = $this->title;
  10. ?>
  11. <div class="log-index">
  12. <h1><?= Html::encode($this->title) ?></h1>
  13. <?= GridView::widget([
  14. 'dataProvider' => $dataProvider,
  15. 'columns' => [
  16. [
  17. 'attribute' => 'time',
  18. 'format' => 'datetime',
  19. 'contentOptions' => [
  20. 'style' => 'white-space: nowrap',
  21. ],
  22. ],
  23. 'ip:text:IP',
  24. 'userId:text:User',
  25. [
  26. 'attribute' => 'level',
  27. 'value' => function (LogRow $row) {
  28. return LogHelper::levelLabel($row->level);
  29. },
  30. 'format' => 'raw',
  31. ],
  32. 'category',
  33. 'text',
  34. ],
  35. ]) ?>
  36. </div>
  1. 在文件config/web.php中附加模块到你的应用中:
  1. $config = [
  2. 'id' => 'basic',
  3. 'basePath' => dirname(__DIR__),
  4. 'bootstrap' => ['log'],
  5. 'modules' => [
  6. 'log' => 'app\modules\log\Module',
  7. ],
  8. 'components' => [
  9. ],
  10. //...
  11. ];
  1. views/layouts/main.php文件中添加一个到这个控制器的链接:
  1. echo Nav::widget([
  2. 'options' => ['class' => 'navbar-nav navbar-right'],
  3. 'items' => [
  4. ['label' => 'Home', 'url' => ['/site/index']],
  5. ['label' => 'Log', 'url' => ['/log/default/index']],
  6. ['label' => 'About', 'url' => ['/site/about']],
  7. ['label' => 'Contact', 'url' => ['/site/contact']],
  8. //...
  9. ],
  10. ]);
  11. NavBar::end();
  1. 访问/index.php?r=log,确保这个模块可以正常工作:

第八章 扩展Yii - 图7

工作原理…

你可以通过独立的模块来组织你的控制器、模型、视图和其它组件,并将他们附加到你的应用中。你可以使用Gii或者手动生成一个模块模板。

没一个模块包含一个主模块类,我们可以定义可配置的属性,定义修改路径,附加控制器等等。默认情况下,使用Gii生成的模块会运行默认控制器的index动作。

参考

创建一个自定义视图渲染器

这里有许多PHP模板引擎。Yii2只提供原生PHP模板。如果你想使用一个存在的模板引擎,或者创建你自己的,你需要实现它——当然,如果还没有被Yii社区实现的话。

在本小节中,我们将会重新实现Smarty模板支持。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
  2. 安装Smarty库:
  1. composer require smarty/smarty

如何做…

执行如下步骤,创建一个自定义视图渲染器:

  1. 创建smarty/ViewRenderer.php
  1. <?php
  2. namespace app\smarty;
  3. use Smarty;
  4. use Yii;
  5. class ViewRenderer extends \yii\base\ViewRenderer
  6. {
  7. public $cachePath = '@runtime/smarty/cache';
  8. public $compilePath = '@runtime/smarty/compile';
  9. /**
  10. * @var Smarty
  11. */
  12. private $smarty;
  13. public function init()
  14. {
  15. $this->smarty = new Smarty();
  16. $this->smarty->setCompileDir(Yii::getAlias($this->compilePath));
  17. $this->smarty->setCacheDir(Yii::getAlias($this->cachePath));
  18. $this->smarty->setTemplateDir([
  19. dirname(Yii::$app->getView()->getViewFile()),
  20. Yii::$app->getViewPath(),
  21. ]);
  22. }
  23. public function render($view, $file, $params)
  24. {
  25. $templateParams = empty($params) ? null : $params;
  26. $template = $this->smarty->createTemplate($file, null,
  27. null, $templateParams, false);
  28. $template->assign('app', \Yii::$app);
  29. $template->assign('this', $view);
  30. return $template->fetch();
  31. }
  32. }
  1. 现在,我们需要连接这个视图渲染器到应用中。在config/web.php文件中,我们需要添加视图组件的渲染器:
  1. 'components' => [
  2. //....
  3. 'view' => [
  4. 'renderers' => [
  5. 'tpl' => [
  6. 'class' => 'app\smarty\ViewRenderer',
  7. ],
  8. ],
  9. ],
  10. //...
  11. ];
  1. 现在让我们测试它。创建一个新的SmartyController
  1. <?php
  2. namespace app\controllers;
  3. use yii\web\Controller;
  4. class SmartyController extends Controller
  5. {
  6. public function actionIndex()
  7. {
  8. return $this->render('index.tpl', [
  9. 'name' => 'Bond',
  10. ]);
  11. }
  12. }
  1. 接下来,我们需要创建views/smarty/index.tpl视图:
  1. <div class="smarty-index">
  2. <h1>Smarty Example</h1>
  3. <p>Hello, {$name}!</p>
  4. </div>
  1. 尝试访问这个控制器。如果成功的话,你应该能得到如下输出:

第八章 扩展Yii - 图8

工作原理…

视图渲染器继承了yii\base\ViewRenderer抽象类,并只需要实现一个方法render

  1. <?php
  2. namespace yii\base;
  3. abstract class ViewRenderer extends Component
  4. {
  5. /**
  6. * Renders a view file.
  7. *
  8. * This method is invoked by [[View]] whenever it tries to render a view.
  9. * Child classes must implement this method to render the given view file.
  10. *
  11. * @param View $view the view object used for rendering the file.
  12. * @param string $file the view file.
  13. * @param array $params the parameters to be passed to the view file.
  14. * @return string the rendering result
  15. */
  16. abstract public function render($view, $file, $params);
  17. }

因此,我们会获得一个视图组件、文件路径和渲染变量。我们需要处理这个问题,并返回渲染的结果。在我们的例子中,处理它自己是通过Smarty模板引擎完成的,所以我们需要正确的初始化它,并调用它的处理方法:

  1. class ViewRenderer extends \yii\base\ViewRenderer
  2. {
  3. public $cachePath = '@runtime/smarty/cache';
  4. public $compilePath = '@runtime/smarty/compile';
  5. private $smarty;
  6. public function init()
  7. {
  8. $this->smarty = new Smarty();
  9. $this->smarty->setCompileDir(Yii::getAlias($this->compilePath));
  10. $this->smarty->setCacheDir(Yii::getAlias($this->cachePath));
  11. $this->smarty->setTemplateDir([
  12. dirname(Yii::$app->getView()->getViewFile()),
  13. Yii::$app->getViewPath(),
  14. ]);
  15. }
  16. //…
  17. }

将Yii临时文件存放在应用runtime文件夹中是一个好习惯。这就是为什么我们设置compile文件夹(Smarty存储它的编译为PHP的模板)到runtime/smarty/compile

渲染它自己非常简单:

  1. public function render($view, $file, $params)
  2. {
  3. $templateParams = empty($params) ? null : $params;
  4. $template = $this->smarty->createTemplate($file, null, null,
  5. $templateParams, false);
  6. $template->assign('app', \Yii::$app);
  7. $template->assign('this', $view);
  8. return $template->fetch();
  9. }

通过$this->render设置的数据被传递到Smarty模板。此外,我们创建特殊的Smarty模板变量,名叫appthis,它分别指向Yii:$appYii::$app->view,这允许我们在一个模板内部获取应用属性。

然后,我们就可以渲染这个模板了。

参考

你可以立刻使用带有插件和配置支持的Smarty视图渲染器,在https://github.com/yiisoft/yii2-smarty

为了了解更多关于Smarty和视图渲染器,参考如下地址:

创建一个多语言应用

每一天,我们会见越来越多的国际公司、软件产品和信息资源,他们发布的内容都是多语言的。Yii2提供内置i18n支持,用于制作多语言应用。

在本小节中,我们翻译应用接口到不同的语言上。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

  1. views/layouts/main.php文件修改主菜单标签,使用Yii::t('app/nav', '...')方法:
  1. echo Nav::widget([
  2. 'options' => ['class' => 'navbar-nav navbar-right'],
  3. 'items' => [
  4. ['label' => Yii::t('app/nav', 'Home'), 'url' => ['/site/index']],
  5. ['label' => Yii::t('app/nav', 'About'), 'url' => ['/site/about']],
  6. ['label' => Yii::t('app/nav', 'Contact'), 'url' => ['/site/contact']],
  7. //...
  8. ],
  9. ]);
  1. 修改所有的标题和面包屑,使用Yii::t('app, '...')方法:
  1. $this->title = Yii::t('app', 'Contact');
  2. $this->params['breadcrumbs'][] = $this->title;
  1. 此外,修改你所有按钮的标签:
  1. <div class="form-group">
  2. <?= Html::submitButton(Yii::t('app', 'Submit'), ['class' => 'btn btn-primary'']) ?>
  3. </div>

修改其它硬编码的信息:

  1. <p>
  2. <?= Yii::t('app', 'The above error occurred while the Web server was processing your request.') ?>
  3. </p>
  1. 修改你的LoginForm表单的属性标签:
  1. class LoginForm extends Model
  2. {
  3. ...
  4. public function attributeLabels()
  5. {
  6. return [
  7. 'username' => Yii::t('app/user', 'Username'),
  8. 'password' => Yii::t('app/user', 'Password'),
  9. 'rememberMe' => Yii::t('app/user', 'Remember Me'),
  10. ];
  11. }
  12. }

此外,修改ContactForm模型的属性标签:

  1. class ContactForm extends Model
  2. {
  3. ...
  4. public function attributeLabels()
  5. {
  6. return [
  7. 'name' => Yii::t('app/contact', 'Name'),
  8. 'email' => Yii::t('app/contact', 'Email'),
  9. 'subject' => Yii::t('app/contact', 'Subject'),
  10. 'body' => Yii::t('app/contact', 'Body'),
  11. 'verifyCode' => Yii::t('app', 'Verification Code'),
  12. ];
  13. }
  14. }

它会输出翻译后的标签。

  1. 为了准备翻译,创建messages目录。马上,我们可以为所有需要的语言创建翻译文件。我们可以手动制作,但是有一个很有用的爬虫,它会扫描所有的项目文件,并为所有的消息构建Yii::t()构造。我们来使用它。
  2. 为消息扫描器生成配置文件:
  1. ./yii message/config-template config/messages.php
  1. 打开配置文件,设置如下值:
  1. <?php
  2. return [
  3. 'sourcePath' => '@app',
  4. 'languages' => ['de', 'fr'],
  5. 'translator' => 'Yii::t',
  6. 'sort' => false,
  7. 'removeUnused' => false,
  8. 'markUnused' => true,
  9. 'only' => ['*.php'],
  10. 'except' => [
  11. '.svn',
  12. '.git',
  13. '.gitignore',
  14. '.gitkeep',
  15. '.hgignore',
  16. '.hgkeep',
  17. '/messages',
  18. '/vendor',
  19. ],
  20. 'format' => 'php',
  21. 'messagePath' => '@app/messages',
  22. 'overwrite' => true,
  23. 'ignoreCategories' => [
  24. 'yii',
  25. ],
  26. ];
  1. 运行爬虫,并将这个配置文件传递给它:
  1. ./yii message config/messages.php
  1. 在处理过以后,我们能得到如下文件夹结构:
  1. messages
  2. ├── de
  3. ├── app
  4. ├── contact.php
  5. ├── nav.php
  6. └── user.php
  7. └── app.php
  8. └── fr
  9. ├── app
  10. ├── contact.php
  11. ├── nav.php
  12. └── user.php
  13. └── app.php
  1. 例如,messages/de/app/contact包含如下内容:
  1. <?php
  2. ...
  3. return [
  4. 'Body' => '',
  5. 'Email' => '',
  6. 'Name' => '',
  7. 'Subject' => '',
  8. ];
  1. 它是一个纯PHP数组,原始的句子会放在keys的的位置,翻译后的消息会放在values的位置上。
  2. 只需要将翻译成德语的内容放在值的位置上:
  1. <?php
  2. ...
  3. return [
  4. 'Password' => 'Passwort',
  5. 'Remember Me' => 'Erinnere dich an mich',
  6. 'Username' => 'Benutzername',
  7. ];
  1. config/web.php文件中附加这些翻译到应用的i18n组件上:
  1. $config = [
  2. 'id' => 'basic',
  3. 'basePath' => dirname(__DIR__),
  4. 'bootstrap' => ['log'],
  5. 'components' => [
  6. //…
  7. 'i18n' => [
  8. 'translations' => [
  9. 'app*' => [
  10. 'class' => 'yii\i18n\PhpMessageSource',
  11. 'sourceLanguage' => 'en-US',
  12. ],
  13. ],
  14. ],
  15. 'db' => require(__DIR__ . '/db.php'),
  16. ],
  17. 'params' => $params,
  18. ];
  1. 打开登录页面,会有默认语言:

第八章 扩展Yii - 图9

  1. 修改应用语言为de
  1. $config = [
  2. 'id' => 'basic',
  3. 'language' => 'de',
  4. 'basePath' => dirname(__DIR__),
  5. 'bootstrap' => ['log'],
  6. ...
  7. ];

然后刷新登录页面:

第八章 扩展Yii - 图10

  1. 内置的框架消息和默认校验错误也会被自动翻译。

工作原理…

Yii2提供Yii::t()方法,用于通过i18n组件翻译接口消息,它支持不用的源类型。在本小节中,我们使用的是yii\i18n\hpMessageSource,它用纯PHP文件存储了翻译后的消息。

这个框架没有人工智能,不支持自己翻译消息。你必须在文件或者在数据库中放好准备好的翻译,框架会从消息源中获取需要的信息。

你可以手动设置当前语言:

  1. $config = [
  2. 'id' => 'basic',
  3. 'language' => 'de',
  4. ...
  5. ];

如果不在配置文件中设置语言的话,你可以在运行时设置应用语言:

  1. Yii::$app->language = 'fr';

例如,如果你存放用户语言在User模型的lang字段上,你可以创建语言加载器:

  1. <?php
  2. namespace app\bootstrap;
  3. use yii\base\BootstrapInterface;
  4. class LanguageBootstrap implements BootstrapInterface
  5. {
  6. public function bootstrap($app)
  7. {
  8. if (!$app->user->isGuest) {
  9. $app->language = $app->user->identity->lang;
  10. }
  11. }
  12. }

在bootstraping列表中注册这个类:

  1. $config = [
  2. 'id' => 'basic',
  3. 'basePath' => dirname(__DIR__),
  4. 'bootstrap' => ['log', 'app'bootstrap\LanguageBoostrap'],
  5. ...
  6. ];

现在,每一个认证的用户将会看到他们自己语言的界面。

此外,你可以复写yii\web\UrlManager,用于将当前语言作为GET参数传递,或者是一个URL的前缀方式传递。此外,作为备选项,你可以在浏览器的cookie中存放选择的语言。

当你使用Gii生成模型或者其它代码时,你可以检查如下选项:

第八章 扩展Yii - 图11

在生成的代码中,所有的标签都被嵌入到Yii::t()中。

注意:本小节中我们没有讨论模型内容的翻译。但是,例如,你可以在数据库中独立的表中存放翻译后的文本(例如post_lang表存放帖子模型表),并使用Yii::$app->language属性,获取当前语言,并为你的模型提取需要的内容。

参考

欲了解Yii2中更多关于国际化的信息,参考http://www.yiiframework.com/doc-2.0/guide-tutorial-i18n.html

制作可发布的扩展

在本章中,你学到了如何创建各种类型的Yii扩展。现在我们来讨论如何分享你的结果,以及为什么这是重要的。

准备

首先为一个好的扩展准备一个清单。一个好的编程产品应该遵守如下点:

  • 好的代码风格
  • 人们应该找到它
  • 一致性,易读,易使用的API
  • 好的文档
  • 扩展应该应用到大部分常用的使用例子上
  • 应该被维护
  • 充分被测试,理想情况下使用单元测试
  • 你需要为它提供支持

当然,所有这些需要很多工作,但是他们是创建一个好产品所必需的的。

如何做…

  1. 每一个现代PHP产品应该遵守自动加载的PSR4标准,以及编码风格的PSR1和PSR2标准,这些标准参考指南http://www.php-fig.org/psr/
  2. 详细回顾我们的清单,从API开始,API应该有一致性,易读易使用。一致性意味着所有的风格不会改变,变量名不变,没有不一致的名称,例如isFlag1()isNotFlag2()等等。每一件事应该遵守你为你代码定义的规则。它会让你很少查看文档,把精力集中在编码上。
  3. 没有文档的代码几乎是没有用的。一个例外是相对简单的代码,但是尽管只是很少几行,如果没有一些单词说明如何安装和如何使用,感觉并不会很好。什么构成了好文档?代码的目的和他的赞成者尽可能可见,并被明显和清晰的写出来。
  4. 如果开发者不知道在哪里放置它,以及如何在应用配置中使用它,那么这个代码是没有用的。不要期望人们知道如何做框架相关的事情。安装指南应该是啰嗦的。大部分开发者都喜欢手把手的形式。如果代码需要SQL schema才能工作,就提供他们。
  5. 尽管你的API方法和属性正确的被命名了,你仍然需要使用PHPDoc注释为他们加文档,指出参数的类型和返回类型,为每一个方法提供一个简洁的描述。不要忘记受保护的和私有方法以及属性,因为这对于阅读代码和理解代码是如何工作的细节是非常有帮助的。此外,考虑在文档中列出公共方法和属性,这样它可以作为一个引用被使用。
  6. 提供被充分注释的示例案例。尝试覆盖这个扩展大部分使用方法。
  7. 在一个例子中,不要尝试一次去解决多个问题,因为这可能会让人困惑。
  8. 让你的代码更灵活非常重要,这样它就会应用到需要使用情况中。但是,因为不能为每一种使用情况创建代码,尝试覆盖大部分的情况。
  9. 让人感觉舒服很重要。提供一个良好的文档是第一步。第二步是提供一个证明,说明你的代码能按预期工作,如果未来更新来可以。最好的方式是提供一组单元测试。
  10. 扩展应该被维护,至少它是稳定的,没有更多的特性请求和bug报告。所以期望问题和报告,并保留一些时间为代码的未来工作。如果你没有更多的时间来维护扩展,但它又是非常创新的,没有在此之前做过它,它仍然是值得分享的。如果社区喜欢它,就会有人提供帮助。
  11. 最后,你需要让扩展可用。为你的扩展创建Composer包,将它放在Github或者其它分享平台上,并将它发布在https://packagist.org网站上。
  12. 每一个扩展应该有一个版本号,以及一个修改日志。它能让社区检查他们是否用的最新版本,以及在升级前检查修改了什么。我们建议使用http://semver.org网站上提供的语义化版本规则。
  13. 尽管你的扩展相对简单,并且文档也很好,但仍然会有人在第一次使用时提问题,而能回答的人只能是你。典型情况下,问题会在官方论坛上提出,所以最好创建一个主题,这样人们可以讨论你的代码并在这个扩展页面上提供这个链接。

工作原理…

如果你想分享一个扩展到社区,并确定它是有用的和流行的,你需要做的不只是写代码。制作可发布的扩展有非常多的工作要做。甚至多于制作扩展本身。所以,why is it good to share extensions with the community in the first place?

将你自己项目中的使用的代码开源有它的赞成者。你能让人们,很多人测试你的闭源项目。使用你扩展的人在测试它,给出有价值的反馈,以及报告bug。如果你的代码是流行的,将会有热情的开发者尝试提高你的代码,让它更可扩展、更稳定以及可复用。而且,你将会感觉很爽,因为你做了一件好事。

我们覆盖了大部分重要的事情。此外,有更多的事情需要检查。在写自己的扩展前尝试用已有的扩展。如果一个扩展已经非常合适了,尝试联系这个扩展的作者,并贡献你自己的想法。检查已有的代码能帮助你找到有用的技巧、需要做什么以及不应该做什么。此外,不时地检查wiki文档,和官方论坛;这里有非常多有用的信息,关于创建扩展和使用Yii进行开发。

参考