来源: https://zhuanlan.zhihu.com/p/61595992
一直在想 PHP 有类的自动载入,为啥子没有函数的自动载入呢?
https://wiki.php.net/rfc/function_autoloading
https://stackoverflow.com/questions/4737199/autoloader-for-functions
总得来说就几种方案,其中 rfc 已经被废。
"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';
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);
}
}
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(); 这样的怪异用法呢,答案是肯定的。
函数实现的原型参考
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
1
DavidNineRoc 2019-04-09 16:17:02 +08:00
* 研究确实是好事,不过我发现呀. 没人会把方法分模块的. 一股脑放在一个文件了.
* 我觉得呀, 等你调用函数不存在再触发,这些都够引入函数了. |
2
doyouhaobaby OP @DavidNineRoc 是这样子的,我发现公司 TP3.2 的项目有一个巨大函数库大约有 4500 多行。很多函数就 1,2 个地方在用,存在滥用。我在研究将这一个超大的函数库拆分了。
并且在项目开发过重拆分的 composer,很多时候想对增加一些辅助方法来从 IOC 容器读取服务,其实也是为了更好地管理辅助函数。 我在框架设计中正在试用这一套规则,发现让代码干净了不少,有不错的实用价值。 https://github.com/hunzhiwange/framework/tree/master/src/Leevel/Leevel/Helper |
3
Junjunya 2019-04-11 18:40:19 +08:00 1
研究是好事, 但是觉得没啥用
|
4
doyouhaobaby OP @Junjunya 我目前主要用于框架助手函数和静态函数库代码解耦
https://github.com/hunzhiwange/framework/commit/d9fd07755602a97838d63c655d834afd406d47df |