199 lines
5.4 KiB
PHP
199 lines
5.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Kiri\Router\Defer;
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Stmt;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\Name;
|
|
use PhpParser\PrettyPrinter\Standard;
|
|
use ReflectionClass;
|
|
use ReflectionMethod;
|
|
|
|
class DeferProxyGenerator
|
|
{
|
|
|
|
private static ?string $cacheDir = null;
|
|
|
|
|
|
/**
|
|
* @throws \ReflectionException
|
|
*/
|
|
public static function create(string $className, array $construct): object
|
|
{
|
|
$proxyClass = $className . '__DeferProxy';
|
|
$cacheFile = self::getCacheFile($className);
|
|
|
|
if (!class_exists($proxyClass, false)) {
|
|
if ($cacheFile !== null && file_exists($cacheFile)) {
|
|
require_once $cacheFile;
|
|
}
|
|
|
|
if (!class_exists($proxyClass, false)) {
|
|
$code = self::generate($className);
|
|
if ($cacheFile !== null) {
|
|
$dir = dirname($cacheFile);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($cacheFile, '<?php ' . $code);
|
|
require_once $cacheFile;
|
|
} else {
|
|
eval($code);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (class_exists($proxyClass, false)) {
|
|
$reflect = new ReflectionClass($proxyClass);
|
|
return $reflect->newInstanceArgs($construct);
|
|
}
|
|
|
|
$reflect = new ReflectionClass($className);
|
|
return $reflect->newInstanceArgs($construct);
|
|
}
|
|
|
|
|
|
/**
|
|
* @throws \ReflectionException
|
|
*/
|
|
private static function generate(string $className): string
|
|
{
|
|
$reflect = new ReflectionClass($className);
|
|
$methods = DeferRegistry::getAll($className);
|
|
$stmts = [];
|
|
|
|
foreach ($methods as $methodName => $defers) {
|
|
if (!$reflect->hasMethod($methodName)) continue;
|
|
$method = $reflect->getMethod($methodName);
|
|
if ($method->isPrivate() || $method->isStatic() || $method->isFinal() || $method->isConstructor() || $method->isDestructor()) continue;
|
|
$stmts[] = self::buildMethod($method);
|
|
}
|
|
|
|
if (empty($stmts)) return '';
|
|
|
|
$classNode = new Stmt\Class_(
|
|
new Name($className . '__DeferProxy'),
|
|
['extends' => new Name\FullyQualified($className), 'stmts' => $stmts]
|
|
);
|
|
|
|
$namespace = $reflect->getNamespaceName();
|
|
$namespaceNode = new Stmt\Namespace_(
|
|
$namespace !== '' ? new Name($namespace) : null,
|
|
[$classNode]
|
|
);
|
|
|
|
$printer = new Standard();
|
|
return $printer->prettyPrintFile([$namespaceNode]);
|
|
}
|
|
|
|
|
|
private static function buildMethod(ReflectionMethod $method): Stmt\ClassMethod
|
|
{
|
|
$methodName = $method->getName();
|
|
$params = [];
|
|
$args = [];
|
|
|
|
foreach ($method->getParameters() as $param) {
|
|
$type = null;
|
|
$refType = $param->getType();
|
|
if ($refType instanceof \ReflectionNamedType) {
|
|
$type = new Name($refType->getName());
|
|
}
|
|
|
|
$default = null;
|
|
if ($param->isDefaultValueAvailable()) {
|
|
$default = self::buildDefaultValue($param);
|
|
}
|
|
|
|
$var = new Expr\Variable($param->getName());
|
|
|
|
$params[] = new Node\Param($var, $default, $type,
|
|
byRef: $param->isPassedByReference(),
|
|
variadic: $param->isVariadic()
|
|
);
|
|
|
|
$args[] = new Node\Arg($var,
|
|
byRef: $param->isPassedByReference(),
|
|
unpack: $param->isVariadic()
|
|
);
|
|
}
|
|
|
|
$returnType = null;
|
|
$refReturnType = $method->getReturnType();
|
|
if ($refReturnType instanceof \ReflectionNamedType) {
|
|
$returnType = new Name($refReturnType->getName());
|
|
}
|
|
|
|
$stmts = [
|
|
new Stmt\Expression(new Expr\Assign(new Expr\Variable('result'),
|
|
new Expr\StaticCall(new Name('parent'), $methodName, $args))),
|
|
new Stmt\Expression(
|
|
new Expr\StaticCall(
|
|
new Name\FullyQualified(DeferRegistry::class), 'execute', [
|
|
new Node\Arg(new Expr\ClassConstFetch(new Name\FullyQualified($method->getDeclaringClass()->getName()), 'class')),
|
|
new Node\Arg(new Node\Scalar\String_($methodName)),
|
|
]
|
|
)
|
|
),
|
|
new Stmt\Return_(new Expr\Variable('result')),
|
|
];
|
|
|
|
return new Stmt\ClassMethod($methodName, [
|
|
'flags' => $method->isPublic() ? Stmt\Class_::MODIFIER_PUBLIC : Stmt\Class_::MODIFIER_PROTECTED,
|
|
'params' => $params,
|
|
'returnType' => $returnType,
|
|
'stmts' => $stmts,
|
|
]);
|
|
}
|
|
|
|
|
|
private static function buildDefaultValue(\ReflectionParameter $param): Node\Expr
|
|
{
|
|
if (!$param->isDefaultValueAvailable()) {
|
|
return new Expr\ConstFetch(new Name('null'));
|
|
}
|
|
$value = $param->getDefaultValue();
|
|
return match (true) {
|
|
is_bool($value) => new Expr\ConstFetch(new Name($value ? 'true' : 'false')),
|
|
is_int($value) => new Node\Scalar\LNumber($value),
|
|
is_float($value) => new Node\Scalar\DNumber($value),
|
|
is_string($value) => new Node\Scalar\String_($value),
|
|
is_array($value) => new Expr\Array_(array_map(
|
|
fn($k, $v) => new Expr\ArrayItem(
|
|
self::buildDefaultValueFromScalar($v),
|
|
is_string($k) ? new Node\Scalar\String_($k) : null
|
|
), array_keys($value), $value
|
|
)),
|
|
default => new Expr\ConstFetch(new Name('null')),
|
|
};
|
|
}
|
|
|
|
|
|
private static function buildDefaultValueFromScalar(mixed $value): Node\Expr
|
|
{
|
|
return match (true) {
|
|
is_bool($value) => new Expr\ConstFetch(new Name($value ? 'true' : 'false')),
|
|
is_int($value) => new Node\Scalar\LNumber($value),
|
|
is_float($value) => new Node\Scalar\DNumber($value),
|
|
is_string($value) => new Node\Scalar\String_($value),
|
|
default => new Expr\ConstFetch(new Name('null')),
|
|
};
|
|
}
|
|
|
|
|
|
private static function getCacheFile(string $className): ?string
|
|
{
|
|
if (self::$cacheDir === null) {
|
|
if (defined('APP_PATH')) {
|
|
self::$cacheDir = APP_PATH . 'runtime/proxies/';
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return self::$cacheDir . str_replace('\\', '_', $className) . '__DeferProxy.php';
|
|
}
|
|
|
|
}
|