您正在查看: 标签 yii2 下的文章

yii2 migrate模板分析

本教程由四哥许坤原创。转载请注明出处。四哥许坤唯一官方博客:http://blog.kunx.org


数据库迁移是yii2中的一个很好的功能,当然现在很多框架都有了。

你是不是一直好奇到底都是可以使用什么规则生成不同的脚本吧


在yii\console\controllers\MigrateController中,有一个方法就是来根据你命令的不同调用不同的模板的:


/**
 * @inheritdoc
 * @since 2.0.8
 */
protected function generateMigrationSourceCode($params)
{
    $parsedFields = $this->parseFields();
    $fields = $parsedFields['fields'];
    $foreignKeys = $parsedFields['foreignKeys'];
    $name = $params['name'];
    $templateFile = $this->templateFile;
    $table = null;
    if (preg_match('/^create_junction(?:_table_for_|_for_|_)(.+)_and_(.+)_tables?$/', $name, $matches)) {
        $templateFile = $this->generatorTemplateFiles['create_junction'];
        $firstTable = mb_strtolower($matches[1], Yii::$app->charset);
        $secondTable = mb_strtolower($matches[2], Yii::$app->charset);
        $fields = array_merge(
            [
                [
                    'property' => $firstTable . '_id',
                    'decorators' => 'integer()',
                ],
                [
                    'property' => $secondTable . '_id',
                    'decorators' => 'integer()',
                ],
            ],
            $fields,
            [
                [
                    'property' => 'PRIMARY KEY(' .
                        $firstTable . '_id, ' .
                        $secondTable . '_id)',
                ],
            ]
        );
        $foreignKeys[$firstTable . '_id'] = $firstTable;
        $foreignKeys[$secondTable . '_id'] = $secondTable;
        $table = $firstTable . '_' . $secondTable;
    } elseif (preg_match('/^add_(.+)_columns?_to_(.+)_table$/', $name, $matches)) {
        $templateFile = $this->generatorTemplateFiles['add_column'];
        $table = mb_strtolower($matches[2], Yii::$app->charset);
    } elseif (preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches)) {
        $templateFile = $this->generatorTemplateFiles['drop_column'];
        $table = mb_strtolower($matches[2], Yii::$app->charset);
    } elseif (preg_match('/^create_(.+)_table$/', $name, $matches)) {
        $this->addDefaultPrimaryKey($fields);
        $templateFile = $this->generatorTemplateFiles['create_table'];
        $table = mb_strtolower($matches[1], Yii::$app->charset);
    } elseif (preg_match('/^drop_(.+)_table$/', $name, $matches)) {
        $this->addDefaultPrimaryKey($fields);
        $templateFile = $this->generatorTemplateFiles['drop_table'];
        $table = mb_strtolower($matches[1], Yii::$app->charset);
    }
    foreach ($foreignKeys as $column => $relatedTable) {
        $foreignKeys[$column] = [
            'idx' => $this->generateTableName("idx-$table-$column"),
            'fk' => $this->generateTableName("fk-$table-$column"),
            'relatedTable' => $this->generateTableName($relatedTable),
        ];
    }
    return $this->renderFile(Yii::getAlias($templateFile), array_merge($params, [
        'table' => $this->generateTableName($table),
        'fields' => $fields,
        'foreignKeys' => $foreignKeys,
    ]));
}


create_xxx_table调用create_table对应的模板,其余的和这个类似,自己试试看吧。


yii2中自定义验证码图片输出

本资料由四哥许坤整理发布,转载请注明出处。让我们共同构建和谐、开放IT环境,做文明IT人。四哥许坤唯一官方博客http://blog.kunx.org


有时候我们并不希望我们的验证码都交由SiteController来处理,比如我就会选在UserController中输出一个验证码,其使用方式如下

在actions()方法中定义captcha用于渲染验证码图片,保存验证码到session

public function actions()
{
    return [
         'captcha' => [
            'class' => 'yii\captcha\CaptchaAction',
            'minLength'=>3,
            'maxLength'=>3,
        ],
    ];
}

然后在视图中使用这个操作方法进行输出

echo $form->field($model, 'verify')->widget(Captcha::className(),[
                    'captchaAction'=>'user/captcha',
]);

验证规则配置在模型中

public function rules() {
    return [
        ['verify', 'captcha','captchaAction'=>'user/captcha'],
    ];
}

最重要的就是captchaAction了,原因在于:

这个验证规则是内置的验证类叫做

blob.png

 

最终验证会调用这个类中的validateValue()方法

该方法代码如下:

protected function validateValue($value)
{
    $captcha = $this->createCaptchaAction();
    $valid = !is_array($value) && $captcha->validate($value, $this->caseSensitive);
    return $valid ? null : [$this->message, []];
}

这里面的value形参就是用户输入的验证码

第一行就创建了一个验证操作对象

对应的代码如下:

public function createCaptchaAction()
{
    $ca = Yii::$app->createController($this->captchaAction);
    if ($ca !== false) {
        /* @var $controller \yii\base\Controller */
        list($controller, $actionID) = $ca;
        $action = $controller->createAction($actionID);
        if ($action !== null) {
            return $action;
        }
    }
    throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction);
}

第一行就是问题的重点

他会创建一个控制器对象,这个对象是当前类的captchaAction属性对应的,这个值应当和user/captcha一致,否则就会调用别的控制器验证码保存的session值,但是其默认值并不是user/captcha

public $captchaAction = 'site/captcha';

所以我们要在定义验证规则的时候指定这个属性配置,才能避免验证码老是错误的问题.

 


yii2自动加载机制(一)composer自动加载器

本内容由四哥许坤原创发布,转载需注明出处。四哥许坤唯一官方博客:http://blog.kunx.org

Yii2的composer自动加载器UML图

结论:

最终composer注册的自动加载机制是ClassLoader::loadClass()


至于ComposerAutoloaderInit0d1d3c13f80202964b8b82ff7f2fb863和ComposerStaticInit0d1d3c13f80202964b8b82ff7f2fb863这两个完全是为了注册上面的那个,并进行配置自动加载规则。名字后面乱七八糟的字符串是为了避免和其它类重名,可以无视。


blob.png

画图实在是痛苦,尤其是调整标注位置什么的,线有点没有对齐,将就看吧。

如果大家有比较好用的制图工具,欢迎推荐,非常感谢。


当你的环境>=5.6并且没有使用HHVM的情况下,composer的加载机制如下:

1.进行psr4检查,获取用户使用类名,去掉命名空间的开头斜线

2.将类名的\转换为系统的路径分隔符,然后在后面拼凑上.php

3.获取类名的首字母,并在prefixLengthsPsr4属性中查看是否有这个前缀的,如果有进入4,否则进入6

4.遍历prefixLengthsPsr4中的每一个元素,看用户使用的类名以哪个元素的键名相同,如果有进入5

5.遍历prefixDirsPsr4中的对应元素,尝试拼凑一个完整的文件路径,如果有该文件,返回,如果没有,进入fallbackDirsPsr4属性下元素的测试,当前环境下数组为空

6.检查psr0和fallbackDirsPsr0检查,和上面的检查逻辑一致

7.找到了就加载,找不到什么都不做



加了点水印,但是不影响观看,谢谢理解。

yii2 init初始化脚本分析

本内容由四哥许坤原创发布,转载需注明出处。四哥许坤唯一官方博客:http://blog.kunx.org

在yii2中有一个非常好用的应用初始化脚本,用于创建各种配置和入口文件

这个脚本是init没有后缀名,在windows下你可以通过以下两种方式调用它:

第一种,通过bat文件再调用init:

path-to-yii2>init
path-to-yii2>init.bat

blob.png

第二种,直接使用php执行init:

path-to-yii2>php -f init
path-to-yii2>php init

blob.png

那么到底init执行了什么呢

首先,获取脚本执行时输入的参数

接着,引入配置了各种环境配置的数组

接着,获取当前指定的环境的配置数组

接着,遍历数组中指定的路径里的所有文件列表

接着,拷贝并覆盖应用下的同名文件

最后,执行收尾工作,对拷贝过去的文件或目录执行后续操作

blob.png

根据这种流程,我们在使用中最好通过修改环境配置文件的方式来快速的切换,你不用担心*-local.php会被提交,因为yii2已经在git中过滤了这些

这样做有诸多好处,比如在工作中常见有多个环境,如开发、测试、生产,我们测试环境和生产环境使用的调试模式等等肯定是不一样的,这样我们就可以通过快速使用init切换来达到快速部署的目的。


需要注意的是:

环境配置应当是存放各环境不同的部分

不应该有敏感信息,敏感信息只需要使用空字符串占位就行,也就是提供结构,由部署者再次修改即可

重新生成会导致用户cookie自动登陆令牌失效。当然经常更换cookie的密钥是安全的做法,但是会带来用户需要再次登陆的问题,解决方案如下:

    在环境配置数组中setCookieValidationKey部分删除不想重置cookie密钥的文件名即可。

附一个配置文件

blob.png

index.php

<?php
/**
 * The manifest of files that are local to specific environment.
 * This file returns a list of environments that the application
 * may be installed under. The returned data must be in the following
 * format:
 *
 * ```php
 * return [
 *     'environment name' => [
 *         'path' => 'directory storing the local files',
 *         'skipFiles'  => [
 *             // list of files that should only copied once and skipped if they already exist
 *         ],
 *         'setWritable' => [
 *             // list of directories that should be set writable
 *         ],
 *         'setExecutable' => [
 *             // list of files that should be set executable
 *         ],
 *         'setCookieValidationKey' => [
 *             // list of config files that need to be inserted with automatically generated cookie validation keys
 *         ],
 *         'createSymlink' => [
 *             // list of symlinks to be created. Keys are symlinks, and values are the targets.
 *         ],
 *     ],
 * ];
 * ```
 */
return [
    'Development' => [
        'path' => 'dev',
        'setWritable' => [
            'backend/runtime',
            'backend/web/assets',
            'frontend/runtime',
            'frontend/web/assets',
        ],
        'setExecutable' => [
            'yii',
            'tests/codeception/bin/yii',
        ],
        'setCookieValidationKey' => [
            'backend/config/main-local.php',
            'frontend/config/main-local.php',
        ],
    ],
    'Production' => [
        'path' => 'prod',
        'setWritable' => [
            'backend/runtime',
            'backend/web/assets',
            'frontend/runtime',
            'frontend/web/assets',
        ],
        'setExecutable' => [
            'yii',
        ],
        'setCookieValidationKey' => [
            'backend/config/main-local.php',
            'frontend/config/main-local.php',
        ],
    ],
    'Kunx' => [
        'path' => 'kunx',//在environments下创建这样一个目录,然后根据init所在目录下的结构创建文件
        'setWritable' => [
            'backend/runtime',
            'backend/web/assets',
            'frontend/runtime',
            'frontend/web/assets',
        ],
        'setExecutable' => [
            'yii',
        ],
        'setCookieValidationKey' => [
            'backend/config/main-local.php',
            'frontend/config/main-local.php',
        ],
    ],
];

再次执行init命令

blob.png


附中文注释

init

#!/usr/bin/env php
<?php
/**
 * Yii Application Initialization Tool
 *
 * In order to run in non-interactive mode:
 *
 * init --env=Development --overwrite=n
 *
 * @author Alexander Makarov <sam@rmcreative.ru>
 *
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

if (!extension_loaded('openssl')) {
    die('The OpenSSL PHP extension is required by Yii2.');
}

//获取请求参数
$params = getParams();

//获取当前目录绝对路径
$root = str_replace('\\', '/', __DIR__);

//引入environments/index.php该文件中保存的是各种环境下的配置
$envs = require("$root/environments/index.php");
$envNames = array_keys($envs);

echo "Yii Application Initialization Tool v1.0\n\n";

$envName = null;
if (empty($params['env']) || $params['env'] === '1') {
    echo "Which environment do you want the application to be initialized in?\n\n";
    foreach ($envNames as $i => $name) {
        echo "  [$i] $name\n";
    }
    echo "\n  Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] ';
    $answer = trim(fgets(STDIN));

    if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) {
        echo "\n  Quit initialization.\n";
        exit(0);
    }

    if (isset($envNames[$answer])) {
        $envName = $envNames[$answer];
    }
} else {
    $envName = $params['env'];
}

if (!in_array($envName, $envNames)) {
    $envsList = implode(', ', $envNames);
    echo "\n  $envName is not a valid environment. Try one of the following: $envsList. \n";
    exit(2);
}

$env = $envs[$envName];

if (empty($params['env'])) {
    echo "\n  Initialize the application under '{$envNames[$answer]}' environment? [yes|no] ";
    $answer = trim(fgets(STDIN));
    if (strncasecmp($answer, 'y', 1)) {
        echo "\n  Quit initialization.\n";
        exit(0);
    }
}

echo "\n  Start initialization ...\n\n";

//获取配置中path下的所有文件
$files = getFileList("$root/environments/{$env['path']}");
if (isset($env['skipFiles'])) {
    $skipFiles = $env['skipFiles'];
    //将忽略文件路径转变为绝对路径
    array_walk($skipFiles, function(&$value) use($env, $root) { $value = "$root/$value"; });
    //从files中去除配置中忽略的文件
    $files = array_diff($files, array_intersect_key($env['skipFiles'], array_filter($skipFiles, 'file_exists')));
}
$all = false;

//拷贝环境文件到应用
foreach ($files as $file) {
    if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) {
        break;
    }
}

$callbacks = ['setCookieValidationKey', 'setWritable', 'setExecutable', 'createSymlink'];
/**
 * 后续处理,比如cookie验证所使用的密钥,目录写权限,命令行工具yii等文件的可执行,以及建立符号链接
 */
foreach ($callbacks as $callback) {
    if (!empty($env[$callback])) {
        $callback($root, $env[$callback]);
    }
}

//执行完毕结束
echo "\n  ... initialization completed.\n\n";

function getFileList($root, $basePath = '')
{
    $files = [];
    $handle = opendir($root);
    while (($path = readdir($handle)) !== false) {
        if ($path === '.git' || $path === '.svn' || $path === '.' || $path === '..') {
            continue;
        }
        $fullPath = "$root/$path";
        $relativePath = $basePath === '' ? $path : "$basePath/$path";
        if (is_dir($fullPath)) {
            $files = array_merge($files, getFileList($fullPath, $relativePath));
        } else {
            $files[] = $relativePath;
        }
    }
    closedir($handle);
    return $files;
}

function copyFile($root, $source, $target, &$all, $params)
{
    if (!is_file($root . '/' . $source)) {
        echo "       skip $target ($source not exist)\n";
        return true;
    }
    if (is_file($root . '/' . $target)) {
        if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) {
            echo "  unchanged $target\n";
            return true;
        }
        if ($all) {
            echo "  overwrite $target\n";
        } else {
            echo "      exist $target\n";
            echo "            ...overwrite? [Yes|No|All|Quit] ";


            $answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN));
            if (!strncasecmp($answer, 'q', 1)) {
                return false;
            } else {
                if (!strncasecmp($answer, 'y', 1)) {
                    echo "  overwrite $target\n";
                } else {
                    if (!strncasecmp($answer, 'a', 1)) {
                        echo "  overwrite $target\n";
                        $all = true;
                    } else {
                        echo "       skip $target\n";
                        return true;
                    }
                }
            }
        }
        file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
        return true;
    }
    echo "   generate $target\n";
    @mkdir(dirname($root . '/' . $target), 0777, true);
    file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
    return true;
}

/**
 * 获取cli下的请求参数
 * @return array
 */
function getParams()
{
    $rawParams = [];
    //请求参数使用空格分隔,比如init arg1 arg2 arg3=kunx
    if (isset($_SERVER['argv'])) {
        $rawParams = $_SERVER['argv'];
        //第一个元素是脚本路径,不需要
        array_shift($rawParams);
    }
    
    $params = [];
    /**
     * 将上面的参数字符串转换为参数数组,比如上面的参数会转换为:
     * [
     *  'arg1',
     *  'arg2',
     *  'arg3'=>'kunx',
     * ]
     */
    foreach ($rawParams as $param) {
        if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) {
            $name = $matches[1];
            $params[$name] = isset($matches[3]) ? $matches[3] : true;
        } else {
            $params[] = $param;
        }
    }
    return $params;
}

function setWritable($root, $paths)
{
    foreach ($paths as $writable) {
        if (is_dir("$root/$writable")) {
            echo "      chmod 0777 $writable\n";
            @chmod("$root/$writable", 0777);
        } else {
            echo "\n  Error. Directory $writable does not exist. \n";
        }
    }
}

function setExecutable($root, $paths)
{
    foreach ($paths as $executable) {
        echo "      chmod 0755 $executable\n";
        @chmod("$root/$executable", 0755);
    }
}

function setCookieValidationKey($root, $paths)
{
    foreach ($paths as $file) {
        echo "   generate cookie validation key in $file\n";
        $file = $root . '/' . $file;
        $length = 32;
        $bytes = openssl_random_pseudo_bytes($length);
        $key = strtr(substr(base64_encode($bytes), 0, $length), '+/=', '_-.');
        $content = preg_replace('/(("|\')cookieValidationKey("|\')\s*=>\s*)(""|\'\')/', "\\1'$key'", file_get_contents($file));
        file_put_contents($file, $content);
    }
}

function createSymlink($root, $links) {
    foreach ($links as $link => $target) {
        echo "      symlink " . $root . "/" . $target . " " . $root . "/" . $link . "\n";
        //first removing folders to avoid errors if the folder already exists
        @rmdir($root . "/" . $link);
        //next removing existing symlink in order to update the target
        if (is_link($root . "/" . $link)) {
            @unlink($root . "/" . $link);
        }
        @symlink($root . "/" . $target, $root . "/" . $link);
    }
}


Yii2在所有操作前执行逻辑

我们经常会有这样的疑问,如何让所有的操作之前都执行某一个逻辑。

以前常采用的方式是,抽出一个基础控制器类,比如\backend\controllers\BaseController,然后在这个基础控制器的beforeAction方法中完成各种逻辑(当然可以是事件绑定和触发),这个当然是可以的,不过不太想抽基础控制器,想在配置文件中解决。

先说说结论,在所有的操作执行之前都会触发一个事件,叫做beforeAction,只需要在应用配置数组的第一维添加

'on beforeAction'=>[\backend\behaviors\CheckPermission::className(),'kunxTest']


以下是我的实例配置文件

main.php

$params = array_merge(
        require(__DIR__ . '/../../common/config/params.php'), require(__DIR__ . '/../../common/config/params-local.php'), require(__DIR__ . '/params.php'), require(__DIR__ . '/params-local.php')
);
return [
    'id'                  => 'back-frontend',
    ...
    'components'          => [
        ...
    ],
    'on beforeAction'     => [
        \frontend\behaviors\CheckPermission::className(), 'kunxTest',
    ],
    ...
];

CheckPermission这个类继承之Object类,这样就可以使用className方法了

可惜的是在配置文件中,一个事件不能绑定多个handler