两双筷子 - PHP
type: channel
link: https://www.dbkuaizi.com/tag/php/
items: Typecho 垃圾评论过滤插件
items: Yii2 学习笔记
items: Yii 动态字段模型
items: PHP是如何与 Web Server 交互的
items: PHP 生成 MD 数据字典
items: PHP 抽象类和接口
items: Typecho 文章版权插件
items: 什么是 XSS、CSRF、SQL注入？
items: 钉钉自定义机器人PHP类库

Typecho 垃圾评论过滤插件
type: item
date: 2025-01-27T15:18:00+00:00
description: 前言目前使用的垃圾评论过滤插件在添加词库时十分繁琐，需要进入插件的设置页面才能添加。但从操作的角度上来说，直接在评论管理页面进行操作是最符合直觉的，所以有了这款插件。功能兼容性要求：依赖 Typecho 1.2.0+ 版本，利用命名空间功能。无需修改数据库结构：通过插件配置表实现拦截功能，安全高效。便捷操作：支持直接在评论管理页面一键添加屏蔽词，操作直观。智能处理：当新增屏蔽关键词时，自动处理符合条件的待审评论，节省人工审核时间。属性联动拦截：插件可自动检测评论中被拉黑的属性。例如，若拉黑某个 IP 地址，可自动屏蔽该 IP 提交的所有评论内容，并将其加入屏蔽词库。详细统计：在插件管理页面显示以下统计信息：已定义的过滤规则总数累计拦截次数自动收集的屏蔽词数量适配移动端：注入拉黑操作按钮时，适配移动端小屏幕自定义提示：多种拦截提示可选，也可自定义提示内容界面评论管理页面拉黑操作插件配置页面下载去码云下载插件

Yii2 学习笔记
type: item
date: 2024-09-29T11:05:44+00:00
description: 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.p…

Yii 动态字段模型
type: item
date: 2022-03-13T15:12:00+00:00
description: 假设这么一个场景: 有一个通过 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() ?>效果展示构建的表单提交后打印的数据

PHP是如何与 Web Server 交互的
type: item
date: 2021-08-20T21:00:00+00:00
description: 我们都知道 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

PHP 生成 MD 数据字典
type: item
date: 2021-06-14T08:08:00+00:00
description: 端午闲着也是闲着，写了一个MD 数据字典的导出工具，后面有机会会完善PDO支持、索引信息导出、HTML文件导出。使用要求PHP 5.3+MySQL/MariaDBMySQLi 扩展导出内容表相关表类型表存储引擎表字段表备注字段相关字段名字段类型是否为空默认值其他（列自增、时间的自动填充等）字段备注导出的Markdown导出方式下载地址去码云下载

PHP 抽象类和接口
type: item
date: 2021-05-12T14:57:00+00:00
description: 抽象类是什么抽象类是一种特殊的类，使用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{}总结：接口可以继承接口，而且是多继承接口是在大型项目中，为了保证底层的实现而设计的规范，通常是抽象类实现接口，增加必要成员属性，然后让实际业务类去继承抽象类类在继承其他类的时候可以同时去实现相应的接口，不需要父类和接口有任何关系接口和抽象的区别抽象类中可以有非抽象方法，而接口中只能有抽象方法一个类可以实现多个接口，而一个类只能继承一个抽象方法接…

Typecho 文章版权插件
type: item
date: 2021-04-23T17:07:00+00:00
description: 前言我现在用的博客主题，目前并不支持针对单文章的版权设置功能，所以我抽空自己做了一个小插件来实现这个功能。写这个插件的原因有两个，一个是前面说的，我需要一个这样的功能。当然 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、CSRF、SQL注入？
type: item
date: 2021-04-14T15:09:00+00:00
description: 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用户操作

钉钉自定义机器人PHP类库
type: item
date: 2021-03-12T16:27:00+00:00
description: 之前写钉钉群通知机器人的时候，封装了的一个机器人类库。支持 老版机器人发送（只用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, $cont…