PHP 基於類的自動載入實現的函數惰性載入
一直在想 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 HelloWorld;
class Foo
{
public static function hello(): string
{
return world;
}
}
其實本質上還是方法,當然還是類的自動載入。
還有一個它的變種,類當函數。
namespace HelloWorld;
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 MyNamespacea;
use function MyNamespacea;
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 MyNamespaceFoohello_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 LeevelSupport;
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 LeevelSupportFn;
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
# 虛擬類為 MyNamespaceFoosingle_func
namespace MyNamespaceFoo;
function single_func()
{
}
使用方法
fn(\MyNamespace\Foo\single_func);
第二優先順序分組模塊化:
# /data/codes/php/MyNamespace/Foo/prefix.php
# 虛擬類為 MyNamespaceFooprefix
namespace MyNamespaceFoo;
function prefix_a()
{}
function prefix_b_c_d()
{}
使用方法
fn(\MyNamespace\Foo\prefix_a);
第三優先順序,index 導入
# /data/codes/php/MyNamespace/Foo/index.php
# 虛擬類為 MyNamespaceFooindex
namespace MyNamespaceFoo;
function hello()
{}
function world()
{}
使用方法
fn(\MyNamespace\Foo\world);
https://github.com/hunzhiwange/framework/tree/master/src/Leevel/Leevel/Helper
https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Support/Fn.php
注意:分組和 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 MyNamespaceFoo;
/**
* 使用方法
*
* ```
* 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{}
推薦閱讀: