一直在想 PHP 有類的自動載入,為啥子沒有函數的自動載入呢?

PHP: 類的自動載入 - Manual

wiki.php.net/rfc/functi

stackoverflow.com/quest

總得來說就幾種方案,其中 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);

通過這種方式,我們可以實現函數的惰性載入,當然方法都差不多。目前用這個類來做函數拆分。

github.com/hunzhiwange/

github.com/hunzhiwange/

注意:分組和 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{}

推薦閱讀:

相關文章