feat(extension): implement ExtensionProvider attribute for universal method extensions

- Add ExtensionProvider attribute class to define extension targets
- Convert function-based extensions to class-based ExtensionProvider pattern
- Support various target types including native_types, complex_types, and class names
- Parse ExtensionProvider attributes during preprocessing phase
- Generate proper extension method calls using provider classes
- Update test cases to use ExtensionProvider syntax instead of function prefixes
- Remove old extension function name patterns and related logic
- Add validation for extension provider method signatures and parameters
- Support both snake_case and camel
pull/17/head
韩天峰 1 month ago
parent 33f6c67e13
commit 431817dee3
  1. 1
      src/Entity/ClassDef.php
  2. 179
      src/Parser/UniversalMethodCall.php
  3. 66
      src/Preprocessor.php
  4. 1
      src/Translator.php
  5. 4
      src/gen_stub.php
  6. 8
      src/polyfills.php
  7. 11
      tests/compiler/keyword_extension/001.phpt
  8. 10
      tests/compiler/keyword_extension/camel.phpt
  9. 2
      tests/compiler/static/static-prop-expr.phpt
  10. 32
      tests/compiler/stream_method/extension.phpt
  11. 39
      tests/compiler/universal_method/object_extension.phpt
  12. 36
      tests/compiler/universal_method/object_extension_exact_name.phpt
  13. 26
      tests/compiler/universal_method/universal_method_extension.phpt
  14. 18
      tests/compiler/universal_method/universal_method_extension_camel.phpt
  15. 49
      tests/compiler/universal_method/universal_method_extension_chain.phpt
  16. 11
      tests/compiler/universal_method/universal_method_internal.phpt

@ -32,6 +32,7 @@ class ClassDef extends ClassLikeDef
public string $extends = '';
public bool $requireCtor = false;
public bool $enum = false;
public ?string $extensionProviderTarget = null;
/**
* Backing type for backed enums ('int' or 'string'), null for pure enums.

@ -13,6 +13,7 @@ use PhpParser\NodeAbstract;
trait UniversalMethodCall
{
private ?array $extensionProviderMethods = null;
protected const array UNIVERSAL_METHODS = [
Type::INT => [
'add' => ['handler' => 'calc_op', 'op' => '+', 'return_type' => Type::INT, 'min_args' => 1, 'max_args' => 1],
@ -337,35 +338,69 @@ trait UniversalMethodCall
return $ext ? $ext['return_type'] : null;
}
protected const array TYPE_EXTENSION_PREFIX = [
Type::INT => 'int',
Type::FLOAT => 'float',
Type::BOOL => 'bool',
Type::STR => 'str',
Type::ARRAY => 'array',
Type::STREAM => 'stream',
Type::BIGINT => 'bigint',
Type::DECIMAL => 'decimal',
Type::BIGFLOAT => 'bigfloat',
Type::BOX => 'box',
];
protected function extensionFunctionName(string $prefix, string $method): string
private function getExtensionProviderMethods(): array
{
return $prefix . '_' . $method;
if ($this->extensionProviderMethods !== null) {
return $this->extensionProviderMethods;
}
$registry = [];
foreach ($this->symbols->classes() as $provider) {
$target = $provider->extensionProviderTarget;
if ($target === null) {
continue;
}
foreach ($provider->methods as $method) {
if (str_starts_with($method->name, '__')) {
continue;
}
if (!($method->flags & \PhpParser\Modifiers::PUBLIC)) {
continue;
}
if (!($method->flags & \PhpParser\Modifiers::STATIC)) {
$this->error("Extension provider method {$provider->getNamespacedName(false)}::{$method->name}() must be static");
}
$function = $method->functionDef;
if ($function === null || empty($function->argInfoList)) {
$this->error("Extension provider method {$provider->getNamespacedName(false)}::{$method->name}() must declare a receiver parameter");
}
$receiver = $function->argInfoList[0];
if ($receiver->byRef || !$this->extensionReceiverMatchesTarget($receiver, $target)) {
$this->error("Invalid receiver parameter for extension provider method {$provider->getNamespacedName(false)}::{$method->name}()");
}
$targetKey = strtolower(ltrim($target, '\\'));
$methodKey = strtolower($method->name);
if (isset($registry[$targetKey][$methodKey])) {
$this->error("Duplicate extension method {$target}::{$method->name}()");
}
$registry[$targetKey][$methodKey] = [
'handler' => 'provider_extension',
'fn' => $this->getNativeName($method->name, $provider->namespace, $provider->name),
'class' => $provider->getNamespacedName(false),
'return_type' => $function->returnType,
'min_args' => max(0, $function->argCountRequired - 1),
'max_args' => $function->hasVariadicArg() ? -1 : count($function->argInfoList) - 1,
];
}
}
return $this->extensionProviderMethods = $registry;
}
protected function findUserExtensionFunction(string $prefix, string $method, string $namespace = ''): ?array
private function extensionReceiverMatchesTarget($receiver, string $target): bool
{
$function = $this->getNativeName($this->extensionFunctionName($prefix, $method), $namespace);
if (!$this->hasFunction($function)) {
return null;
}
$definition = $this->getFunction($function);
if ($definition->namespace !== $namespace) {
return null;
$builtinTargets = [
Type::VAR, Type::INT, Type::FLOAT, Type::BOOL, Type::STR,
Type::ARRAY, Type::OBJECT, Type::STREAM, Type::BIGINT,
Type::BIGFLOAT, Type::DECIMAL, Type::BOX,
];
if (!in_array($target, $builtinTargets, true)) {
return $receiver->type === Type::OBJECT && $this->isSameClassName($receiver->declaredClass, $target);
}
return ['name' => $function, 'definition' => $definition];
return $receiver->type === $target;
}
private function findProviderExtension(string $target, string $method): ?array
{
return $this->getExtensionProviderMethods()[strtolower(ltrim($target, '\\'))][strtolower($method)] ?? null;
}
protected const array TO_CONVERT_FN = [
@ -421,34 +456,7 @@ trait UniversalMethodCall
if ($class === '' || !$this->hasClass($class)) {
return null;
}
$separator = strrpos($class, '\\');
$namespace = $separator === false ? '' : substr($class, 0, $separator);
$shortClass = $separator === false ? $class : substr($class, $separator + 1);
$extension = $this->findUserExtensionFunction($shortClass, $method, $namespace);
if ($extension === null) {
return null;
}
$function = $extension['name'];
$funcDef = $extension['definition'];
if (empty($funcDef->argInfoList)) {
return null;
}
$receiver = $funcDef->argInfoList[0];
if ($receiver->byRef
|| $receiver->type !== Type::OBJECT
|| !$this->isSameClassName($receiver->declaredClass, $class)) {
return null;
}
$totalParams = count($funcDef->argInfoList);
return [
'handler' => 'object_extension_fn',
'fn' => $function,
'return_type' => $funcDef->returnType,
'min_args' => max(0, $funcDef->argCountRequired - 1),
'max_args' => $funcDef->hasVariadicArg() ? -1 : $totalParams - 1,
];
return $this->findProviderExtension($class, $method);
}
/**
@ -461,36 +469,7 @@ trait UniversalMethodCall
*/
protected function findExtensionMethod(string $type, string $method): ?array
{
$prefix = self::TYPE_EXTENSION_PREFIX[$type] ?? null;
if ($prefix === null) {
return null;
}
$funcName = $this->extensionFunctionName($prefix, $method);
$resolvedName = $this->resolveExtensionFunctionName($funcName);
if ($resolvedName !== null) {
$funcDef = $this->getFunction($resolvedName);
if ($funcDef->namespace !== '' || !$this->validateExtensionFirstParam($type, $funcDef)) {
return null;
}
return [
'handler' => 'php_fn',
'fn' => $resolvedName,
'receiver_pos' => 1,
'return_type' => $funcDef->returnType,
'min_args' => 0,
'max_args' => -1,
];
}
if ($this->isInternalFunction($funcName)) {
$internal = $this->buildInternalExtensionMethod($type, $funcName);
if ($internal !== null) {
return $internal;
}
}
return null;
return $this->findProviderExtension($type, $method);
}
/**
@ -499,32 +478,7 @@ trait UniversalMethodCall
*/
protected function findKeywordExtensionMethod(string $method): ?array
{
$extension = $this->findUserExtensionFunction('_', $method);
if ($extension === null) {
return null;
}
$funcName = $extension['name'];
$funcDef = $extension['definition'];
if (empty($funcDef->argInfoList)) {
return null;
}
$firstParam = $funcDef->argInfoList[0];
if ($firstParam->type !== Type::VAR) {
return null;
}
$totalParams = count($funcDef->argInfoList);
$minArgs = max(0, $funcDef->argCountRequired - 1);
$maxArgs = $funcDef->hasVariadicArg() ? -1 : $totalParams - 1;
return [
'handler' => 'php_fn',
'fn' => $funcName,
'receiver_pos' => 1,
'return_type' => $funcDef->returnType,
'min_args' => $minArgs,
'max_args' => $maxArgs,
];
return $this->findProviderExtension(Type::VAR, $method);
}
/**
@ -651,15 +605,17 @@ trait UniversalMethodCall
'php_fn' => $this->genUniversalPhpFn($receiver, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0, $def['const_args'] ?? []),
'php_fn_ref' => $this->genUniversalPhpFnRef($receiver, $def['fn'], $expr->args, $def['return_type']),
'cpp_fn' => $this->genUniversalCppFn($receiver, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0),
'object_extension_fn' => $this->genObjectExtensionFn($receiver, $def['fn'], $expr->args),
'provider_extension' => $this->genProviderExtensionCall($receiver, $def, $expr->args),
default => null,
};
}
protected function genObjectExtensionFn(string $receiver, string $nativeFunc, array $args): string
protected function genProviderExtensionCall(string $receiver, array $definition, array $args): string
{
$nativeFunc = $definition['fn'];
$tail = $this->parseNativeCallArgs($args, $nativeFunc, 1);
return self::PREFIX . $nativeFunc . '(' . $receiver . ($tail === '' ? '' : ', ' . $tail) . ')';
return self::PREFIX . $nativeFunc . '(' . $this->getCeWrapper($definition['class']) . ', '
. $receiver . ($tail === '' ? '' : ', ' . $tail) . ')';
}
/**
@ -678,6 +634,7 @@ trait UniversalMethodCall
'php_fn' => $this->genUniversalPhpFn('php::toStream(' . $streamVar . ')', $def['fn'], $expr->args, $def['receiver_pos'] ?? 0, $def['const_args'] ?? []),
'direct_method' => $this->genUniversalDirectMethod('php::toStream(' . $streamVar . ')', $def['method'], $expr->args, $def['int_cast_args'] ?? []),
'cpp_fn' => $this->genUniversalCppFn('php::toStream(' . $streamVar . ')', $def['fn'], $expr->args, $def['receiver_pos'] ?? 0),
'provider_extension' => $this->genProviderExtensionCall('php::toStream(' . $streamVar . ')', $def, $expr->args),
default => null,
};

@ -524,6 +524,7 @@ class Preprocessor extends CompilerBase
}
$this->classDef = new ClassDef($this->class, $flags, $this->namespace);
$this->classDef->extensionProviderTarget = $this->parseExtensionProviderTarget($class);
$this->addClass($fullClassName, $this->classDef);
if (!empty($class->extends)) {
@ -602,6 +603,71 @@ class Preprocessor extends CompilerBase
return $code;
}
protected function parseExtensionProviderTarget(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): ?string
{
foreach ($class->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) {
$parts = $attribute->name->getParts();
if (strtolower((string) end($parts)) !== 'extensionprovider') {
continue;
}
if (!$class instanceof Node\Stmt\Class_) {
$this->fatalError($class, 'ExtensionProvider can only be applied to classes');
}
if (count($attribute->args) !== 1) {
$this->fatalError($attribute, 'ExtensionProvider expects exactly one target');
}
$target = $this->parseExtensionProviderTargetValue($attribute->args[0]->value, $attribute);
unset($group->attrs[$attributeIndex]);
$group->attrs = array_values($group->attrs);
if (empty($group->attrs)) {
unset($class->attrGroups[$groupIndex]);
$class->attrGroups = array_values($class->attrGroups);
}
return $target;
}
}
return null;
}
private function parseExtensionProviderTargetValue(Node\Expr $value, NodeAbstract $errorNode): string
{
if ($value instanceof Node\Expr\ClassConstFetch
&& $this->isNameExpr($value->class)
&& $this->isIdExpr($value->name)) {
$class = strtolower(ltrim($value->class->toString(), '\\'));
$constant = strtolower($value->name->toString());
if ($constant === 'class') {
return $this->getNamespacedClassName($value->class->toString());
}
$targets = [
'native_types' => [
'type_int' => Type::INT,
'type_float' => Type::FLOAT,
'type_bool' => Type::BOOL,
'type_bigint' => Type::BIGINT,
'type_bigfloat' => Type::BIGFLOAT,
'type_decimal' => Type::DECIMAL,
],
'complex_types' => [
'type_any' => Type::VAR,
'type_var' => Type::VAR,
'type_variant' => Type::VAR,
'type_str' => Type::STR,
'type_string' => Type::STR,
'type_array' => Type::ARRAY,
'type_object' => Type::OBJECT,
'type_stream' => Type::STREAM,
'type_box' => Type::BOX,
],
];
if (isset($targets[$class][$constant])) {
return $targets[$class][$constant];
}
}
$this->fatalError($errorNode, 'ExtensionProvider target must use native_types, complex_types, or ClassName::class');
}
protected function buildLiteralArrayInitPlan(Node\Expr\Array_ $defaultNode): ArrayInitPlan
{
$localVarCount = count($this->context->localVars);

@ -2504,6 +2504,7 @@ CODE;
$this->fatalError($class, "class {$fullName} not found");
}
$this->classDef = $this->getClass($fullName);
$this->parseExtensionProviderTarget($class);
// 如果不是继承自内置类,需要检查父类是否存在,在预处理阶段只需检查了是否继承内置类
// 目前不允许继承自动态加载的自定义类

@ -3549,6 +3549,10 @@ class AttributeInfo {
foreach ($attributeGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
$parts = $attr->name->getParts();
if (strtolower((string) end($parts)) === 'extensionprovider') {
continue;
}
$attributes[] = new AttributeInfo($attr->name->toString(), $attr->args);
}
}

@ -6,6 +6,14 @@
* @contact service@swoole.com
*/
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class ExtensionProvider
{
public function __construct(public string $target)
{
}
}
class native_types
{
public const type_int = 'int';

@ -1,12 +1,17 @@
--TEST--
keyword extension method: exact snake_case name
Keyword ExtensionProvider method with snake_case name
--FILE--
<?php
declare(strict_types=1);
use native_types;
function __var_dump(mixed $var): void {
var_dump($var);
#[ExtensionProvider(complex_types::type_any)]
final class KeywordExtensions
{
public static function var_dump(mixed $var): void
{
var_dump($var);
}
}
function main(): void {

@ -1,14 +1,18 @@
--TEST--
Keyword extension methods support lowerCamelCase function names
Keyword ExtensionProvider method with lowerCamelCase name
--FILE--
<?php
declare(strict_types=1);
use native_types;
function __inspectValue(mixed $value, string $prefix): void
#[ExtensionProvider(complex_types::type_any)]
final class KeywordExtensions
{
echo $prefix, ':', $value, "\n";
public static function inspectValue(mixed $value, string $prefix): void
{
echo $prefix, ':', $value, "\n";
}
}
function main(): void

@ -15,4 +15,4 @@ function main(): void
}
?>
--EXPECTF--
string(%d) "%s/aot/static/../../A"
string(%d) "%s/compiler/static/../../A"

@ -3,24 +3,28 @@ stream extension method support
--FILE--
<?php
function stream_read_chunk($stream, int $size): string
#[ExtensionProvider(complex_types::type_stream)]
final class StreamExtensions
{
return fread($stream, $size);
}
public static function readChunk(stream $stream, int $size): string
{
return fread($stream, $size);
}
function stream_count_lines($stream): int
{
$count = 0;
$pos = ftell($stream);
rewind($stream);
while (!feof($stream)) {
$line = fgets($stream);
if ($line !== false) {
$count++;
public static function countLines(stream $stream): int
{
$count = 0;
$pos = ftell($stream);
rewind($stream);
while (!feof($stream)) {
$line = fgets($stream);
if ($line !== false) {
$count++;
}
}
fseek($stream, $pos);
return $count;
}
fseek($stream, $pos);
return $count;
}
function main()

@ -1,5 +1,5 @@
--TEST--
Namespaced object extension methods use Class_method naming
Namespaced object methods use an ExtensionProvider class
--FILE--
<?php
@ -23,29 +23,28 @@ namespace App {
}
}
function User_testMethod(User $user, string $suffix): string
#[\ExtensionProvider(User::class)]
final class UserExtensions
{
return $user->name . $suffix . ':snake';
}
function User_displayName(User $user): string
{
return strtoupper($user->name) . ':camel';
}
public static function testMethod(User $user, string $suffix): string
{
return $user->name . $suffix . ':snake';
}
function User_format_name(int $invalid): string
{
return 'invalid';
}
public static function displayName(User $user): string
{
return strtoupper($user->name) . ':camel';
}
function User_formatName(User $user): string
{
return '[' . $user->name . ']';
}
public static function formatName(User $user): string
{
return '[' . $user->name . ']';
}
function User_existing(User $user): string
{
return 'extension';
public static function existing(User $user): string
{
return 'extension';
}
}
}

@ -1,5 +1,5 @@
--TEST--
Object extension methods require consistent names and ignore letter case
Object ExtensionProvider methods require consistent names and ignore letter case
--FILE--
<?php
@ -18,29 +18,23 @@ namespace App {
}
}
function UserService_displayName(UserService $service): string
#[\ExtensionProvider(UserService::class)]
final class UserServiceExtensions
{
return 'camel:' . $service->name;
}
function UserService_profile_label(UserService $service): string
{
return 'snake:' . $service->name;
}
function user_service_wrongName(UserService $service): string
{
return 'wrong class prefix';
}
public static function displayName(UserService $service): string
{
return 'camel:' . $service->name;
}
function UserService_other_name(UserService $service): string
{
return 'wrong method suffix';
}
public static function profile_label(UserService $service): string
{
return 'snake:' . $service->name;
}
function UserService_CASECheck(UserService $service): string
{
return 'case-insensitive:' . $service->name;
public static function CASECheck(UserService $service): string
{
return 'case-insensitive:' . $service->name;
}
}
}

@ -1,23 +1,35 @@
--TEST--
Universal method: extension functions
Universal methods provided by ExtensionProvider classes
--FILE--
<?php
use native_types;
function int_to_bytes(int $int, string $unit = 'Kb'): string
#[ExtensionProvider(native_types::type_int)]
final class IntExtensions
{
return ($int / 1024) . $unit;
public static function to_bytes(int $int, string $unit = 'Kb'): string
{
return ($int / 1024) . $unit;
}
}
function array_get_first_element(array $array): mixed
#[ExtensionProvider(complex_types::type_array)]
final class ArrayExtensions
{
return $array[0];
public static function get_first_element(array $array): mixed
{
return $array[0];
}
}
function str_shout(string $str): string
#[ExtensionProvider(complex_types::type_string)]
final class StringExtensions
{
return strtoupper($str) . '!';
public static function shout(string $str): string
{
return strtoupper($str) . '!';
}
}
function main()

@ -1,18 +1,26 @@
--TEST--
Universal extension methods support lowerCamelCase function names
Universal ExtensionProvider methods use their declared names
--FILE--
<?php
use native_types;
function int_toBytes(int $value): string
#[ExtensionProvider(native_types::type_int)]
final class IntExtensions
{
return ($value / 1024) . 'Kb';
public static function toBytes(int $value): string
{
return ($value / 1024) . 'Kb';
}
}
function array_getFirstElement(array $value): mixed
#[ExtensionProvider(complex_types::type_array)]
final class ArrayExtensions
{
return $value[0];
public static function getFirstElement(array $value): mixed
{
return $value[0];
}
}
function main(): void

@ -1,36 +1,49 @@
--TEST--
Universal method: extension function chaining with typed return
ExtensionProvider method chaining with typed returns
--FILE--
<?php
use native_types;
function int_to_words(int $int): string
#[ExtensionProvider(native_types::type_int)]
final class IntExtensions
{
$map = [1 => 'one', 2 => 'two', 3 => 'three'];
return $map[$int] ?? 'unknown';
public static function to_words(int $int): string
{
$map = [1 => 'one', 2 => 'two', 3 => 'three'];
return $map[$int] ?? 'unknown';
}
}
function str_double(string $str): string
#[ExtensionProvider(complex_types::type_string)]
final class StringExtensions
{
return $str . $str;
}
public static function double(string $str): string
{
return $str . $str;
}
function str_get_length(string $str): int
{
return strlen($str);
}
public static function get_length(string $str): int
{
return strlen($str);
}
function str_to_array(string $str, string $delimiter): array
{
return $str->split($delimiter);
public static function to_array(string $str, string $delimiter): array
{
return $str->split($delimiter);
}
}
function array_last(array $arr): mixed {
if ($arr->count() === 0) {
return null;
#[ExtensionProvider(complex_types::type_array)]
final class ArrayExtensions
{
public static function last(array $arr): mixed
{
if ($arr->count() === 0) {
return null;
}
return $arr[$arr->count() - 1];
}
return $arr[$arr->count() - 1];
}
function main()

@ -1,10 +1,19 @@
--TEST--
Universal method: PHP internal function as extension method via reflection
Universal method provider may wrap a PHP internal function
--FILE--
<?php
use native_types;
#[ExtensionProvider(complex_types::type_string)]
final class StringExtensions
{
public static function rot13(string $value): string
{
return str_rot13($value);
}
}
function main()
{
$str = "hello";

Loading…
Cancel
Save