V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
doyouhaobaby
V2EX  ›  PHP

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

  •  
  •   doyouhaobaby · 2019-04-09 12:06:38 +08:00 · 3232 次点击
    这是一个创建于 2089 天前的主题,其中的信息可能已经有所发展或是发生改变。

    来源: https://zhuanlan.zhihu.com/p/61595992

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

    PHP: 类的自动加载 - Manual

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

    https://stackoverflow.com/questions/4737199/autoloader-for-functions

    总得来说就几种方案,其中 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 <[email protected]>
     *
     * @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{}
    

    https://github.com/hunzhiwange/framework/tree/master/src/Leevel/Leevel/Helper https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Support/Fn.php

    测试用例请稍后访问,已写好,整理中,大家可以用在项目中,不错

    https://github.com/hunzhiwange/framework/blob/master/tests/Support/FnTest.php

    4 条回复    2019-04-11 21:33:45 +08:00
    DavidNineRoc
        1
    DavidNineRoc  
       2019-04-09 16:17:02 +08:00
    * 研究确实是好事,不过我发现呀. 没人会把方法分模块的. 一股脑放在一个文件了.
    * 我觉得呀, 等你调用函数不存在再触发,这些都够引入函数了.
    doyouhaobaby
        2
    doyouhaobaby  
    OP
       2019-04-09 16:31:34 +08:00
    @DavidNineRoc 是这样子的,我发现公司 TP3.2 的项目有一个巨大函数库大约有 4500 多行。很多函数就 1,2 个地方在用,存在滥用。我在研究将这一个超大的函数库拆分了。

    并且在项目开发过重拆分的 composer,很多时候想对增加一些辅助方法来从 IOC 容器读取服务,其实也是为了更好地管理辅助函数。

    我在框架设计中正在试用这一套规则,发现让代码干净了不少,有不错的实用价值。

    https://github.com/hunzhiwange/framework/tree/master/src/Leevel/Leevel/Helper
    Junjunya
        3
    Junjunya  
       2019-04-11 18:40:19 +08:00   ❤️ 1
    研究是好事, 但是觉得没啥用
    doyouhaobaby
        4
    doyouhaobaby  
    OP
       2019-04-11 21:33:45 +08:00
    @Junjunya 我目前主要用于框架助手函数和静态函数库代码解耦
    https://github.com/hunzhiwange/framework/commit/d9fd07755602a97838d63c655d834afd406d47df
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   940 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 21:54 · PVG 05:54 · LAX 13:54 · JFK 16:54
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.