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

2019-04-09 12:06:38 +08:00
 doyouhaobaby

来源: 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 <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{}

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

3203 次点击
所在节点    PHP
4 条回复
DavidNineRoc
2019-04-09 16:17:02 +08:00
* 研究确实是好事,不过我发现呀. 没人会把方法分模块的. 一股脑放在一个文件了.
* 我觉得呀, 等你调用函数不存在再触发,这些都够引入函数了.
doyouhaobaby
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
2019-04-11 18:40:19 +08:00
研究是好事, 但是觉得没啥用
doyouhaobaby
2019-04-11 21:33:45 +08:00
@Junjunya 我目前主要用于框架助手函数和静态函数库代码解耦
https://github.com/hunzhiwange/framework/commit/d9fd07755602a97838d63c655d834afd406d47df

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/553303

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX