链式调用 PHP 标准函数

2025/02/14 PHP

PHP内置函数太多,记不住怎么办?尤其PHP 一直被人垢病函数名不统一,这就导致更不容易记忆了。这时候是不是在想,要是我随便定义一个变量,就能列出可以对其进行操作的函数就好了,虽然列出来也不一定知道用哪个,但起码一般情况下可以根据函数名猜到哪个才是要用的函数。例如$a = "Hello world";,我在IDE上输入$a->IDE就能将可用函数显示出来。简单,这不就是面向对象吗?那就动手吧。

首先定义一个函数,用于将基础类型封装成类,以字符串为例,Str类有一个静态函数wrap接收一个字符串参数,返回Str的实例,并且将$value参数赋给$value属性,那么wrap的返回值就是对象,可以以->符号来调用属于Str的方法了。

<?php

class Str
{
    private mixed $value;

    public static function wrap(string $value): Str
    {
        return new self($value);
    }
    
    public function __construct(string $value)
    {
        $this->value = $value;
    }
}

这时候就碰到一个问题,字符串操作函数那么多,难道我们要一个个封装进类里面?这种方法也太落后且难维护了吧。幸好PHP有个魔术方法__call,思路打开,那么我们是不是可以通过__call来调用所有函数?实践起来吧。

    public function __call($callable, $args)
    {
        $this->value = $callable(...$args);
        return $this;
    }

但是现在还有个问题,就是例如封装了一个字符串"foo",我想对它调用strlen函数,显然Str::wrap("foo")->strlen()是行不通的,因为没有参数,默认将封闭的值传进去?在调用strlen时没问题,但是并非所有函数都将操作参数放在第一位,所以需要让__call方法知道我们的参数位置才行。方法是定义一个唯一值来标记封装值所在位置,这里我们使用uniqid函数,虽然没有一些uuid库那么标准,但在这里也够用了。然后对__call方法进行一定调整,调整后的代码如下:

<?php

if (!defined('wrapped_placeholder')) {
    define('wrapped_placeholder', 'wrapped-placeholder-' . uniqid());
}

class Str
{
    private mixed $value;

    public static function wrap(string $value): Str
    {
        return new self($value);
    }

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

    public function __call($callable, $args)
    {
        foreach ($args as &$arg) {
            if ($arg === wrapped_placeholder) {
                $arg = $this->value;
            }
        }

        $this->value = $callable(...$args);
        return $this;
    }
}

现在基本雏形已经出来了。使用方法如下:

<?php

print_r(Str::wrap("foo")->strlen(wrapped_placeholder));
// 输出:
// Str Object
// (
//     [value:Str:private] => 3
// )

大概就是这样,但结果是Str对象,显然不是我们想要的结果。再加个函数获取实际的结果吧。

    public function get(): mixed
    {
        return $this->value;
    }

由于get方法直接通过之前的__call方法逻辑是行不通的,需要对其修改,当方法不可调用时,就调用对象内部的方法。

    public function __call($callable, $args)
    {
        if (!is_callable($callable)) {
            return (function (callable $callable) use ($args) {
                return $callable(...$args);
            })($this->$callable);
        }

        foreach ($args as &$arg) {
            if ($arg === wrapped_placeholder) {
                $arg = $this->value;
            }
        }

        $this->value = $callable(...$args);
        return $this;
    }

完结,撒花。

<?php

if (!defined('wrapped_placeholder')) {
    define('wrapped_placeholder', 'wrapped-placeholder-' . uniqid());
}

class Str
{
    private mixed $value;

    public static function wrap(string $value): Str
    {
        return new self($value);
    }

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

    public function __call($callable, $args)
    {
        if (!is_callable($callable)) {
            return (function (callable $callable) use ($args) {
                return $callable(...$args);
            })($this->$callable);
        }

        foreach ($args as &$arg) {
            if ($arg === wrapped_placeholder) {
                $arg = $this->value;
            }
        }

        $this->value = $callable(...$args);
        return $this;
    }

    public function get(): mixed
    {
        return $this->value;
    }
}

$res = Str::wrap('hello_world_that_is_so_interesting_just_a_test')->strtoupper(wrapped_placeholder)->str_replace(['A', 'E', 'I', 'O', 'U'], ['a', 'e', 'i', 'o', 'u'], wrapped_placeholder)->get();
var_dump($res);
// 输出:
// string(46) "HeLLo_WoRLD_THaT_iS_So_iNTeReSTiNG_JuST_a_TeST"

等等,我们做这些的目的是啥?是为了IDE提示啊,调用是可以了,但IDE它没提示呢,差了最关键的一步。IDE的提示使用文档注解,跟俗称的注释差不多吧,就在Str顶部加上函数原型即可。PHP内置的参数这么多,总不能一个个自己写吧,配套原型生成代码这就给你,以下代码就不详细说明了,将其运行输出结果复制到Str顶部的文档注释就可以。

<?php

class Stubs
{
    private static array $stubs;

    private static function toString(mixed $value)
    {
        if ($value === false) {
            return 'false';
        } else if ($value === true) {
            return 'true';
        } else if ($value === null) {
            return 'null';
        } else if (is_array($value) || is_object($value) || is_string($value)) {
            return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } else {
            return $value;
        }
    }

    /**
     * @param $type
     * @return void
     * @throws ReflectionException
     */
    public static function gen($type): void
    {
        $ret = [];
        foreach (get_defined_functions() as $functionType => $functions) {
            foreach ($functions as $function) {
                if (!str_contains($function, '\\')) { // 忽略一些包含命名空间的函数
                    $refFunc = new ReflectionFunction($function);
                    $params = $refFunc->getParameters();
                    $retType = (string)$refFunc->getReturnType();

                    // 只有参数或返回值匹配包装类型的函数才返回
                    $typePattern = "~$type~"; // 匹配类型的正则,如 "string|int"、"string","int|string"、"?string"
//                    $matchRetType = !empty(preg_match($typePattern, $retType));
                    $matchParamType = false;
                    foreach ($params as $item) {
                        $paramType = (string)$item->getType();
                        if (preg_match($typePattern, $paramType)) {
                            $matchParamType = true;
                        }
                    }
                    if ($matchParamType) {
                        $ret[] = [
                            'func' => $function,
                            'retType' => $retType,
                            'params' => $params,
                        ];
                    }
                }
            }
        }

        self::$stubs = $ret;
    }

    /**
     * @return void
     */
    public static function print(): void
    {
        foreach (self::$stubs as $stub) {
            $func = $stub['func'];
            $retType = $stub['retType'];
            $paramsArray = [];
            /**
             * @var ReflectionParameter $item
             */
            foreach ($stub['params'] as $item) {
                $type = (string)$item->getType();
                $name = $item->getName();
                $prefixName = '$';
                $value = '';
                if ($item->isPassedByReference()) {
                    $prefixName = '&' . $prefixName;
                }
                if ($item->isOptional() && $item->isDefaultValueAvailable()) {
                    $value = '= ' . self::toString($item->getDefaultValue());
                }

                $paramsArray[] = trim(implode(' ', [$type, $prefixName . $name, $value]));

                // debug
//                if ($func == '') {
//                    var_dump([$type, $prefixName. $name, $value], implode(' ', [$type, $prefixName. $name, $value]));
//                }
            }

            $params = implode(',', $paramsArray);
            printf("@method self %s(%s): %s\n",
                $func,
                $params,
                $retType
            );
        }
    }
}

try {
    Stubs::gen('string');
    Stubs::print();
} catch (Throwable $e) {
    echo $e->getMessage(), "\n";
}

可能会由于文档注释过多,PhpStorm会比较卡(解决方法就是不直接打开使用封装类,而是在另一个文件引入封装类文件),vs code则没那么卡,提示效果如下。

res

注意的是,这样做虽然写起来很方便,但性能会比直接调用原生函数差。

本文参考代码:php-pipe-operator

Search

    Table of Contents