阅读视图

发现新文章,点击刷新页面。

PHP 8 重大变更:is_callable() 不再支持类名+非静态方法检查,强制返回 false!

最近在 PHP 8 环境下,发现 #WPJAM Basic# 的一个严重问题:就是后台文章和分类列表的一些操作无效了,点击保存按钮没有任何反应。经过深入调试,发现问题根源在于 PHP 8 对is_callable()函数的行为进行了重大调整。

技术细节

在 PHP 8 之前,is_callable() 函数在检查类名与非静态方法的组合时会返回 true,即使这种调用方式在实际执行时可能会导致问题。PHP 8 对此进行了更严格的检查,以提前发现潜在的错误调用方式,因此 is_callable() 在检查一个类名与非静态方法时将返回失败(应当检查一个类的实例)。

具体看下面这个例子就大概能够明白了:

class Test{
    public function method1() { }
    public static function method2() { }
}

// PHP 8 之前
var_dump(is_callable(['Test', 'method1']));	// bool(true)
var_dump(is_callable(['Test', 'method2']));	// bool(true)

// PHP 8 之后
var_dump(is_callable(['Test', 'method1']));	// bool(false)
var_dump(is_callable(['Test', 'method2']));	// bool(true)
var_dump(is_callable([new Test, 'method1']));	// bool(true)

变更原因

PHP 8 的这一变更是为了更严格地执行面向对象编程规范:

  1. 静态与非静态方法调用的区分:非静态方法需要类的实例才能调用,而静态方法可以直接通过类名调用。
  2. 提前错误检测:在 PHP 7 及以下版本,虽然 is_callable() 会返回 true,但实际以静态方式调用非静态方法时会产生警告。PHP 8 通过 is_callable() 提前返回 false 来更早发现问题。
  3. 代码质量提升:强制开发者明确方法调用的上下文,避免模糊的调用方式。

解决方案

既然知道这个原因,那就是代码要根据上下文来写了。所以如果要检查某个对象的非静态方法是否可调用,应该使用一个对象实例而不是类名。

此外,其实很多时候我们只需要检查类的方法是否存在,而不关心其可调用性(例如访问控制),那么可以简单使用 method_exists() 进行判断就好了:

if (method_exists('Test', 'method1')) {
    // Do something
}

原来 explode 还有第三个参数,竟然这么好用!

explode 是 PHP 一个常见的字符串处理函数,主要用于将字符串按照指定的分隔符拆分成数组。比如下面的代码就是通过英文逗号将字符串转换为数组。

$str	= "apple,banana,orange";
$fruits	= explode(",", $str);

// 结果:
// Array
// (
//     [0] => apple
//     [1] => banana
//     [2] => orange
// )

我相信大部分人(主要是我)都是这么用这个函数,但是绝大部分都不知道,explode 这个 PHP 函数还有第三个参数:$limit,这是一个可选参数,限制返回数组的最大元素数量。

我标题里面说这么好用,好用在哪里呢?🧐

🙋‍♀️🌰 我有个字符串,比如 args=type=select&name=gravatar,这个字符串其实是两部分,第一部分是 args 是 key,第二部分(type=select&name=gravatar),它其实是个 query_string,我的目的到时候通过 WordPress 函数 wp_parse_args 函数是将其解析成数组的。

如果直接使用 explode 分割字符串拆成数组的话:

$str	= "args=type=select&name=gravatar";
$arr	= explode("=", $str);

// 结果:
// Array
// (
//     [0] => args
//     [1] => type
//     [2] => select&name
//     [3] => gravatar
// )

这是我又要使用 implode 函数将除了第一个元素之外,其他元素拼成字符串。

有点烦躁。😒

这是第三个参数就派上用场了,设置为 2,看看结果

$str	= "args=type=select&name=gravatar";
$arr	= explode("=", $str, 2);

// 结果:
// Array
// (
//     [0] => args
//     [1] => type=select&name=gravatar
// )

是不是就是我需要的结果!🥳

最后我们在看看第三个参数 $limit 的用法:

  1. 如果设置了 limit 参数并且是正数,则返回的数组包含最多 limit 个元素,而最后那个元素将包含 str 的剩余部分
  2. 如果 limit 参数是负数,则返回除了最后的 -limit 个元素外的所有元素。
  3. 如果 limit 是 0,则会被当做 1。

好了,以后不要傻傻自己使用 implode 函数拼贴回去了,记得 explode  还有第三个参数的,而且非常好用。

从 Division by zero 到 set_error_handler

最近做了个简单的表格应用,其中有个功能是支持公式运算,如下图,毛利率这列是其他两列相除算出来的:

Division by zero

做完,客户还没用一天,就碰到问题了,因为有时候公式中的被除数是 0,这时候 log 中一堆 「Division by zero」的 Warning,影响查看其他的 log 的查看。

刚开始我想到的办法是在填充公式变量的时候,判断一下被除数是零就抛出「除零错误」,不去真正计算就好了,确实可以解决问题。嘻嘻,我还是挺聪明的。😎

但是很快又碰到新的问题了,因为有这样的公式:$a / ($b + $c) ,然后 ($b + $c) 的结果为 0。😂

如果继续按照原来的做法,我要根据运算的优先级,来判断被除数是 0,如果公式一复杂,那么什么时候是个头啊。🤦🤦‍♂️🤦‍♀️🤕

Warning 还是异常

先搜索了一圈 Division by zero,然后在 PHP 文档,看到 PHP 就已经有一个预定义的异常 DivisionByZeroError

但是为啥我的代码能够执行成功,只是在 log 中出现一堆 「Division by zero」的 Warning,那就继续看文档:

在 PHP 7 中使用算术运算符 / 不会抛出异常,而在 PHP 8 中会抛出异常,可惜客户的系统还在使用 PHP 7.4。

直接问 DeepSeek 我该怎么办?它给了我四个方法:

1. 手动检查并抛出异常

我这个又不是简单的被除数,真的是。🙄

2. 使用 set_error_handler 捕获警告

貌似这个可以。😘

3. 封装除法操作类

我是算术公式,这个不合适。🤪

4. 升级 PHP 8

我能升级就不会问你了。🤬

看来使用方案二了!

使用 set_error_handler 捕获警告

就是通过 set_error_handler 函数将警告转换成异常抛出。

set_error_handler(function($no, $str){
	if(str_contains($str , 'Division by zero')){
		throw new DivisionByZeroError($str); 
	}

	throw new ErrorException($str , $no);

	return true;
});

如果警告信息的字符串里面有 Division by zero,就抛出 DivisionByZeroError 异常,其他情况抛出 ErrorException

然后原来的执行表达式运算的代码改成 try - catch 结构:

try{
	// 原来执行运算表达式代码
}catch(DivisionByZeroError $e){
	return $if_error ?? '!除零错误';
}catch(throwable $e){
	return $if_error ?? '!计算错误';
}

完美解决!🍾🥂🎆🎇🎊

还没开心 5 分钟,很快就来了但是。😞😮‍💨

如果这样自定义错误处理程序,那么之后的代码的警告都被他接管了,然后直接抛出异常。这 🙄🫤

继续查 PHP 文档,问 DeepSeek,原来 set_error_handler 有返回值的,如果之前有定义的错误处理程序,则返回之前定义的错误处理程序,如果没有定义或者是内置的错误处理程序,则返回 null。

这样的话,那就好处理,给 try - catch 结构加上 finally,最终的代码如下:

$handler	= set_error_handler(function($no, $str){
	if(str_contains($str , 'Division by zero')){
		throw new DivisionByZeroError($str); 
	}

	throw new ErrorException($str , $no);

	return true;
});

try{
	// 原来执行运算表达式代码
}catch(DivisionByZeroError $e){
	return $if_error ?? '!除零错误';
}catch(throwable $e){
	return $if_error ?? '!计算错误';
}finally{
	if($handler){ // 之前有定义的错误处理程序
		set_error_handler($handler);
	}else{ // 恢复到内置的错误处理程序
		restore_error_handler();
	}
}

搞定,收工,记录一下!📝 🐂🍺

Cursor 另类应用 — 逆向分析

hacker

昨天登录后台的时候,发现 loginpress 的设置界面挂了,所有的数据样式都是 hidden。让 deepseek 给分析了一通,都是废话,没什么卵用。

今天上午直接让 cursor 分析了一下代码,让给出解决方案,发现依然没什么效果:

不过查看页面元素的时候发现了一段提示:

<tr class="secret_key_v2_invisible" style="display: none;"><th scope="row"><label for="loginpress_setting[secret_key_v2_invisible]">Secret Key</label></th><td><input type="text" class="regular-text" id="loginpress_setting[secret_key_v2_invisible]" name="loginpress_setting[secret_key_v2_invisible]" value=""><p class="description">Get <a href="https://www.google.com/recaptcha/admin" target="_blank"> reCaptcha</a> secret key. <br> <span class="alert-note">Make sure you are adding right  secret key for this domain. If it's incorrect may be you'r not able to access your website.</span></p></td></tr>

让 cusour 分析了一通,依然没什么效果:

不过此时已经基本确认还是授权的问题了,基于上面的废话改起来难度较大,搜索了一下,找了一个新版本的 loginpress pro。

https://wpe98.com/loginpress-pro-nulled/

按照文件的方法直接添加 hook 代码:

<?php
function bypass_license_verification() {
    add_filter('loginpress_license_check', '__return_true');
    add_action('admin_notices', function() {
        echo '<div class="notice notice-success"><p>License successfully nulled for demonstration purposes!</p></div>';
    });
}
add_action('init', 'bypass_license_verification');
?>

此时后台显示的确 ok 了,虽然功能已经激活了,但是页面看起来有些蛋疼,显示没授权: 

根据提示信息搜索就会发现代码位于loginpress-pro 2/classes/loginpress-main.php

大概浏览下代码,发现基本数据都在所谓的授权信息中license_data的结构内,直接让 cursor 给生成一个结构:

此时直接修改授权代码为:

public static function get_registration_data() {
        $license_data = array(
            'success' => true|false,      // 布尔值,表示许可证验证是否成功
            'license' => 'valid',         // 字符串,许可证状态,可能的值包括: valid, invalid, expired, revoked, missing, site_inactive, item_name_mismatch, no_activations_left
            'error' => '',               // 字符串,如果有错误时的错误代码
            'expires' => '2050-10-10',             // 字符串,许可证过期日期,可以是具体日期或 'lifetime'
            'price_id' => 4,            // 整数,许可证价格ID,用于确定许可证类型:
                                        // 1 = Personal
                                        // 2 = Small Business
                                        // 3 或 6 = Agency
                                        // 4 = Ultimate
                                        // 7 = Startup
        );
return $license_data;
}

测试了一下没效果,继续看代码,发现外面还套嵌了一层,cursor 给继续处理一下:

继续调整代码为:

$result = array(
            'license_key' => '',         // 字符串,许可证密钥
            'license_data' => array(     // 数组,许可证详细数据
                'success' => true|false,      // 布尔值,表示许可证验证是否成功
                'license' => 'valid',         // 字符串,许可证状态,可能的值包括: valid, invalid, expired, revoked, missing, site_inactive, item_name_mismatch, no_activations_left
                'error' => '',               // 字符串,如果有错误时的错误代码
                'expires' => '2050-10-10',             // 字符串,许可证过期日期,可以是具体日期或 'lifetime'
                'price_id' => 4,            // 整数,许可证价格ID,用于确定许可证类型:
                                            // 1 = Personal
                                            // 2 = Small Business
                                            // 3 或 6 = Agency
                                            // 4 = Ultimate
                                            // 7 = Startup
            ),
            'error_message' => '',      // 字符串,错误信息
        );
        return $result;

此时再看效果,就完美啦:

 

The post Cursor 另类应用 — 逆向分析 appeared first on obaby@mars.

给博客增加 文章归档 和 年度统计 功能

终于在建站11年的当下终于给博客增加了文章归档页。
是基于 Rank Math SEO 插件的 Sitemap Settings 功能实现了一个简单的文章归档页。
顺带在文章归档页内塞了一个额外的网站数据年度统计功能。
大概长这样:文章归档


起因 为什么想要加文章归档页了

起因是在《WindowTabs: 让几乎一切 Windows 窗口支持多标签功能》一文下,博友 ddw2019 在评论中提到我的站点“没有找到 Archive 页面,一个个翻页起来,感觉还是有点点费劲的呀。”

之前我一直感觉文章归档页的感知不强,我认为它的主要意义和好处是:系统化整理文章、提高可发现性、增强用户体验、SEO优化、展示创作历程这 5 个方面。

毕竟现在右侧有文章分类、随机文章、热门文章三个模块。系统化整理文章提高可发现性性上已经足够了。
SEO 优化上文章归档页固然归可以帮助搜索引擎更好地抓取和索引博客内容,不过在已经有 xml 站点地图的时候,文章归档页对于提供博客的 SEO 优化上作用不大,相较这种 html 格式站点地图,xml 格式的站点地图对搜索引擎会更加友好一些。

不过嘛,确实考虑到博客里确实是缺少一个展示创作历程和增强用户体验的东西。于是在2024年的年底,终于在建站 11 年后决定加上这个东西。

思考 实现文章归档页的方案

那么现在问题来了,如何多快好省的实现文章归档页 Archive 呢?
从这个博客的可以称之为极简的风格上估计大家都能看出,我并不太喜欢在美化折腾博客页面上消耗过多精力。

  • 方案一 网友 ddw2019 的博客中写到的方案 年度归档插件
    好处是省事的多,装个插件的事情。缺点是又双叒叕需要多装一个插件了,博客速度又要被拖累。
  • 方案二 利用现有插件 Rank Math SEO 的站点地图功能实现。
    好处是利用了现有插件,少装了一个插件,缺点是样式就简陋的多了,样式的自定义空间也小的多。

  • 方案三 自己实现一个页面
    优点是更加轻量,样式完全可以自定义。缺点是自己写东西需要费更多的时间。

权衡利弊后,我决定先用方案二顶上,先解决有无问题再说。

开干 实现文章归档

以下的必要前提是:你要有装 Rank Math SEO 这个插件,并且开启了他的 Sitemap 组件。
这里我只介绍必要的操作,如果想了解更多一点,这里有篇不错的文章 《家庭数据中心系列Rank Math SEO设置与优化-无敌的个人博客》

  1. 新建一个页面,起名叫文章归档并把固定链接设置为ex.your.domain/archives并发布。
    (是的,内容为空就行)
  2. 进入博客后台,左侧找到 Rank Math SEO 的 Sitemap Settings 部分,切换到 HTML Sitemap 标签,并开启开关。

  3. 将 Display Format (显示模式)设置为 Page 也就是页面模式。如果你想在文章归档页放入额外的东西,也可以设置为 Shortcode 模式,这样上一步时,需要在你想要插入文章归档页的地方写短代码[rank_math_html_sitemap] 即可。

  4. Sort by (排序方式),既然是文章归档页,那自然是选 Published Date (基于发表日期)啦。并且开启下边的 Show Dates (显示日期),这样文章后面就会显示发布日期了。

  5. Item Titles (项目标题),作为文章归档页,建议选 Item Titles (文章标题)而不是 SEO Titles (SEO用标题)这两者的区别是:SEO用标题是给搜索引擎和搜索结果用户看的,可能会比实际的文章标题会有些奇妙的拉长和缩短。比如文章实际标题很长,但是 SEO 标题可能会为了能保证在搜索结果页把关键词凸显出来会相对减少次要描述。也就是说 Item Titles (文章标题)更能反映文章内容。

  6. 最后保存即可,现在前边设定的固定链接看到一个比较简陋的,文章归档页了。
    类似本站这样:文章归档 (https://www.tjsky.net/archives)

增强 增加博客年度数据统计功能

本来事情就该结束了,结果中午的时候,我又在博友圈里看到了 ts小陈 的文章:《为WordPress网站添加年度数据汇总
我发现,“诶,这个东西不错嘛,可以很直观的展现每年博客的发展数据。”
随后尝试直接套用代码,然后果不其然的发现:事情并没有那么简单。
毕竟我现在用的主题上次更新已经是2022年了,主题并没有完美支持页面模板php文件。
并不能像他原文介绍的那样:

  • 将php文件上传到你主题的页面模板文件夹(pages文件夹)
  • 创建新页面选择该模板-年度数据
  • 发布页面,搞定。

于是我又研究了一下WordPress这个页面模板php文件应该怎么写,以及他代码具体是做了什么。
重新写了适配自己主题的页面模板文件。主要修改点是三个

  • 修改一下样式适配博客主题
    主要是套用原有page页的代码,改一个新的模板文件。还有些小修改,比如表格做成圆角啊,表格改个主题色啊,属于毫无技术含量的修改。
  • 修改其中统计字数的函数str_word_count()mb_strlen()
    中文不用空格来分隔单词,对于中文字符串,str_word_count()会将两个标点之间的整个字符串都视为一个单词,mb_strlen()则会根据当前PHP设定的编码自适应用基于UTF8或gbk等编码正确统计字数。所以改一下得到准确的字数统计。

//修改前的代码,在原文文件的第79行
word_count = str_word_count(strip_tags(content)); // 去除HTML标签后计算字数

//修改后的代码
word_count = mb_strlen(strip_tags(content)); // 去除HTML标签后计算中文字数

大部分面板和一键脚本部署的 PHP 应该默认就开了 mb_strlen 这个函数,不过对于用虚拟主机的可能需要注意下,这个 PHP 函数不一定被启用了。

  • 把缓存改成1天,30天有些长了。

原文写的是”数据使用 Memcached 缓存”,让人以为只能用于 Memcached。不过实际上,如果你用 Redis 或者其他什么缓存,只要你装了对应的缓存插件,比如用 Redis 并且装了 Redis Object Cache。可以保证wp_cache_get()wp_cache_set() 函数能正常工作就不需要注释掉缓存代码,不然每次都现场计算一次,这性能浪费不小。

最终效果如下图

很直观的数据
2017年上一个博客被我rm -rf */干没了之后,在 17 年 12月 底重建,18 年还在保持正常的更新,可惜博客没啥人看(看评论数量),外在驱动力不足 导致 19 ~ 21 年我基本上没更新过博客,21 年底,因为一些机缘巧合博客收获了一波较大流量,加之因为这个机缘巧合重新拾起写代码的乐趣,重新开始恢复正常的更新频率。

The post 给博客增加 文章归档 和 年度统计 功能 appeared first on 秋风于渭水.

还没升级到 PHP 8.4,也可以提前在 WordPress 中使用 PHP 8.4 引入的数组函数

PHP 升级到了 8.4,个人觉得对我们写代码帮助最大的就是 PHP 8.4 引入的 array_find()array_find_key()array_any() 和 array_all() 这 4 个 array_*() 数组函数。

其实这四个函数在 JavaScript 的 Array 的实例方法中就有了,只是他的命名方式和 PHP 有点不一样,就他是用 every 和 some 而不是 all 和 any,我之前写代码的时候,也有一些地方需要使用到这四个数组函数,所以我基于 JavaScript 的方式在 #WPJAM Basic# 中集成了这四个方法。

在 PHP 8.4 发布之后我基于 PHP 8.4 命名方式做了一些优化,使得大家在没有升级到 PHP 8.4,也可以提前在 WordPress 中使用 PHP 8.4 引入的数组函数。

array_find()

array_find() 是返回数组中第一个满足回调函数的元素,如果没有则返回 null,根据这个特性,我们很容易就是先这个函数:

if(!function_exists('array_find')){
	function array_find($arr, $callback){
		foreach($arr as $k => $v){
			if($callback($v, $k)){
				return $v;
			}
		}
	}
}

比如我在字段处理中,如果字段没有定义类型,我根据它是否有其他属性来给他设置类型的代码,如果没有 array_find,我们一般这么写:

if(!empty($field['options'])){
	$field['type']	= 'select';
}elseif(!empty($field['label'])){
	$field['type']	= 'checkbox';
}elseif(!empty($field['fields'])){
	$field['type']	= 'fieldset';
}else{
	$field['type']	= 'text';
}

有了 array_find,一行搞定了:

$field['type']	= array_find(['options'=>'select', 'label'=>'checkbox', 'fields'=>'fieldset'], fn($v, $k)=> !empty($field[$k])) ?: 'text';

array_find_key()

array_find_key()array_find() 很像,只是它返回数组中第一个满足回调函数的元素的健名:

if(!function_exists('array_find_key')){
	function array_find_key($arr, $callback){
		foreach($arr as $k => $v){
			if($callback($v, $k)){
				return $k;
			}
		}
	}
}

相比 array_find() 函数的定义,唯一的区别,它 return 的是 key 而不是 value。

上面的获取字段的类型的代码,也可以使用 array_find_key() 来实现,大家看看区别:

$field['type']	= array_find_key(['select'=>'options', 'checkbox'=>'label', 'fieldset'=>'fields'], fn($v, $k)=> !empty($field[$v])) ?: 'text';

array_all()

array_all() 判断数组中的每个元素是否都满足回调函数,意思是将每个元素传递给回调函数,都要返回 true

if(!function_exists('array_all')){
	function array_all($arr, $callback){
		foreach($arr as $k => $v){
			if(!$callback($v, $k)){
				return false;
			}
		}

		return true;
	}
}

比如我写的 SMTP 发信扩展里面就有判断代码,需要「发信地址」,「邮箱账号」和「邮箱密码」这三个选项都设置了之后才生效:

if(array_all(['host', 'user', 'pass'], fn($k)=> WPJAM_SMTP::get_setting($k))){
	// 执行代码
}

array_any()

array_any() 也是判断数组的元素是否满足满足回调函数,但是 array_any() 只需要一个满足即可,而 array_all() 一样则是需要全部都满足:

if(!function_exists('array_any')){
	function array_any($arr, $callback){
		foreach($arr as $k => $v){
			if($callback($v, $k)){
				return true;
			}
		}

		return false;
	}
}

上面例子中,发信设置扩展需要「发信地址」,「邮箱账号」和「邮箱密码」这三个选项都要设置,换成 array_any() 的思维,则只要一个没有设置就返回:

if(array_any(['host', 'user', 'pass'], fn($k)=> !WPJAM_SMTP::get_setting($k))){
	return;	// 返回,不生效
}

大家对比一下两个例子的代码,使用相类似的例子,通过不同的写法,也是一种更加清晰的区分函数的方式。😄

标题说了,还没升级到 PHP 8.4,也可以提前在 WordPress 中使用 PHP 8.4 引入的数组函数,意思只要你更新 WPJAM Basic 到最新版就能使用了,所以记得更新,有什么问题,记得给我留言。

PHP 8.4 正式版发布,一文快速预览新功能

PHP 8.4 在 11 月 21 号就发布了,它包含了许多新功能,对于 WordPress 用户来说,了解 PHP 新版的功能也是一个简单必要的工作,并且  8.4 版是 PHP 在 2024 年的年度的一个主要的版本更新,所以我们就简单快速过一遍 PHP 8.4 的新功能。😄

属性挂钩

对象属性现在可以其 get 和 set 操作中关联相关的附加逻辑。根据用法,这可能会也可能不会使属性变为虚拟属性,即该属性根本没有实际的存储值。

<?php
class Person
{
    // “虚拟”属性,可能无法明确设置。
    public string $fullName {
        get => $this->firstName . ' ' . $this->lastName;
    }

    // 所有的写入操作都会经过这个挂钩,结果就是写入的内容。
    // 读取访问正常。
    public string $firstName {
        set => ucfirst(strtolower($value));
    }

    // 所有的写入操作都会经过这个挂钩,它必须写入支持值本身。
    // 读取访问正常。
    public string $lastName {
        set {
            if (strlen($value) < 2) {
                throw new \InvalidArgumentException('Too short');
            }
            $this->lastName = $value;
        }
    }
}

$p = new Person();

$p->firstName = 'peter';
print $p->firstName; // 打印“Peter”
$p->lastName = 'Peterson';
print $p->fullName; // 打印“Peter Peterson”

不对称属性可见性

现在可以将对象属性的 set 可见性和 get 可见性分开控制。

<?php
class Example
{
    // 第一个可见性修饰符控制 get 可见性,第二个修饰符控制 set 可见性。
    // The get-visibility must not be narrower than set-visibility.
    public protected(set) string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

惰性对象

现在可以创建对象,将初始化延迟到访问时。库和框架可以利用这些惰性对象来延迟获取初始化所需的数据或依赖项。

<?php
class Example
{
    public function __construct(private int $data)
    {
    }

    // ...
}

$initializer = static function (Example $ghost): void {
    // 获取数据或者依赖项
    $data = ...;
    // 初始化
    $ghost->__construct($data);
};

$reflector = new ReflectionClass(Example::class);
$object = $reflector->newLazyGhost($initializer);

#[\Deprecated] 属性

新的 #[\Deprecated] 属性使 PHP 的现有弃用机制可用于用户定义的函数、方法和类常量。

class PhpVersion
{
    #[\Deprecated(
        message: "use PhpVersion::getVersion() instead",
        since: "8.4",
    )]
    public function getPhpVersion(): string
    {
        return $this->getVersion();
    }

    public function getVersion(): string
    {
        return '8.4';
    }
}

$phpVersion = new PhpVersion();
// Deprecated: Method PhpVersion::getPhpVersion() is deprecated since 8.4, use PhpVersion::getVersion() instead
echo $phpVersion->getPhpVersion();

新的 ext-dom 功能和 HTML5 支持

新的 DOM API 包括符合标准的支持,用于解析 HTML5 文档,修复了 DOM 功能行为中的几个长期存在的规范性错误,并添加了几个函数,使处理文档更加方便。

新的 DOM API 可以在 Dom 命名空间中使用。使用新的 DOM API 可以使用 Dom\HTMLDocument 和 Dom\XMLDocument 类创建文档。

$dom = Dom\HTMLDocument::createFromString(
    <<<HTML
        <main>
            <article>PHP 8.4 is a feature-rich release!</article>
            <article class="featured">PHP 8.4 adds new DOM classes that are spec-compliant, keeping the old ones for compatibility.</article>
        </main>
        HTML,
    LIBXML_NOERROR,
);

$node = $dom->querySelector('main > article:last-child');
var_dump($node->classList->contains("featured")); // bool(true)

BCMath 的对象 API 

新的 BcMath\Number 对象使在处理任意精度数字时可以使用面向对象的方式和标准的数学运算符。

这些对象是不可变的,并实现了 Stringable 接口,因此可以在字符串上下文中使用,如 echo $num

use BcMath\Number;

$num1 = new Number('0.12345');
$num2 = new Number('2');
$result = $num1 + $num2;

echo $result; // '2.12345'
var_dump($num1 > $num2); // false

新的 array_*() 函数

新增函数 array_find()array_find_key()array_any() 和 array_all()

$animal = array_find(
    ['dog', 'cat', 'cow', 'duck', 'goose'],
    static fn (string $value): bool => str_starts_with($value, 'c'),
);

var_dump($animal); // string(3) "cat"

PDO 驱动程序特定子类

新的 Pdo\DblibPdo\FirebirdPdo\MySqlPdo\OdbcPdo\Pgsql 和 Pdo\Sqlite 的子类可用。

$connection = PDO::connect(
    'sqlite:foo.db',
    $username,
    $password,
); // object(Pdo\Sqlite)

$connection->createFunction(
    'prepend_php',
    static fn ($string) => "PHP {$string}",
); // Does not exist on a mismatching driver.

$connection->query('SELECT prepend_php(version) FROM php');

new MyClass()->method() 不需要括号

现在可以在不使用括号包裹 new 表达式的情况下访问新实例化对象的属性和方法。

$connection = PDO::connect(
    'sqlite:foo.db',
    $username,
    $password,
); // object(Pdo\Sqlite)

$connection->createFunction(
    'prepend_php',
    static fn ($string) => "PHP {$string}",
); // Does not exist on a mismatching driver.

$connection->query('SELECT prepend_php(version) FROM php');

新的类、接口和函数

新的 延迟对象。
基于 IR 框架的新 JIT 实现。
新增 request_parse_body() 函数。
新增 bcceil()、bcdivmod()、bcfloor() 和 bcround() 函数。
新增 RoundingMode 枚举用于 round(),包括 4 个新的舍入模式 TowardsZero、AwayFromZero、NegativeInfinity 和 PositiveInfinity。
新增 DateTime::createFromTimestamp()、DateTime::getMicrosecond()、DateTime::setMicrosecond()、DateTimeImmutable::createFromTimestamp()、DateTimeImmutable::getMicrosecond() 和 DateTimeImmutable::setMicrosecond() 方法。
新增 mb_trim()、mb_ltrim()、mb_rtrim()、mb_ucfirst() 和 mb_lcfirst() 函数。
新增 pcntl_getcpu()、pcntl_getcpuaffinity()、pcntl_getqos_class()、pcntl_setns() 和 pcntl_waitid() 函数。
新增 ReflectionClassConstant::isDeprecated()、ReflectionGenerator::isClosed() 和 ReflectionProperty::isDynamic() 方法。
新增 http_get_last_response_headers()、http_clear_last_response_headers() 和 fpow() 函数。
新增 XMLReader::fromStream()、XMLReader::fromUri()、XMLReader::fromString()、XMLWriter::toStream()、XMLWriter::toUri() 和 XMLWriter::toMemory() 方法。
新增 grapheme_str_split() 函数。

弃用和向后不兼容

IMAP、OCI8、PDO_OCI 和 pspell 扩展已从 PHP 中分离并移至 PECL。
隐式可空参数类型现已弃用。
使用 _ 作为类名现已弃用。
将零的负数次幂现已弃用。
向 round() 传递无效模式将抛出 ValueError。
来自扩展 date、intl、pdo、reflection、spl、sqlite、xmlreader 的类常量现在是有类型的。
GMP 类现已是 final 类。
已删除 MYSQLI_SET_CHARSET_DIR、MYSQLI_STMT_ATTR_PREFETCH_ROWS、MYSQLI_CURSOR_TYPE_FOR_UPDATE、MYSQLI_CURSOR_TYPE_SCROLLABLE 和 MYSQLI_TYPE_INTERVAL 常量。
已弃用 mysqli_ping()、mysqli_kill()、mysqli_refresh() 函数,mysqli::ping()、mysqli::kill()、mysqli::refresh() 方法,以及 MYSQLI_REFRESH_* 常量。
stream_bucket_make_writeable() 和 stream_bucket_new() 现在返回 StreamBucket 实例而不是 stdClass。
exit() 行为变更。
E_STRICT 常量已弃用。

所以你打算升级吗?我打算先把 WPJAM 所有插件的最低版本的要求从 7.4 先升级到 8.0 了,不过现在我已经开始修复一些插件在 PHP 8 版本的兼容问题了。

使用Docker搭建Umami统计,显示近一年的pv、uv数据的API搭建

因为百度统计使用unload导致浏览器报错和一系列性能问题,并且去广告插件之类的对百度统计都有屏蔽,导致无法获取真实的数据,所以决定之下还是选择了自搭建umami。这也是向访客隐私迈出的一大步,也就是说在未来我的博客将不会将访客数据传输给第三方,避免了潜在的隐私风险。

从 LAMP 到 Docker based PaaS 工具

白宦成简史 当中,我写到过,我从 2013 年就开始写博客,至今已经 11 年有余。而我和互联网、编程的缘分,也从 2013 年开始。

在 2013 年的时候,我主要是使用 WordPress 建站(现在也还在用,比如本站)。所以,从哪个时候开始,我开始接触 LAMP、LNMP 这些个概念,并在过去的若干年里,使用了不少「一键安装包」来部署我的网站。

我用的一键安装包 / 控制面板不算少:LNMP.orgOneInStack(从它还是 LinuxEye 的时候开始用),LAMP.sh等一键配置包,AMHWDCPAppNodeWebminBTVestaCPVirtualmin等等一系列控制面板。

如果说这些工具有什么相同点,那便是都提供了十分方便的 LAMP / LNMP 的配置方式,让彼时不够专业的我、主要是用别人开发好的应用的我能够快速部署一个基于 MySQL + PHP 的应用,让它 Run 起来。

而随着时间的流逝,我已经不再是曾经的我了。我不再局限于使用别人写好的程序,我开始自己写;我不再局限于使用 PHP 来编写程序,我同样会使用 Python、Ruby、Golang 、Node.js 来编写应用程序;所有的这些,都告诉我,我需要在现有的框架和程序上去做很多额外的配置,比如,我需要在 LNMP 的基础之上,配置 NPM,以完成 Node.js 的构建;我需要在系统上配置 Docker ,以便于去运行某些需要复杂配置的环境。

曾经那些可以帮到我的程序已经不再能帮到我了,如今的他们,成了我的累赘。我开始需要为了他们去多做一些事情了。

如今的我,更需要的是一个能够基于 Docker 来运行的管理工具,能够帮助我完成不同环境的配置、管理的能力。我需要的是一个类似于 Heroku 的管理工具,能够让我把更多的精力放在把事做好上。

我试用了

最终,选择了 CapRover ,主要原因有几个:

  1. 支持基础的 Docker 管理功能:这样意味着我其实可以在网页端管理这些资源。
  2. 使用 Nginx ,并集成了 Let’s Encrypt:我的应用都希望有 HTTPs 的能力,所以默认集成了 Let’s Encrypt 可以帮助我解决不少的问题。我也不需要自己去维护一个 Traefik 来解决请求转发的问题(我没有使用 Rancher / Kubesphere 之类的容器管理平台也是这个原因)
  3. 提供了一些一键配置的 Sample:这意味着我把一些我常用的应用迁移过去的时候,可以抄袭一下其官方推荐的配置,可以降低我的使用门槛。
  4. 足够久远:CapRover 作为一个从 2017 年就开始运作的工具,代表着有足够多的 issue 可以供我参考 / 使用,可以减少我踩坑的概率。
  5. 提供了 CLI 来进行部署:对于一个经常需要部署的人来说,提供 CLI / Github Action 可以帮助我快速实现多种不同需求下的部署,帮助我来提升效率。

种种的这些,让我最终从过去的 LNMP,跳船到了 Docker Base PaaS 工具上。

近期调整计划

最近这几天心情挺烦躁,有时候压抑,有时候暴躁的很。真不知怎么了做事也特别没有耐心,也许是太多事压在心头闹的! 试了新的wordpress自带主题,感觉还蛮不错的,配置起来还不是很麻烦。 最近网站的一些调整: 1. 打算把文章别名改为数字ID,因为有时候文章名会重复,也懒得在一次次修改这个拼音别名了。(毕竟新版wordpress已经很好了,别名终将成为过去,必须接受新的事物。)-待定 2. 彻底修改图床程序,现已改为wordpress,图片链接地址不变。-已实现 3. 准备取消自建的统计?统计了又有什么用?CF自带的也挺爽,折腾这些多余的没啥作用。-待定 4. 有计划取消自建的RSS,2-3究其 […]

测试一下WordPress后台SVG格式图片上传

试着用纯代码的方式来开启wordpress SVG格式图片上传功能。目前看使用Block编辑器图片能正常上传和显示。

function minuo_allow_additional_mime_types($mime_types) {
	if ( ! current_user_can( 'administrator' ) ) {
		return $mime_types;
	}
	$mime_types['svg'] = 'image/svg+xml';
	$mime_types['svgz'] = 'image/svg+xml';
	$mime_types['webp'] = 'image/webp';
	$mime_types['ico'] = 'image/vnd.microsoft.icon';
	return $mime_types;
}
add_filter('upload_mimes', 'minuo_allow_additional_mime_types');

function minuo_wp_check_filetype_and_ext( $wp_check_filetype_and_ext, $file, $filename, $mimes, $real_mime ) {
	if ( ! $wp_check_filetype_and_ext['type'] ) {
		$check_filetype  = wp_check_filetype( $filename, $mimes );
		$ext = $check_filetype['ext'];
		$type = $check_filetype['type'];
		$proper_filename = $filename;
		if ( $type && 0 === strpos( $type, 'image/' ) && 'svg' !== $ext ) {
			$ext  = false;
			$type = false;
		}
		$wp_check_filetype_and_ext = compact( 'ext', 'type', 'proper_filename' );
	}
	return $wp_check_filetype_and_ext;
}
add_filter('wp_check_filetype_and_ext', 'minuo_wp_check_filetype_and_ext' , 10, 5);

strlen、mb_strlen、str_word_count的区别

strlen() 、mb_strlen()和 str_word_count() 是 PHP 中的三个字符串处理函数。

strlen — 获取字符串长度。

strlen() 函数用于获取一个字符串的长度,返回值为整数类型,表示字符串中字符的数量。该函数不会计算字符串中的空格或其他特殊字符,只计算字符本身的数量。

例如,如果有一个字符串 $str = "hello world";,则使用 strlen() 函数可以获取它的长度:echo strlen($str); 将输出 11,因为字符串中有 11 个字符,包括空格。

<?php
$str = 'abcdef';
echo strlen($str); // 6

$str = ' ab cd ';
echo strlen($str); // 7
?>

mb_strlen — 获取字符串的长度。

mb_strlen ( string $str [, string $encoding = mb_internal_encoding() ] )

返回具有 encoding 编码的字符串 str 包含的字符数。 多字节的字符被计为 1。如果给定的 encoding 无效则返回 FALSE

str_word_count — 返回字符串中单词的使用情况。

函数用于计算一个字符串中单词的数量。该函数默认会将所有连续的字母字符视为一个单词,并返回单词的数量。如果需要统计非字母字符,可以使用第二个参数来指定统计方式,例如 str_word_count($str, 1) 将返回一个数组,其中包含字符串中所有单词的位置和长度信息。

例如,如果有一个字符串 $str = "hello world";,则使用 str_word_count() 函数可以获取它的单词数量:echo str_word_count($str); 将输出 2,因为字符串中只有两个单词。

如果使用 str_word_count($str, 1),则可以获取每个单词的位置和长度:print_r(str_word_count($str, 1)); 将输出 Array ( [0] => 0 [1] => 5 [2] => 6 [3] => 11 ),其中第一个元素表示第一个单词的位置和长度,第二个元素表示第二个单词的位置和长度。

总之,strlen() 、mb_strlen()和 str_word_count() 函数都是字符串处理函数,用于获取字符串的长度和单词数量。根据具体的需求,可以选择使用不同的函数来处理字符串。

以上,其实还是等于没写,没有说清楚,

WordPress函数:add_action(添加动作)用法

将函数连接到指定action(动作)。

Plugin API/Action Reference 上查看动作hook列表。wordpress核心调用do_action() 时触发动作。

用法:

<?php
   add_action( $tag, $function_to_add, $priority,
         $accepted_args );
?> 

参数:

$tag

(字符串)(必填)$function_to_add  所挂载的动作(action)的名称。(在Plugin API/Action Reference 上查看动作hook列表)。也可以是一个主题或插件文件内部的一个动作,或者特定的标签“all”,这个函数将被所有的钩子(hooks)调用。

默认值:None

$function_to_add

(回调)(必填)你希望挂载的函数的名称。注:在 PHP“回调”类型文档中 所罗列的字符串格式化的语法均可用。

默认值:None

$priority

(整数)(可选)用于指定与特定的动作相关联的函数的执行顺序。数字越小,执行越早,具有相同优先级的函数在它们被添加到动作的顺序执行。

默认值:10

$accepted_args

(整数)(可选)挂钩函数所接受的参数数量。在 WordPress1.5.1 及以后的版本中,挂钩函数可以是调用do_action() 或 apply_filters()时设置的参数。例如,comment_id_not_found动作将传递任何函数,若该函数将所请求的评论编号连接到该动作。

默认值:1

返回值

(布尔)总是True。


示例:博客发表新内容时用电子邮件通知朋友:

function email_friends( $post_ID )  
{
   $friends = 'bob@example.org, susie@example.org';
   wp_mail( $friends, "sally's blog updated", 'I just put something on my blog: http://blog.example.com' );
 
   return $post_ID;
}
add_action( 'publish_post', 'email_friends' );

接受的参数

挂钩函数可以选择接受从动作调用的参数,如果有任何要传递的话。在这个简单的例子中,echo_comment_id  函数需要 $comment_id 参数,该参数将在 comment_id_not_found 过滤钩子运行时通过 do_action() 传递。

function echo_comment_id( $comment_id ) 
{
   echo 'Comment ID ' . $comment_id . ' could not be found';
}
add_action( 'comment_id_not_found', 'echo_comment_id', 10, 1 );

注释

要找出一个动作的参数的ID和名称,只需搜索匹配 do_action() 调用的代码库。举例来说,如果你挂载到’save_post’,你会在 post.php 找到:

<?php do_action( 'save_post', $post_ID, $post ); ?>

你的 add_action 调用将是这样:

<?php add_action( 'save_post', 'my_save_post', 10, 2 ); ?>

而且你的函数将是这样:

function my_save_post( $post_ID, $post )
{
   // do stuff here
}

在一个类中使用 add_action

当你的插件或主题使用类来创建时,使用 add_action 钩子,在类中添加 $this 和 函数名称 到你的 add_action 回调,像这样:

class MyPluginClass
{
    public function __construct()
    {
         //add your actions to the constructor!
         add_action( 'save_post', array( $this, 'myplugin_save_posts' ) );
    }
    
    public function myplugin_save_posts()
    {
         //do stuff here...
    }

源文件

add_action() 位于 wp-includes/plugin.php

❌