PHP 基于类的自动加载实现的函数惰性加载

一直在想 PHP 有类的自动载入,为啥子没有函数的自动载入呢?

PHP: 类的自动加载 - Manual

https://wiki.php.net/rfc/function_autoload...

https://stackoverflow.com/questions/473719...

总得来说就几种方案,其中 rfc 已经被废。

方案1:Composer files

"autoload": {
    "files": [
        "common/Infra/functions.php"
    ]
 }

用 composer 动不动就几十个助手函数,90% 以上对我们的多少来说 API 来说都是一种加载负担。

<?php

// autoload_files.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
    'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php',
    '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
    '72579e7bd17821bb1321b87411366eae' => $vendorDir . '/illuminate/support/helpers.php',
    '6b06ce8ccf69c43a60a1e48495a034c9' => $vendorDir . '/react/promise-timer/src/functions.php',
    '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
    '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
    '2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
    'ebf8799635f67b5d7248946fe2154f4a' => $vendorDir . '/ringcentral/psr7/src/functions_include.php',
    'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
    'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
    'cea474b4340aa9fa53661e887a21a316' => $vendorDir . '/react/promise-stream/src/functions_include.php',
    '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
    '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
    'cf97c57bfe0f23854afd2f3818abb7a0' => $vendorDir . '/zendframework/zend-diactoros/src/functions/create_uploaded_file.php',
    '9bf37a3d0dad93e29cb4e1b1bfab04e9' => $vendorDir . '/zendframework/zend-diactoros/src/functions/marshal_headers_from_sapi.php',
    'ce70dccb4bcc2efc6e94d2ee526e6972' => $vendorDir . '/zendframework/zend-diactoros/src/functions/marshal_method_from_sapi.php',
    'f86420df471f14d568bfcb71e271b523' => $vendorDir . '/zendframework/zend-diactoros/src/functions/marshal_protocol_version_from_sapi.php',
    'b87481e008a3700344428ae089e7f9e5' => $vendorDir . '/zendframework/zend-diactoros/src/functions/marshal_uri_from_sapi.php',
    '0b0974a5566a1077e4f2e111341112c1' => $vendorDir . '/zendframework/zend-diactoros/src/functions/normalize_server.php',
    '1ca3bc274755662169f9629d5412a1da' => $vendorDir . '/zendframework/zend-diactoros/src/functions/normalize_uploaded_files.php',
    '40360c0b9b437e69bcbb7f1349ce029e' => $vendorDir . '/zendframework/zend-diactoros/src/functions/parse_cookie_header.php',
    '4a1f389d6ce373bda9e57857d3b61c84' => $vendorDir . '/barryvdh/laravel-debugbar/src/helpers.php',
    '6506d72cb66769ba612eb2800e4b0b6e' => $vendorDir . '/hunzhiwange/framework/src/Leevel/Leevel/functions.php',
    '05a007f8491620f2bc6b891fc6e46c02' => $vendorDir . '/php-pm/php-pm/src/functions.php',
    '0ccdf99b8f62f02c52cba55802e0c2e7' => $vendorDir . '/zircote/swagger-php/src/functions.php',
    '629bcf4896f1b026f50c8c0a44b87e34' => $baseDir . '/common/Infra/functions.php',
);

曾经为这些助手函数很烦恼,因为他们都不是惰性加载,并且去掉了他们。

$files = include __DIR__.'/vendor/composer/autoload_files.php';

/**
 * Ignore the helper functions.
 * Because most of them are useless.
 */
foreach ($files as $fileIdentifier => $_) {
    $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}

require_once __DIR__.'/vendor/autoload.php';

方案2:类方法

namespace Hello\World;

class Foo
{
    public static function hello(): string
    {
        return 'world';
    }
}

其实本质上还是方法,当然还是类的自动加载。

还有一个它的变种,类当函数。

namespace Hello\World;

class Foo
{
    public function __invoke(): string
    {
        return 'world';
    }
}

还有变种

namespace MyNamespace;

class Fn {

    private function __construct() {}
    private function __wakeup() {}
    private function __clone() {}

    public static function __callStatic($fn, $args) {
        if (!function_exists($fn)) {
            $fn = "YOUR_FUNCTIONS_NAMESPACE\\$fn";
            require str_replace('\\', '/', $fn) . '.php';
        }
        return call_user_func_array($fn, $args);
    }

}

方案3:利用类自动导入来实现函数代码

namespace MyNamespace;

class a
{
}

function a()
{
}

function b()
{
}

你可以

use MyNamespace\a;
use function MyNamespace\a;

new a(); // 或者 class_exits(a::class);
a();

上面的实现是否有可以改进的地方呢。比如去掉 class a 的定义,不用 new a(); 这样的怪异用法呢,答案是肯定的。

方案4: 基于虚拟类的自动导入实现的惰性函数加载方案

函数实现的原型参考

return call_user_func('\\MyNamespace\\Foo\\hello_world', 1, 2);

实现如下

return fn('\\MyNamespace\\Foo\\hello_world', 1, 2);

第二种用法

use function MyNamespace\Foo\hello_world;

return fn(function() {
   return hello_world(1, 2);
});

第三种用法

return fn(function($a, $b) {
   return hell0_world($a, $b);
}, 1, 2);

我们定义一个类

<?php

declare(strict_types=1);

namespace Leevel\Support;

use Closure;
use Error;

/**
 * 函数自动导入.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2019.04.05
 *
 * @version 1.0
 */
class Fn
{
    /**
     * 自动导入函数.
     *
     * @param \Closure|string $fn
     * @param array           $args
     *
     * @return mixed
     */
    public function __invoke($fn, ...$args)
    {
        $this->validate($fn);

        try {
            return $fn(...$args);
        } catch (Error $th) {
            $fnName = $this->normalizeFn($fn, $th);

            if ($this->match($fnName)) {
                return $fn(...$args);
            }

            throw $th;
        }
    }

    /**
     * 匹配函数.
     *
     * @param string $fn
     *
     * @return bool
     */
    protected function match(string $fn): bool
    {
        foreach (['Fn', 'Prefix', 'Index'] as $type) {
            if ($this->{'match'.$type}($fn)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 校验类型.
     *
     * @param \Closure|string $fn
     */
    protected function validate($fn): void
    {
        if (!is_string($fn) && !($fn instanceof Closure)) {
            $e = sprintf('Fn first args must be Closure or string.');

            throw new Error($e);
        }
    }

    /**
     * 整理函数名字.
     *
     * @param \Closure|string $fn
     * @param \Error $th
     *
     * @return string
     */
    protected function normalizeFn($fn, Error $th): string
    {
        $message = $th->getMessage();
        $undefinedFn = 'Call to undefined function ';

        if (0 !== strpos($message, $undefinedFn)) {
            throw $th;
        }

        if (is_string($fn)) {
            return $fn;
        }

        return substr($message, strlen($undefinedFn), -2);
    }

    /**
     * 匹配一个函数一个文件.
     *
     * @param string $fn
     * @param string $virtualClass
     *
     * @return bool
     */
    protected function matchFn(string $fn, string $virtualClass = ''): bool
    {
        if (!$virtualClass) {
            $virtualClass = $fn;
        }

        class_exists($virtualClass);

        return function_exists($fn);
    }

    /**
     * 匹配前缀分隔一组函数.
     *
     * @param string $fn
     *
     * @return bool
     */
    protected function matchPrefix(string $fn): bool
    {
        if (false === strpos($fn, '_')) {
            return false;
        }

        $fnPrefix = substr($fn, 0, strpos($fn, '_'));

        return $this->matchFn($fn, $fnPrefix);
    }

    /**
     * 匹配基于 index 索引.
     *
     * @param string $fn
     *
     * @return bool
     */
    protected function matchIndex(string $fn): bool
    {
        if (false === strpos($fn, '\\')) {
            return false;
        }

        $fnIndex = substr($fn, 0, strripos($fn, '\\')).'\\index';

        return $this->matchFn($fn, $fnIndex);
    }

定义一个助手函数

use Leevel\Support\Fn;

if (!function_exists('fn')) {
    /**
     * 自动导入函数.
     *
     * @param \Closure|string $call
     * @param array           $args
     * @param mixed           $fn
     *
     * @return mixed
     */
    function fn($fn, ...$args)
    {
        return (new Fn())($fn, ...$args);
    }
}

实现原理如下,我们可以通过 try catch 捕捉到一个函数不存在的错误,利用函数所在命名空间的虚拟类,通过判断虚拟类 class exits 来导入一个类,触发 composer PSR 4 规则来访问路径

第一优先级,一个文件一个函数

# /data/codes/php/MyNamespace/Foo/single_func.php
# 虚拟类为 MyNamespace\Foo\single_func

namespace MyNamespace\Foo;

function single_func()
{
}

使用方法

fn('\\MyNamespace\\Foo\\single_func');

第二优先级分组模块化:

# /data/codes/php/MyNamespace/Foo/prefix.php
# 虚拟类为 MyNamespace\Foo\prefix

namespace MyNamespace\Foo;

function prefix_a()
{}

function prefix_b_c_d()
{}

使用方法

fn('\\MyNamespace\\Foo\\prefix_a');

第三优先级,index 导入

# /data/codes/php/MyNamespace/Foo/index.php
# 虚拟类为 MyNamespace\Foo\index

namespace MyNamespace\Foo;

function hello()
{}

function world()
{}

使用方法

fn('\\MyNamespace\\Foo\\world');

通过这种方式,我们可以实现函数的惰性加载,当然方法都差不多。目前用这个类来做函数拆分。

注意:分组和 index 索引还是得显示定义虚拟类防止函数不存在时的 class_exits 重复载入。
因为 composer 使用的是 include 会出现重复载入的问题。

vendor/composer/ClassLoader.php

/**
 * Scope isolated include.
 *
 * Prevents access to $this/self from included files.
 */
function includeFile($file)
{
    include $file;
}

例如:

<?php

declare(strict_types=1);

namespace MyNamespace\Foo;

/**
 * 使用方法
 * 
 * ```
 * echo fn('\\MyNamespace\\Foo\\foo_bar');
 * ```
 *
 * @param string $extend
 * @return string
 */
function foo_bar(string $extend = ''): string
{
    return 'foo bar'.$extend;
}

/**
 * Prevent duplicate loading.
 */
class index{}
xiaoniuge
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!