前言目前使用的垃圾评论过滤插件在添加词库时十分繁琐，需要进入插件的设置页面才能添加。但从操作的角度上来说，直接在评论管理页面进行操作是最符合直觉的，所以有了这款插件。功能兼容性要求：依赖 Typecho 1.2.0+ 版本，利用命名空间功能。无需修改数据库结构：通过插件配置表实现拦截功能，安全高效。便捷操作：支持直接在评论管理页面一键添加屏蔽词，操作直观。智能处理：当新增屏蔽关键词时，自动处理符合条件的待审评论，节省人工审核时间。属性联动拦截：插件可自动检测评论中被拉黑的属性。例如，若拉黑某个 IP 地址，可自动屏蔽该 IP 提交的所有评论内容，并将其加入屏蔽词库。详细统计：在插件管理页面显示以下统计信息：已定义的过滤规则总数累计拦截次数自动收集的屏蔽词数量适配移动端：注入拉黑操作按钮时，适配移动端小屏幕自定义提示：多种拦截提示可选，也可自定义提示内容界面评论管理页面拉黑操作插件配置页面下载去码云下载插件
Yii 2 开发文档，主要参考 Yii 2.0 权威指南 两双筷子: https://www.dbkuaizi.comYii 2 安装Composercomposer create-project --prefer-dist yiisoft/yii2-app-basic basicdownload下载地址：https://www.yiiframework.com/download修改 config/web.php 文件，给 cookieValidationKey 配置项 添加一个密钥。（使用 Composre 安装自动完成该步骤）'cookieValidationKey' => '在此处输入你的密钥',内置服务器使用 serve 命令启动 php 内置服务器，启动后可通过：http://localhost:8080/ 访问php yii serve::: alert-info默认情况下会使用 8080 端口，如果需要手动指定端口可以 加上 --port 来指定:::php yii serve --port=8888基础目录结构有部分精简，主要列出核心的目录结构 ├── assets // 目录用于存放前端资源包PHP类 ├── codeception.yml ├── commands // 控制器 ├── config // 应用配置及其他配置 │ ├── console.php // 控制台应用配置信息 │ ├── db.php // 数据库配置文件 │ ├── params.php // 参数配置文件 │ ├── test.php │ ├── test_db.php │ └── web.php // Web 应用配置信息 ├── controllers // 用于存放控制器的文件夹 ├── mail // 与邮件相关的布局文件 ├── models // 模型文件夹 ├── runtime // 记录日志和缓存，要求权限 777 ├── tests // 用于存放一些测试类 ├── vendor // composer 包及 Yii 框架核心 ├── views // 视图目录 ├── web // web服务根目录（同public） │ ├── index.php // 入口文件 ├── widgets // 存放一些常用的小挂件的类文件 ├── yii // yii 控制台入口 （同 think 与 artisan ） └── yii.bat // 与 yii 文件 功能一致，运行时可省略开头的 php 框架请求流程用户向入口脚本 web/index.php 发起请求。入口脚本加载应用配置并创建一个应用 实例去处理请求。应用通过请求组件解析请求的 路由。应用创建一个控制器实例去处理请求。控制器创建一个动作实例并针对操作执行过滤器。如果任何一个过滤器返回失败，则动作取消。如果所有过滤器都通过，动作将被执行。动作会加载一个数据模型，或许是来自数据库。动作会渲染一个视图，把数据模型提供给它。渲染结果返回给响应组件。响应组件发送渲染结果给用户浏览器。模块模块是一个独立的软件单元，由 模型、视图、控制器和其他组件组成，终端用户可以访问应用主体中已安装的模块的控制器，模块应该当作小应用主体来看待，和应用主体不同的是，模块不能单独部署，必须属于某个应用主体。可以看作是 ThinkPHP 的多应用模式模块的目录结构modules // 模块目录 └── admin // admin 模块 ├── controllers // 控制器目录 │ └── DefaultController.php // 模块默认控制器 ├── models // 模型目录 ├── Module.php // 模块 └── views // 视图目录 └── default └── index.php模块构建有两种方法，一种是 使用 Gii 工具创建，另一种是手动创建。Gii 创建模块 参考：https://www.cnblogs.com/liuwanqiu/p/6815817.html以 Admin 模块为例创建模块类<?php modules/admin/Module.php namespace app\modules\admin; class Module extends \yii\base\Module { public $controllerNamespace = 'app\modules\admin\controllers'; public function init() { parent::init(); // 在这里可以引入模块自己的配置项 \Yii::configure($this, require __DIR__ . '/config.php'); } }给模块定义一个控制器// modules/admin/controllers/DefaultController.php <?php namespace app\modules\admin\controllers; use yii\web\Controller; class DefaultController extends Controller { public function actionIndex() { return $this->render('index'); } } 通过命令调用模块php yii <模块名>/<控制器>/<操作>URL 重写Nginx URl 重写if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?s=$1 last; break; }请求 Request请求数据::: alert-warning当获取的参数不存在时，返回 NULL:::GET 请求$request = Yii::$app->request; // 获取所有 get 数据 $request->get(); // 获取 get 中的 id $id = $request->get('id'); // 获取 get 中的 type,如果未传递 就返回 default $type = $request->get('type','default');POST 请求与 GET用法 一致$request = Yii::$app->request; $post = $request->post(); $name = $request->post('name'); $name = $request->post('name','default');所有参数有时候前端传参除了 POST GET 外还会用 POST，PUT，PATCH 等其他方法传递。可以使用 getBodyParam 获取$request = Yii::$app->request; // 返回所有参数 $params = $request->bodyParams; // 返回参数 "id" $param = $request->getBodyParam('id');请求方法获取当前请求使用的HTTP方法，也可以直接使用方法进行判断$request = Yii::$app->request; // 获取当前请求的方法 $request->method; if ($request->isAjax) { /* 该请求是一个 AJAX 请求 */ } if ($request->isGet) { /* 请求方法是 GET */ } if ($request->isPost) { /* 请求方法是 POST */ } if ($request->isPut) { /* 请求方法是 PUT */ }请求URLYii 提供了一些成员属性用来获取 URL 的信息，我们以 http://example.com/admin/index.php/product?id=100 为例：属性名说明$request->url获取URL，不包含主机部分：返回 /admin/index.php/product?id=100$request->absoluteUrl获取完整URL返回：http://example.com/admin/index.php/product?id=100$request->hostInfo获取主机部分，返回 http://example.com$request->pathInfo获取入口文件之后的路径部分，返回 /product$request->queryString获取 问号后面的部分，返回 id=100$request->baseUrl获取 主机之后，入口脚本之前的内容，返回 /admin$request->scriptUrl获取没有路径信息和查询字符串的部分，返回 /admin/index.php$request->serverName获取主机名称，返回 example.com$request->serverPort获取客户端接口Header 头获取 Header 的方法$headers = Yii::$app->request->headers; // 获取 header 头中，Accept 的值 $headers->get('Accept'); // 判断 header 头是否存在 $headers->has('Token');客户端信息可以通过 userHost 和 userIP 来获取客户端的 信息// 获取主机名 Yii::$app->request->userHost; // 获取 ip Yii::$app->request->userIP;数据库操作数据库配置在配置文件中 配置数据的 dsn 以及账户密码'db' => [ 'class' => 'yii\db\Connection', 'driverName' => 'mysql', 'dsn' => ' mysql:host=localhost;dbname=mydatabase', 'username' => 'root', 'password' => '', ],配置完成之后就可以使用 Yii::$app->db 来操作数据库了创建数据库连接如果只需要在某个地方单独使用数据库，创建一个 yii\db\Connection 实例来与之建立连接。$db = new yii\db\Connection([ 'dsn' => 'mysql:host=localhost;dbname=example', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]);查询数据库查询多条数据返回二维数组，如果查询为空返回空数组。Yii::$app->db->createCommand('SELECT * FROM post')->queryAll();查询单条返回一维数组，为空则返回空数组Yii::$app->db->createCommand('SELECT * FROM post WHERE id=1')->queryOne();查询一列返回一维数组，为空则返回空数组Yii::$app->db->createCommand('SELECT title FROM post')->queryColumn();查询一个只获取一个字段的时候，有点像tp中的 value()$count = Yii::$app->db->createCommand('SELECT COUNT(*) FROM post')->queryScalar();绑定参数创建 SQL 的时候，可以通过绑定参数来防止 SQL 注入。$parame = ['username' => '两双筷子']; // 批量绑定 Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username',$parame) ->queryAll(); // 或 Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username') ->bindValues($parame)->queryAll();也可以使用一个预处理语句实现多次查询$command = Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username'); $query1 = $command->bindValue('username','张三')->queryOne(); $query2 = $command->bindValue('username','李四')->queryOne();通过引用绑定参数$username = ''; // 要先声明变量 $command = Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username') ->bindParam(':username',$username); $username = '张三'; $query1 = $command->queryOne(); $username = '李四'; $query2 = $command->queryOne();非查询语句对数据进行非查询操作需要使用 execute() 方法，返回影响行数。Yii::$app->db->createCommand('UPDATE `gm_managers` SET username = "王五" WHERE id = 20') ->execute();对于 INSERT() 、UPDATE()、DELETE() 语句，可以不用写 SQL 而直接使用相应的方法来构建SQL。插入单条数据插入单条数据，返回影响行数。// INSERT("表名",[字段数组]) Yii::$app->db->createCommand() ->insert('user', ['username' => '赵六','age' => 18]) ->execute();插入多条数据批量插入多条数据// batchInsert("表明",[列数组]，[插入数组]) Yii::$app->db->createCommand()->batchInsert('user', ['name', 'age'], [ ['Tom', 30], ['Jane', 20], ['Linda', 25], ])->execute();更新数据更新数据，返回影响行数// UPDATE("表名",[更新字段],[条件数组]|”条件字符串“) Yii::$app->db->createCommand() ->update('user', ['username' => '赵六'],['id' => 20]) ->execute();删除数据删除数据，返回影响行数// DELETE(”表名“,"条件") Yii::$app->db->createCommand() ->delete('user', 'status = 0') ->execute();注意： 上面只是构建 SQL 语句，最后在调用 execute() 时才会执行。执行事务自动事务执行出错时将自动回滚。Yii::$app->db->transaction(function($db) { $db->createCommand($sql1)->execute(); $db->createCommand($sql2)->execute(); });手动事务通过手动事务可以更精准的进行错误控制$db = Yii::$app->db; $transaction = $db->beginTransaction(); try { $db->createCommand($sql1)->execute(); $db->createCommand($sql2)->execute(); $transaction->commit(); } catch(\Exception $e) { $transaction->rollBack(); throw $e; } catch(\Throwable $e) { $transaction->rollBack(); throw $e; }指定隔离级别默认情况下直接使用数据库设置的隔离级别，Yii 也支持设定其他的事务隔离级别。$isolationLevel = \yii\db\Transaction::REPEATABLE_READ; // 自动事务设定 Yii::$app->db->transaction(function ($db) { .... }, $isolationLevel); // 手动事务 设定 $transaction = Yii::$app->db->beginTransaction($isolationLevel);注意： 隔离级别是针对整个链接设置的，后续的事务操作即使没有指定，也会使用相同的隔离级别。查询构建器使用 查询构造器可以更灵活的构建SQL，同时也提高了 SQL 的安全性。SELECT指定SELECT 子句$query = new Query(); // 以下的写法都是等同的 $query->select('user.id AS user_id, email'); $query->select(['user.id AS user_id', 'email']); $query->select(['user_id' => 'user.id', 'email']); // 追加 SELECT 子句 $query->addSelect(['email']);子查询从 Yii 2.0.1 开始，就可以使用子查询来嵌套构建SQL了$subQuery = (new Query())->select('COUNT(*)')->from('user'); // SELECT `id`, (SELECT COUNT(*) FROM `user`) AS `count` FROM `post` $query = (new Query())->select(['id', 'count' => $subQuery])->from('post');Distinct调用 distinct 过滤重复行// SELECT DISTINCT `user_id` ... $query->select('user_id')->distinct();FROM通过 form() 方法指定 SQL 中的 FROM 子句// SELECT * FROM `user` $query->from('user'); // 以下的写法都是等同的 $query->from('public.user u, public.post p'); $query->from(['public.user u', 'public.post p']); $query->from(['u' => 'public.user', 'p' => 'public.post']);FROM 中使用子查询$subQuery = (new Query())->select('id')->from('user')->where('status=1'); // SELECT * FROM (SELECT `id` FROM `user` WHERE status=1) u $query->from(['u' => $subQuery]);WHERE使用 where() 可以构建 WHERE 子句，支持以下四种格式：字符串格式：status=1哈希格式：["status" => 1,"type" => 2]操作符格式：['like' 'name' ,'test']对象格式：new LikeCondition('name','LIKE','test')字符串格式$query->where('status=1'); // 或使用参数绑定来绑定动态参数值 $query->where('status=:status', [':status' => $status]); // 原生 SQL 在日期字段上使用 MySQL YEAR() 函数 $query->where('YEAR(somedate) = 2015'); // 使用 addParams 进行参数绑定 $query->where('status=:status')->addParams([':status' => $status]);哈希格式将多个数组条件使用 AND 拼接起来，使用哈希格式，Yii 会在内部对相应的值进行参数绑定。// ...WHERE (`status` = 10) AND (`type` IS NULL) AND (`id` IN (4, 8, 15)) $query->where([ 'status' => 10, 'type' => null, 'id' => [4, 8, 15], ]);操作符格式操作符格式可以指定类程序任意风格的条件语句：["操作符", "操作数1", "操作数2" ...]每一个操作数都可以是 字符串格式、哈希格式或嵌套的操作符格式，而操作符可以是下面中的一个：操作符用法说明AND字符串：['and', 'id=1', 'id=2']哈希：['AND',['name' => "张三"],['age' => 18]]AND 查询OR哈希：['or',['name' => "张三"],['age' => 18]]OR 查询NOT['not', ['status' => 'draft', 'name' => 'example'],['delete_time' => null]]NOT 取反查询BETWEEN['between', 'id', 1, 10]范围查找 id 1~ 10NOT BETWEEN['not between', 'id', 1, 10]查找不在 1~10 范围IN['in', 'id', [1, 2, 3]]in 查询NOT IN['not in', 'id', [1, 2, 3]]not inLIKE全模糊：['like','name',"张三"]右模糊：['like','name',"张三%",false] 查询数组 ['like', 'name', ['test', 'sample']]模糊查询OR LIKE['or like','name',["张三","李四"]]多个 LIKE 查询 使用 or 拼接NOT LIKE['not like','name','张三']模糊查询 取反OR NOT LIKE['or not like','name',["张三","李四"]]多个 not like 查询 使用 or 拼接EXISTS['exists',(yii\db\Query 实例的子查询)]EXISTS 查询NOT EXISTS['not exists',(yii\db\Query 实例的子查询)]NOT EXISTS 查询>,<,>=,<=['>','age',18]比较运算符，生成 age > 18追加条件可以使用 andWhere() 或 orWhere() 在原有条件的基础上追加额外的条件，可以多次调用追加不同的条件。$query->where(['status' => 1]); $search Yii::$app->request->get('title'); if (!empty($search)) { $query->andWhere(['like', 'title', $search]); } // ... WHERE (`status` = 10) AND (`title` LIKE '%yii%')过滤条件使用 filterWhere() 方法可以过滤掉 值为空的条件（ 当一个值为 null、空数组、空字符串或者一个只包含空格的字符串时，那么它将被判定为空值）。$query->from('users'); $username = '张三'; $email = ''; $query->filterWhere([ 'name' => $username, 'email' => $email, ]); $result = $query->createCommand()->getRawSql(); // SELECT * FROM `users` WHERE `name`='张三'同样可以使用 andFilterWhere() 和 orFilterWhere() 追加过滤条件。此外可以使用 andFilterCompare() 可以根据值中的内容智能地确定运算符；$query->andFilterCompare('name', 'John Doe'); $query->andFilterCompare('rating', '>9'); $query->andFilterCompare('value', '<=100');也可以显式的指定运算符：$query->andFilterCompare('name', 'Doe', 'like');HAVINGYii 从2.0.11 版本开始，提供了 HAVING 条件的构建方法：$query->having(['age' => 20]); // 追加 AND $query->andHaving(['>', 'age', 30]); // 追加 or $query->orHaving(['>', 'age', 30]); $query->filterHaving(['name' => null, 'age' => 20]); $query->andFilterHaving();OrderBy使用 ORDER BY 子句来构建排序语句// ... ORDER BY `id` ASC, `name` DESC $query->orderBy([ 'id' => SORT_ASC, 'name' => SORT_DESC, ]); // 简单的排序可以直接使用字符串声明 $query->orderBy('id ASC, name DESC');groupBy()用来指定分组，简单的字段名称，你可以使用字符串来声明它。$query->groupBy(['id', 'status']); $query->groupBy('id, status');limit 与 offset用来指定 SQL 语句当中 的 LIMIT 和 OFFSET 子句，如果设定了一个错误的值比如 负值，则会忽略这个语句// ... LIMIT 10 OFFSET 20 $query->limit(10)->offset(20);JOIN用来指定SQL 中的 JOIN 子句。$query->join('type', 'table', 'on','params');type：连接类型，例如 INNER JOIN，LEFT JOIN、RIGHT JOINtable：连接表名称on：连接条件，只能用 字符串格式的 Where 条件params：绑定参数，on用的可以分别调用如下的快捷方法来指定 INNER JOIN, LEFT JOIN 和 RIGHT JOIN。$query->innerJoin('post', 'post.user_id = user.id'); $query->leftJoin('post', 'post.user_id = user.id'); $query->rightJoin('post', 'post.user_id = user.id');除了连接表之外，也可以使用子查询：$subQuery = (new \yii\db\Query())->from('post'); $query->leftJoin(['u' => $subQuery], 'u.id = author_id');查询方法查询构造器提供了一套用于查询不同数据的方法all()多条查询，返回二维数组one() 返回结果集的第一行记录column() 返回结果集的第一列scalar() 返回结果集的第一行第一列标量值exists() 返回一个表示该查询是否包含结果集的值。count() 返回 COUNT 查询的结果其他方法：sum($q)、average($q)、max($q)、min(q) 等。$q 是必选字段，可以是一个字段也可以是一个表达式。indexBy使用 all() 查询数据，会返回一个整数的索引数组，有时候我们想要指定某个字段做这个数组的键。可以在 all() 方法之前调用 indexBy() 方法，并传递一个字段或表达式// 使用字段做key $query = (new \yii\db\Query()) ->from('user') ->limit(10) ->indexBy('id') ->all(); // 使用闭包函数的返回值做 key $query = (new \yii\db\Query()) ->from('user') ->indexBy(function ($row) { return $row['id'] . $row['username']; })->all();批量查询处理当处理大数据的时候，使用all()一次性将所有数据加载在内存上就不太合适了，Yii 提供了批量查询功能，MySQL 先保存查询结果，然后 Yii 使用游标分批获取数据。use yii\db\Query; $query = (new Query()) ->from('user') ->orderBy('id'); foreach ($query->batch() as $users) { // $users 是一个包含100条或小于100条用户表数据的数组 } // or to iterate the row one by one foreach ($query->each() as $user) { // 数据从服务端中以 100 个为一组批量获取， // 但是 $user 代表 user 表里的一行数据 }MySQL中批量查询的局限性打印 SQL可以使用 createCommand() 函数返回的对象打印 SQL$query = new Query(); $command = $query->select(['id', 'email'])->from('user')->createCommand(); // 打印 SQL 语句 echo $command->sql; // 或者使用 getRawSql() 函数 echo $command->getRawSql(); // 打印被绑定的参数 print_r($command->params);模型操作Yii2 提供了一个面向对象模型类，用以访问和操作数据库中的数据。模型基类Yii2 中基础模型类 yii\base\Model，官方基于这个类提供了很多高级模型，例如 yii\db\ActiveRecord 。而基类自身提供如下特性：属性模型通过属性代表业务数据，就像Thinkphp 及 Laravel 那样，结果集既可以对象访问，也可以数组访问。$FilmModel = \app\models\Film::findOne(1); echo $FilmModel->title; echo $FilmModel['title'];场景一个模型可能会在多个场景下使用，在不同的场景下我们希望校验不同的值，比如我们注册用户的时候邮箱是必填的，但登录的时候却不是。这种情况下可以配合验证规则实现不同场景的数据校验。设置场景// 场景作为属性来设置 $model = new User; $model->scenario = 'login'; // 场景通过构造初始化配置来设置 $model = new User(['scenario' => 'login']);验证规则 核心验证器（Core Validators）当接收到了用户提交的数据，可以通过验证规则来进行校验，而不需要手动判断。使用验证$model = new \app\models\ContactForm; // 用户输入数据赋值到模型属性 $model->attributes = \Yii::$app->request->post('ContactForm'); if ($model->validate()) { // 所有输入数据都有效 all inputs are valid } else { // 验证失败：$errors 是一个包含错误信息的数组 $errors = $model->errors; }定义验证规则 如果想让校验在所有场景下都生效，去掉 on 即可。public function rules() { return [ // 在"register" 场景下 username, email 和 password 必须有值 [['username', 'email', 'password'], 'required', 'on' => 'register'], // 在 "login" 场景下 username 和 password 必须有值 [['username', 'password'], 'required', 'on' => 'login'], ]; }块赋值块赋值只用一行代码将用户所有输入填充到一个模型，非常方便， 它直接将输入数据对应填充到 yii\base\Model::attributes() 属性。$model = new \app\models\ContactForm; $model->attributes = \Yii::$app->request->post('ContactForm');安全属性因为块赋值会将用户提交的所有数据都放模型中，有时候我们只希望用户修改部分数据，这时可以通过设置 安全属性来实现（个人理解就是块赋值的黑白名单） 在登陆场景下允许提交 用户名和密码，注册场景下，允许提交 用户名、密码和邮箱。通过别名为 safe 的验证器来表示这些字段是安全的public function scenarios() { return [ 'login' => ['username', 'password'], 'register' => ['username', 'email', 'password'], ]; }不安全属性如果要表示不安全属性，则在属性前面加一个 ! 即可。public function rules() { return [ [['username', 'password', '!secret'], 'required', 'on' => 'login'] ]; }不安全的属性在块赋值的时候会被过滤掉，如果需要必须手动指定。$model->secret = $secret;快捷填充使用 load() 填充可以简化代码：例如：if (isset($_POST['FormName'])) { $model->attributes = $_POST['FormName']; if ($model->save()) { // handle success } }简化写法：if ($model->load($_POST) && $model->save()) { // handle success }声明模型类声明一个模型类 需要继承 yii\db\ActiveRecord 类。::: alert-warning需要注意的是 Yii2 提供的 base 只是一个基类，并没有模型的操作方法，所以我们需要继承的是 yii\db\ActiveRecord 类，而不是 yii\base\Model 类:::<?php namespace app\models; use yii\db\ActiveRecord; class Film extends ActiveRecord { }设置表名称设置模型的表名称有两种方式，第一种就是使用模型类名，第二种方式是手动指定一个表名。使用模型类名类名使用大驼峰，例如：类名表名UseruserAdminUseradmin_user手动指定// 手动指定表名称 public static function tableName() { return 'film'; } // 手动指定表名并使用前缀 public static function tableName() { return '{{%film}}'; }指定数据库如果想在这个类中单独使用其他数据库配置，可以重写 getDb()方法：public static function getDb() { // 使用 "db2" 组件 return \Yii::$app->db2; }查询数据Find使用 Active Query 查询数据，这种操作和直接使用查询生成器差不多，唯一的区别在于 使用 find()方法获取查询生成器对象，而不是 new 一个新对象。 // 返回 ID 为 123 的客户： // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::find() ->where(['id' => 123]) ->one(); // 取回所有活跃客户并以他们的 ID 排序： // SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id` $customers = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->orderBy('id') ->all(); // 取回活跃客户的数量： // SELECT COUNT(*) FROM `customer` WHERE `status` = 1 $count = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->count(); // 以客户 ID 索引结果集： // SELECT * FROM `customer` $customers = Customer::find() ->indexBy('id') ->all();findOne 与 findAll根据主键获取 id 是比较常用的功能，所以 Yii 提供了两个快捷方法：findOne() 查询单条数据findAll() 查询多条数据这两个方法接受如下传参：整数值：这个值会当作主键去查询，Yii 通过读取数据库信息来识别主键列。整数数组：主键的 IN 查询关联数组：需要传递一个哈希格式的条件数组。// 返回 id 为 123 的客户 // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // 返回 id 是 100, 101, 123, 124 的客户 // SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124) $customers = Customer::findAll([100, 101, 123, 124]); // 返回 id 是 123 的活跃客户 // SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1 $customer = Customer::findOne([ 'id' => 123, 'status' => Customer::STATUS_ACTIVE, ]); // 返回所有不活跃的客户 // SELECT * FROM `customer` WHERE `status` = 0 $customers = Customer::findAll([ 'status' => Customer::STATUS_INACTIVE, ]);::: alert-danger如果你需要将用户输入传递给这些方法，请确保输入值是标量或者是 数组条件，确保数组结构不能被外部所改变：// yii\web\Controller 确保了 $id 是标量 public function actionView($id) { $model = Post::findOne($id); // ... } // 明确了指定要搜索的列，在此处传递标量或数组将始终只是查找出单个记录而已 $model = Post::findOne(['id' => Yii::$app->request->get('id')]); // 不要使用下面的代码！可以注入一个数组条件来匹配任意列的值！ $model = Post::findOne(Yii::$app->request->get('id'));:::原生SQL除了查询生成器之外，模型还支持使用原生SQL查询，使用 findBySql() 方法，需要注意的是 findBySql()后面追加的其他查询方法都会被忽略$sql = 'SELECT * FROM customer WHERE status=:status'; $customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();访问数据查询的数据被返回在模型实例中，可以通过模型属性来访问结果集。// "id" 和 "email" 是 "customer" 表中的列名 $customer = Customer::findOne(123); $id = $customer->id; $email = $customer->email;修改器与获取器官方的叫法是“数据转换”，但在其他框架中更通用的名字叫做“获取器”。class Customer extends ActiveRecord { // 设置获取器 public function getBirthdayText() { return date('Y/m/d', $this->birthday); } // 设置修改器 public function setBirthdayText($value) { $this->birthday = strtotime($value); } }asArray虽然 Yii 中的模型操作十分灵活，但如果查询大量数据时，对象会造成大量的内存占用。我们可以在查询前调用 asArray() 方法来转换为数组，以节省内存。$customers = Customer::find()->asArray()->all();分批获取数据使用分批获取数据，可以最小化内存使用。// 每次获取 10 条客户数据 foreach (Customer::find()->batch(10) as $customers) { // $customers 是个最多拥有 10 条数据的数组 } // 每次获取 10 条客户数据，然后一条一条迭代它们 foreach (Customer::find()->each(10) as $customer) { // $customer 是个 `Customer` 对象 } // 贪婪加载模式的批处理查询 foreach (Customer::find()->with('orders')->each() as $customer) { // $customer 是个 `Customer` 对象，并附带关联的 `'orders'` }保存数据save()Active Record 类提供了 save 方法，可以轻松的将数据存入数据库。// 插入新记录 $customer = new Customer(); $customer->name = 'James'; $customer->email = 'james@example.com'; $customer->save(); // 更新已存在的记录 $customer = Customer::findOne(123); $customer->email = 'james@newexample.com'; $customer->save();插入还是写入数据，这取决于模型实例是通过 new 获取的还是通过查询获取的。:::alert-info在保存时如果确认数据不需要校验，可以通过 save(false) 的方式来跳过验证过程。:::insert()插入数据，返回布尔值 表示成功还是失败$customer = new Customer; $customer->name = $name; $customer->email = $email; $customer->insert();update()更新数据，返回影响行数:::alert-w需要注意的是，更新可能不会影响表中的任何行。 在该情况下，此方法有可能返回 0。 因此，需要使用 !== false 的方式判断 update() 是否成功：:::$customer = Customer::findOne($id); $customer->name = $name; $customer->email = $email; $customer->update();更新计数在数据库中经常会遇到需要自增或自减的场景，我们将这些列称之为计数列，可以使用 updateCounters() 更新一个或多个计数列。如果是递减 使用负值即可。$post = Post::findOne(100); // UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100 $post->updateCounters(['view_count' => 1]);:::alert-warning如果使用 save() 更新一个计数列，可能会因为并发请求导致出现错误的结果。:::脏属性调用 save() 保存数据的时候，只有“脏属性”即被修改的属性才会被保存。需要注意的是，无论如何 Active Record 都会执行数据验证 不管有没有脏属性。Active Record 类会自动维护一个脏属性列表，并且保存所有属性的旧值，并于新值比较（使用 === 比较）。可以通过 getDirtyAttributes() 来获取当前的脏属性，也可以通过 markAttributeDirty('field_name') 将属性显式的标注为脏属性。如果需要获取原先的旧值，可以通过 getOldAttributes() 来一次性获取所有旧值，或使用 getOldAttribute('field_name') 获取单个旧值。批量更新上面所说的保存数据是针对单条的，而批量更新多条数据时我们可以使用 updateAll() 方法。updateAll($attributes, $condition = '', $params = [])// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%` Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);批量更新计数计数列也可以通过批量的方式来更新：updateAllCounters($counters, $condition = '', $params = [])// UPDATE `customer` SET `age` = `age` + 1 Customer::updateAllCounters(['age' => 1]);删除数据删除单条返回 删除的行数，如果由于某种原因删除失败，则为 false。 注意，即使删除执行成功，删除的行数也可能为 0。$customer = Customer::findOne(123); $customer->delete();删除多条批量删除数据，传参是删除条件，返回删除行数。Customer::deleteAll(['status' => 1]);:::alert-danger需要注意的是，Yii2 模型方法中没有软删除，如果批量删除条件出错，可能会导致数据表被清空。:::生命周期通过模型的生命周期，可以在不同的节点调用想要执行的代码，具体参考：Active Record 的生命周期下面以 Yii2 的删除方法为例，实现软删除的扩展：class Film extends ActiveRecord { public function beforeDelete() { if (!parent::beforeDelete()) { return false; } // 如果存在软删除字段，就修改软删除状态,并返回false if(isset($this->is_delete)) { $this->is_delete = 1; $this->save(); return false; } // 如果不存在 就走正常删除 return true; } }:::alert-info调用以下方法，不会触发任何生命周期，因为这些方法是直接操作数据库而不是 ActiveRecord 模型：updateAll()deleteAll()updateCounters()updateAllCounters():::事务操作Yii 包含两种事务方法。显式事务$customer = Customer::findOne(123); Customer::getDb()->transaction(function($db) use ($customer) { $customer->id = 200; $customer->save(); // ...其他 DB 操作... }); // 或 $transaction = Customer::getDb()->beginTransaction(); try { $customer->id = 200; $customer->save(); // ...other DB operations... $transaction->commit(); } catch(\Exception $e) { $transaction->rollBack(); throw $e; } catch(\Throwable $e) { $transaction->rollBack(); throw $e; }::: alert-warning上面的 \Exception 用于兼容 PHP5，如果开发环境是 PHP7+，则可以跳过这部分。:::自动事务在模型中的 transactions() 方法里声明需要事务支持的DB操作：class Customer extends ActiveRecord { public function transactions() { return [ 'admin' => self::OP_INSERT, 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, // 上面等价于： // 'api' => self::OP_ALL, ]; } }key 是场景，描述了在不同场景下的事务要求：OP_INSERT 执行插入操作时触发事务OP_UPDATE 执行更新操作时触发事务OP_DELETE 执行删除操作时触发事务多个操作使用 | 拼接，如果需要全部触发，OP_ALL 执行 插入、更新、删除操作时都会触发。自动事务的执行逻辑是：相应的事务在调用 beforeSave() 方法时开启， 在调用 afterSave() 方法时被提交。模型关联模型管理以 MySQL 官方示例库 sakila 为例，展示模型关联用法。一对一关联语法：return $this->hasOne($class, $link);$class 关联类$link 主外键按约束，数组格式。key是关联表字段 ，value 是主表字段return 返回关联查询对象// 电影演员表模型 class FilmActor extends ActiveRecord { // 一对一关联 获取演员信息 public function getInfo() { return $this->hasOne(Actor::class, ['actor_id' => 'actor_id']); } }一对多关联语法：return $this->hasMany($class, $link);$class 关联类$link 主外键按约束，数组格式。key是关联表字段 ，value 是主表字段return 返回关联查询对象// 电影表模型 class Film extends ActiveRecord { // 一对多关联，获取电影演员 public function getActor() { return $this->hasMany(FilmActor::class, ['film_id' => 'film_id']); } }访问关联数据定义好关联关系后，我们就可以通过关联属性来访问数据了。而 Yii2 提供了两种方法用以访问这些数据：通过方法直接通过关联方法名获取的是一个 ActiveRecord 查询对象。$FilmModel = Film::findOne(1); var_dump($FilmModel->getActor()); 通过属性如果通过属性获取，则定义关联时方法名必须是 'getXxx()' 的命名形式，否则无法获取到属性。使用时 去掉 前缀get，并且首字母小写，例：getActor() -> actor、getAcTor-> acTor一对多关联时，通过属性获取到的是一个二维数组，数组的元素是 yii\db\BaseActiveRecord 对象。$FilmModel = Film::findOne(1); foreach ($FilmModel->actor as $key => $value) { var_dump($value->info->first_name); }动态关联查询由于关联方法返回的是 ActiveRecord 查询对象，所以你可以在这个对象上继续进行查询操作。需要注意的是，每次动态的关联查询时，都会执行SQL语句。$FilmModel = Film::findOne(1); // 获取结果中的第一列 var_dump($FilmModel->getActor()->column()); // 获取关联表的 数组数据 var_dump($FilmModel->getActor()->asArray()->all()); // 查询演员id 小于50 的数据 $actorModel = $FilmModel->getActor()->where(['<','actor_id','50'])->all(); var_dump($actorModel);也可以通过传参的方式简化动态查询我们以查询演员id 小于50的为例。class Film extends ActiveRecord { // 获取id小于某个值的演员， public function getMinActor($number = 50) { return $this->hasMany(FilmActor::class, ['film_id' => 'film_id']) ->where(['<','actor_id',$number]); } } $FilmModel->getMinActor(40)->asArray()->all()::: alert-w如果需要通过属性获取，则必须给关联方法设置一个默认值。:::多对多关联在日常的查询中，当两张表的关系是 多对多，通常会使用一个中间表来关联。例如，一个电影有多个演员，而一个演员也会出演多部电影。那么演员表 actor 和电影表 film 之间就会有一个电影演员关系表 film_actor。Yii2 有两种方法声明 多对多关联，一种是使用 via() 引用现有的一对多关联方法，另一种是使用 viaTable() 来直接声明中间表。viaTable语法：viaTable($tableName, $link, callable $callable = null);$tableName 中间表名$link 主表与中间表关联条件$callable 一个 PHP 的回调，用于定制与连接表相关的关联。class Film extends ActiveRecord { // 多对多关联 public function getActorInfo() { // hasMany 第二个参数是 演员表和电影演员关系表的关联关系 return $this->hasMany(Actor::class, ['actor_id' => 'actor_id']) ->viaTable('film_actor', ['film_id' => 'film_id']); } via使用已有的一对多关联方法来指定连接表class Film extends ActiveRecord { // 多对多关联 public function getActorInfo() { return $this->hasMany(Actor::class, ['actor_id' => 'actor_id']) ->via('actor'); } // 一对多关联，获取电影演员 public function getActor() { return $this->hasMany(FilmActor::class, ['film_id' => 'film_id']); }多表连续关联通过使用 via() 方法们可以通过多个表来定义关联声明。比如 一个演员会出演多部电影 ，每部电影会有不同的分类，我们可以通过如下方法获取：class Actor extends ActiveRecord { // 通过电影类别中间表关联类别 public function getActorCate() { return $this->hasMany(Category::class,['category_id' => 'category_id']) ->via('filmCate'); } // 关联电影的类别中间表 public function getFilmCate() { return $this->hasMany(FilmCategory::class, ['film_id' => 'film_id']) ->via('film'); } // 演员关联电影 public function getFilm() { return $this->hasMany(FilmActor::class,['actor_id' => 'actor_id']); } }预加载前面有提到 访问关联数据的时候才会执行SQL，这样就会存在一个性能问题，比如下面这段代码，执行了101次SQL查询。// SELECT * FROM `customer` LIMIT 100 $customers = Customer::find()->limit(100)->all(); foreach ($customers as $customer) { // 每次循环都会被执行一次 // SELECT * FROM `order` WHERE `customer_id` = ... $orders = $customer->orders; }我们可以使用 with()方法进行预加载，这时需要执行的 SQL 语句只有两条。// SELECT * FROM `customer` LIMIT 100; // SELECT * FROM `orders` WHERE `customer_id` IN (...) $customers = Customer::find() ->with('orders') ->limit(100) ->all(); foreach ($customers as $customer) { // 没有任何的 SQL 执行 $orders = $customer->orders; }with各种用法// 即时加载 "orders" and "country" $customers = Customer::find()->with('orders', 'country')->all(); // 等同于使用数组语法 如下 $customers = Customer::find()->with(['orders', 'country'])->all(); // 即时加载“订单”和嵌套关系“orders.items”，这个嵌套层级是没有限制的，可以“a.b.c.d” $customers = Customer::find()->with('orders.items')->all();使用预加载的时候可以通过闭包函数实现条件过滤：// 查找所有客户，并带上他们国家和活跃订单 // SELECT * FROM `customer` // SELECT * FROM `country` WHERE `id` IN (...) // SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1 $customers = Customer::find()->with([ 'country', 'orders' => function ($query) { $query->andWhere(['status' => Order::STATUS_ACTIVE]); }, ])->all();::: alert-warning如果你在即时加载的关联中调用 select() 方法，你要确保 在关联声明中引用的列必须被 select。:::其他操作可以参考 Yii2 的模型文档：活动记录（Active Record）数据库迁移通过数据库迁移，可以记录数据库结构的变化，使数据库可以和源代码一样受版本控制。迁移命令提交迁移命令： yii migrate 该操作会列出所有未提交的迁移，如果确定要提交这些迁移，将会按照类名中的时间戳，逐个运行里面的 up() 方法或 safup() 方法。其中任意一个迁移执行失败（比如执行报错），那么这条命令将会退出并停止剩下未执行的迁移。每次迁移成功，都会在数据库的 migration 表中添加一条迁移成功的记录，用于判断那些迁移是已提交的，那些是未提交的。有时候篇，我们只想提交前几个迁移，而不是提交所有可用迁移。下面的命令则表示只提交前三个可用迁移：php yii migrate 3提交特定迁移也可以只提交特定的迁移，使用 migrate/to 命令。yii migrate/to 150101_185401 # 使用时间戳来指定迁移 yii migrate/to "2015-01-01 18:54:01" # 使用一个可以被 strtotime() 解析的字符串 yii migrate/to m150101_185401_create_news_table # 使用全名 yii migrate/to 1392853618 # 使用 UNIX 时间戳如果指定的迁移前面还有未提交的迁移，则未提交的迁移将会被提交。如果指定的迁移在之前已经被提交过，则在其之后的迁移将会被还原。创建迁移命令：yii migrate/create <name>可以使用此命令创建一个新的迁移文件，创建的迁移文件类名将按照 m<YYMMDD_HHMMSS>_<Name> 的格式自动生成，其中：<YYMMDD_HHMMSS> 指执行创建迁移命令的 UTC 时间。<Name> 指创建迁移命令所带的 Name 参数值创建命令执行后会生成一个类文件，里面包含 up() 和 down() 方法。当提交迁移文件时 up() 将会被执行，当还原迁移时 down() 将会被提交。如果迁移文件无法被还原时，例如删除了一条记录，则应该在 down() 方法中返回 false 来表示这个 migration 是无法恢复的。还原迁移命令：yii migrate/down 可以通过此命令还原之前提交的一个或多个迁移：yii migrate/down # 还原最近一次提交的迁移 yii migrate/down 3 # 还原最近三次提交的迁移重做迁移命令：yii migrate/redo重做的意思是，先还原指定的迁移，然后再次提交。yii migrate/redo # 重做最近一次提交的迁移 yii migrate/redo 3 # 重做最近三次提交的迁移刷新迁移命令：yii migrate/fresh删除数据表中所有的表和外键，从头开始重新提交所有迁移。列出迁移可以通过命令列出已提交了那些或未提交那些迁移：yii migrate/history # 显示最近10次提交的迁移 yii migrate/history 5 # 显示最近5次提交的迁移 yii migrate/history all # 显示所有已经提交过的迁移 yii migrate/new # 显示前10个还未提交的迁移 yii migrate/new 5 # 显示前5个还未提交的迁移 yii migrate/new all # 显示所有还未提交的迁移修改迁移历史有时候我们只需要标记一下你的数据库升级到一个特定的迁移，而不是实际提交或还原迁移。这个经常发生在已经手动修改了数据库的结构，而又不想相应的迁移被重复提交，那么你可以使用如下命令来达到目的：yii migrate/mark 150101_185401 # 使用时间戳来指定迁移 yii migrate/mark "2015-01-01 18:54:01" # 使用一个可以被 strtotime() 解析的字符串 yii migrate/mark m150101_185401_create_news_table # 使用全名 yii migrate/mark 1392853618 # 使用 UNIX 时间戳自定义迁移可以通过很多方法来自定义迁移命令。命令行选项迁移命令附带了几个命令行选项，可以用自定义它的行为：interactive 布尔值（默认为true）指定是否通过交互模式来提示用户将执行那些迁移，如果希望在后台执行该命令，应该设置为 false。migrationPath 字符串|数组（默认值为 @app/migrations） 指定存放迁移类文件的目录。该选项可以是路径，也可以是路径别名。需要注意的是指定的目录必须存在，否则将会触发一个错误。从 2.0.12 版本开始，可以使用数组来指定多个来源读取迁移文件。migrationTable 字符串（默认值为 migration）指定用于存储迁移历史信息的数据库表名称，如果这张表不存在，那么迁移命令将自动的创建这张表。db 字符串（默认值为 db） 用于指定被该命令迁移的数据库templateFile：字符串 (默认值为 @yii/views/migration.php)， 指定生产迁移框架代码类文件的模版文件路径。fields 由用来创建迁移代码的多个字段定义字符串所组成的数组。默认是 []。 字段定义的格式是 COLUMN_NAME:COLUMN_TYPE:COLUMN_DECORATOR。例如，--fields=name:string(12):notNull 会创建 一个长度为 12 的，非空的，字符串类型的字段。
假设这么一个场景: 有一个通过 Yii ActiveForm 构建的表单，里面所有的字段都是虚拟的，比如说 这个表单的字段来自于一个 JSON 或者说数据库的结果集，需要实现同一个模型，根据不同的传参，构建不同的表单项。问题： Yii 的表单是通过模型来创建的，也就是说我有这个表单项，模型中必须有对应的字段（成员属性），否则就会报错。且这个成员属性在模型实例化之后是不允许添加的，那么我们如何去动态的添加这些表单项的字段？解决思路： 既然模型字段都是不存在的，我们干脆用一个虚拟的模型，不是说这个模型文件不存在，而是指模型文件不对应任何实际的数据表。那么我们只需要在实例化模型的时候，传入动态的字段，然后在对应的 rules() 、attributes() 、attributeLabels() 方法里面返回我们的动态字段，就可以实现需要的效果。模型部分创建一个虚拟的模型，重写构造方法，传入字段，赋值给成员属性 $field<?php namespace app\models; use Attribute; use yii\db\ActiveRecord; class Actor extends ActiveRecord { public $field; public function __construct($field) { $this->field = $field; parent::__construct([]); } public function rules() { return [ [$this->field, 'required'] ]; } public function attributes() { return $this->field; } }控制器部分<?php namespace app\controllers; use Yii; use Yii\base\Controller; use app\models\Actor; class TestController extends Controller { public function actionIndex() { /** * 动态创建的字段，在实例化模型的时候传递进去 * 这里只是为了测试，实际应用的时候可以是从其他途径来的数组: * 比如 JSON 动态转换、数据库查询出的结果集等等 */ $field = ['username','json','token']; $model = new Actor($field); if (Yii::$app->request->getIsGet()) { return $this->render('index',['model' => $model]); } // 装载数据 $model->load(Yii::$app->request->post()); // 获取模型提交的数据 var_dump($model->getDirtyAttributes()); } }视图部分<?php use yii\helpers\Html; use yii\widgets\ActiveForm; $form = ActiveForm::begin([ 'id' => 'login-form', 'options' => ['class' => 'form-horizontal'], ]) ?> <? foreach($model->field as $field) { ?> <?= $form->field($model, $field) ?> <? }?> <?= Html::submitButton('submit', ['class' => 'btn btn-primary']) ?> <?php ActiveForm::end() ?>效果展示构建的表单提交后打印的数据
我们都知道 Web Server (Nginx/Apache) 本身只能处理静态文件，对于 PHP 这种动态语言是没办法处理的，所以我们需要借助其他的方式来使 PHP 与 Web Server 进行交互。CGICGI （Common Gateway Interface）中文全称是“通用网关接口”，它的出现就是为了解决上面提到的 Nginx 与 动态语言交互的问题。由于CGI只是一个协议，可以使用任何一种语言编写，只要这种语言支持标准输入、标准输出、和环境变量。而PHP实现这个协议的程序就叫 “PHP-CGI”Fast-CGIFast-CGI 是对 CGI 协议的一种改进，上面提到的 CGI 协议动态语言的交互了，但每一次请求都会 fork 一个 CGI 的进程，请求结束再 kill 掉这个进程。如果请求多的话，就会反复的 fork、kill PHP-CGI 进程很多次，造成很大的资源消耗。于是出现了 Fast-CGI 这个协议，Fast-CGI 在每次请求处理完成之后，并不会 kill 掉这个进程，而会保留这个进程等待处理下次请求。不需要每次都 fork 一个新的进程，大大的提高了效率。PHP-FPMPHP-FPM 是对 Fast-CGI 的具体实现，启动后会有两种进程，一个是 master 主进程，和多个 CGI 子进程，主进程负责管理这个子进程，而子进程负责PHP的处理。如果子进程挂掉了，则 master 会启动一个新的子进程，除此之外，PHP-FPM 还有其他的管理功能、例如：修改 php.ini 后平滑重启，具体可以参考官方文档：https://www.php.net/manual/zh/install.fpm.phpApache ModPHP 与 Web Server 传递数据的另一种方法就是通过 PHP Module 加载方式。就是将 PHP 作为 Apache 的子模块来运行，当通过 Web 请求 PHP 文件时，Apache 就会调用 php5_module 来解析 PHP 代码。 Apache -> httpd -> php_module -> sapi -> php这种实现会有上面提到的 CGI 一样的问题，每一个请求都会产生一个新的进程来处理 PHP 的请求，并发一旦多了，服务器就会承载不住。（由于Fast-CGI可以长时间保持运行，所以可以做PHP数据库链接持久化，不需要每次处理 PHP 请求都链接一次数据库。官方文档：https://www.php.net/manual/zh/features.persistent-connections.php）PHP 的 SAPISAPI 是 PHP 内核提供的一个与外部通讯的一个接口，类似与 Socket。使PHP可以与其他应用进行数据交互 (Apache、Nginx)。PHP 默认提供了很多中 SAPI，常见的有 php5_module、CGI、FastCGI、IIS 的 ISAPI、Shell 的 CLI。参考文档：https://www.awaimai.com/371.htmlhttps://www.cnblogs.com/schips/p/12995252.html
端午闲着也是闲着，写了一个MD 数据字典的导出工具，后面有机会会完善PDO支持、索引信息导出、HTML文件导出。使用要求PHP 5.3+MySQL/MariaDBMySQLi 扩展导出内容表相关表类型表存储引擎表字段表备注字段相关字段名字段类型是否为空默认值其他（列自增、时间的自动填充等）字段备注导出的Markdown导出方式下载地址去码云下载
抽象类是什么抽象类是一种特殊的类，使用abstract修饰抽象类不能被实例化，只能被继承抽象方法不能设置使用private 修饰，因为抽象类不能被实例化，私有成员没有意义。abstract 还可以修饰方法，称之为抽象方法：抽象方法所在的类必须是抽象类，抽象方法不能有方法体如果子类继承了抽象类，要么子类自身是抽象类，要么实现所有抽象方法抽象类的目的是规范子类，通常会在比较大型或者规范的项目中才会使用抽象类中不允许有成员属性<?php // 抽象类 abstract abstract class Human{ // 普通方法 public function show(){} // 抽象方法，抽象方法存在的类必须是抽象类 // 抽象方法不能有方法体 public abstract function eat(); }; // 抽象类不能被实例化 // new Human(); // 继承抽象类的要么为抽象类，要么实现抽象类的所有抽象方法。 class Man extends Human{ // 实现抽象方法 eat public function eat(){} }; // 可以正常实例化 new Man(); // 抽象子类继承抽象类 abstract class Woman extends Human{ public abstract function makeup(); } abstract class Wo extends demo{ // 实现抽象方法 eat public function eat(){} // 实现抽象方法 makeup public function makeup(){} } // 只有实现了抽象类和继承的抽象类的所有所有抽象方法，才可以实例化 new Wo();接口是什么PHP是单继承的，为了解决这个问题，PHP引入了接口。接口是一种特殊的抽象类，而抽象类又是一种特殊的类。接口不是类，但与类有类似的结构，目的是为了约束类。接口不能实例化，但类可以实现接口（注意是实现不是继承）<?php interface 接口名{} class 类名 implements 接口名{} 接口是用来规范项目体系，提供一些必须的行为规范接口成员： 在接口内部定义的成员接口成员只允许有两类接口常量：constpublic的接口方法（普通方法和静态方法）接口的方法为抽象方法，没有方法体（不需要abstract 关键字，因为接口方法都是抽象方法）实现接口的类：可以访问接口的常量：接口常量不能被重写需要实现所有的接口方法（除非本身是抽象类）接口方法实现不允许增加控制权限（必须为 public）接口成员：接口中只能定义共有抽象方法和接口常量<?php interface Humen{ // 接口常量 const NAME = '人'; // 接口抽象方法 public function eat(); public static function show(); }接口成员方法必须被实现的子类实现或类为抽象类，接口常量可以直接在实现类中使用// 实现接口：实体类 class Man implements Humen{ // 必须实现接口里的所有的抽象方法 public function eat(){ // 可以访问常量 echo self::NAME; } public static function show(){} } // 实现接口：抽象类 abstract class Women implements Humen{};类实现接口成员时，不允许重写接口中的常量，不允许增加接口方法的控制权限class Women implements Humen{ // 不允许重写接口常量 const NAME = '小孩'; // 不允许使用其他访问修饰符，必须使用public private function eat(); };接口继承： 接口可以被接口继承接口继承接口的目的实现接口成员的扩展，丰富接口内容，从而实现更好的对实现类的规范为了形成完整的接口体系，让不同级别的类实现不同级别的接口接口可以一次性继承多个接口 interface A{} interface B{} // 接口继承 interface C extends A{} // 接口多继承 interface D extends A,B{}总结：接口可以继承接口，而且是多继承接口是在大型项目中，为了保证底层的实现而设计的规范，通常是抽象类实现接口，增加必要成员属性，然后让实际业务类去继承抽象类类在继承其他类的时候可以同时去实现相应的接口，不需要父类和接口有任何关系接口和抽象的区别抽象类中可以有非抽象方法，而接口中只能有抽象方法一个类可以实现多个接口，而一个类只能继承一个抽象方法接口的使用方式通过 implements实现，而抽象类使用extends 关键字继承。抽象类中可以声明成员变量，而接口中不能声明成员变量，但可以声明常量。接口没有构造函数，而抽象类可以有构造函数接口中的抽象方法只能使用public来修饰，而构造方法可以使用public和protected来修饰抽象类的抽象方法要使用abstract 来修饰，而接口不需要
前言我现在用的博客主题，目前并不支持针对单文章的版权设置功能，所以我抽空自己做了一个小插件来实现这个功能。写这个插件的原因有两个，一个是前面说的，我需要一个这样的功能。当然 Typecho 有很多现有的插件可以实现这个功能，所以我自己做这个插件还有第二个目的。熟悉一下 Typecho 的插件开发，后面可以自己扩展出更多玩法。安装插件去码云下载插件将压缩包中的文件夹Copyright 上传至typecho 插件目录 typecho/usr/plugins在 管理端 的插件管理页面启用插件 "Copyright"在 post.php中添加调用代码：// typecho/usr/themes/主题目录/post.php <?php Copyright_Plugin::Output($this) ?>将调用代码放置在合适的位置，例如： <?php Copyright_Plugin::Output($this) ?> <?php $this->content(); ?> 插件设置插件支持自定义版权选项，并且预置了四个，你可以随意的修改它们，只要符合要求的格式即可。插件启用后会自动在管理端的文章编辑页面添加一个下拉框：如果设置的版权JSON错误，则会显示相应的提示：
XSS跨站脚本攻击跨站脚本攻击，英文全称Cross site Script，本来缩写是CSS，但为了和层叠样式表有所区别，所以在安全领域叫做“XSS”。XSS攻击，通常指黑客通过HTML注入篡改了网页，插入了恶意的脚本，从而在用户浏览网页时，控制用户浏览器的一种攻击。一开始这种攻击的演示案例是跨域的，所以叫做“跨站脚本”攻击，但由于JS强大功能以及前端应用的复杂化，是否跨域已经不再重要。但由于历史原因，XSS这个名字却被保留了下来。XSS可分为三类：反射型XSS：简单地把用户输入的数据反射给浏览器，比如需要用户点击一个恶意链接。存储型XSS：会把用户输入的数据存储在服务器，这种XSS具有很强的稳定性DOM based XSS：通过修改页面的DOM节点形成的XSS，称之为DOM Based XSS解决方法：主要是过滤用户输入的内容，让攻击失效。在用户输入的时候进行检查，在输出的时候对内容进行HTML编码转义，PHP可以使用htmlspecialchars()函数来完成这一操作有时候需要用户编辑富文本，这时候可以过滤一些危险的标签，比如 、 、 、 等。在 标签的选择上应该使用白名单，避免使用黑名单。比如，只允许、 、 等比较 "安全" 的标签存在。CSRF跨站点请求伪造跨站点请求伪造是一种用户在当前已登陆的网站上执行非本意的操作的攻击方法，XSS利用的是用户对网站的信任，CSRF利用的是网站对用户浏览器的信任。例如：当用户登录了a站点，之后用户访问b站点，b站点可以加载一个图片资源，url设置为：http://www.a.com?delete=id，当加载图片资源的时候浏览器会对这个url发起请求，目标网站以为是用户执行了删除操作，而去删除一些东西，比如订单或者文章什么的。解决方法：使用验证码：CSRF请求往往是在用户不知情的情况下构造的网络请求。而验证码，之前强制用户必须与应用进行交互，才能完成最终的请求。通常情况下验证码可以很好的遏制CSRF攻击。检查Referer字段：常见的互联网应用，页面和页面之间都有一定的关系，使得每个正常请求的Referer具有一定的规律。当请求过来时，可以通过判断发起请求的页面是否是合法的地址，当请求合法后再处理。使用Token验证：CSRF可以成功的本质是请求的所有参数都可以被攻击者猜到，如果使用了token，攻击者无法猜到的参数，则无法构建CSRF攻击。SQL注入攻击注入攻击的本质是：把用户输入的数据当作代码去执行，这里有两个关键条件用户能够控制输入原本程序要执行的代码，拼接了用户输入的数据解决方法：使用预编译语句使用存储过程检查数据类型数据库使用最小原则：避免直接使用root用户操作
之前写钉钉群通知机器人的时候，封装了的一个机器人类库。支持 老版机器人发送（只用token）、新版机器人发送（需要token + 验签）token 就是生成机器人时地址后面的 access_token 的值.封装类库 <?php // 钉钉自定义机器人 webhook 封装 class Dingtalk { private $token = null; // token 类型 private $isAt = false; // 是否@所有人 // 实例化的时候 传递发送群的token public function __construct($token = '') { // 如果传递的是数组 就走验签 if (is_array($token)) { $this->tokenSign($token); } else { $this->token = $token; } } // 走加签过程 private function tokenSign($Array) { // 赋值基础 $this->token = $Array['token']; $Temptime = explode(' ', microtime()); $msectime = (float)sprintf('%.0f', (floatval($Temptime[0]) + floatval($Temptime[1])) * 1000); $stringToSign = $msectime . "\n" . $Array['secret']; $stringToSign = $stringToSign; $sig = hash_hmac('sha256', $stringToSign, $Array['secret'], true); $sign = urlencode(base64_encode($sig)); $this->token .= '&timestamp=' . $msectime; $this->token .= '&sign=' . $sign; } // 获取 token public function token() { return $this->token; } // 是否@所有人 public function isAt(bool $type) { $this->isAt = $type; } /** 发送文本类 * @param $content 文本内容 * @return mixed|string */ public function text($content) { $data['msgtype'] = 'text'; $data['text'] = ['content' => $content]; preg_match_all("/(?<=@)\d{11}/", $content, $regular); if ($regular[0]) $data['at']['atMobiles'] = $regular[0]; if ($this->isAt) $data['at']['isAtAll'] = $this->isAt; return $this->send($data); } /** 发送链接类 * @param $title link的标题 * @param $text link的内容 * @param $url 跳转的超链接 * @param string $img 图片的地址 选填 */ public function link($title, $text, $url, $img = '') { $data['msgtype'] = 'link'; // 链接类型 $data['link']['title'] = $title; $data['link']['text'] = $text; $data['link']['messageUrl'] = $url; if ($img) $data['link']['picUrl'] = $img; preg_match_all("/(?<=@)\d{11}/", $text, $regular); if ($regular[0]) $data['at']['atMobiles'] = $regular[0]; if ($this->isAt) $data['at']['isAtAll'] = $this->isAt; return $this->send($data); } /** 发送markdown消息 * @param $post_json * @return mixed|string */ public function markdown($title, $content) { $data['msgtype'] = 'markdown'; //markdown 类型 $data['markdown']['title'] = $title; $data['markdown']['text'] = $content; // 正则取at用户手机号，如果有手机号 则@ preg_match_all("/(?<=@)\d{11}/", $content, $regular); if ($regular[0]) $data['at']['atMobiles'] = $regular[0]; $data['at']['isAtAll'] = $this->isAt; return $this->send($data); } /** * 发送钉钉消息 * 若上面预设的类型不能满足 则在实例化之后自行拼装数组 使用send发送 * @param $post_json * @return mixed|string */ public function send($post_json) { $post_json = json_encode($post_json); try { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://oapi.dingtalk.com/robot/send?access_token=' . $this->token); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json;charset=utf-8')); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_json); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 线下环境不用开启curl证书验证, 未调通情况可尝试添加该代码 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $data = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 获取请求的 curl_close($ch); $resdata['data'] = $data; $resdata['code'] = $httpCode; return $resdata; } catch (Exception $e) { return 'error:' . $e->getMessage(); } } } 食用方法初始化// 使用老版token,一个长度64的字符串 $token = 'qnfxx**********'; $Ding = new Dingtalk($token); // 使用新版 token + 加签 的形式 $token = [ 'token' => ''qnfxx**********'', 'secret' => '' ]; $Ding = new Dingtalk($token); 获取token这样主要是用来获取加签后的 token 一般是用不到的$token = $Ding->token();@所有人发送消息时,是否@所有人,默认不会 @所有人$Ding->isAt(true);发送消息发送消息目前支持四种方式,其中前三种是对第四种的封装.如果提供的封装不能满足发送类型的需要,可以使用第四种方式发送.其中前三个方法均支持直接在内容中 @群成员,需要@其钉钉绑定的手机号即可, 例如 "早上好, @1500xxxxxxx"// 第一种 发送 text 内容 $Ding->text(); // 第二种 发送 link 链接 $Dind->link(); // 第三种 发送 markdown,注意仅支持简单的md语法,以及部分简单的行内样式 $Ding->markdown(); // 第四种 send(),接收一个数组,数组需要按照需要的JSON格式拼装, 可以参考钉钉机器人文档: // https://developers.dingtalk.com/document/robots/custom-robot-access $Ding->send();