diff --git a/examples/extends/test.php b/examples/extends/test.php new file mode 100644 index 00000000..fcb74183 --- /dev/null +++ b/examples/extends/test.php @@ -0,0 +1,40 @@ +foo(); + + $c = new C; + $c->offsetSet(0, 1); + $c->offsetSet(1, 2); + var_dump($c); + var_dump($c->foo()); +} diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index 03d34579..35fae7eb 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -18,5 +18,6 @@ class ArgInfo public ?Expr $defaultValue = null; public bool $byRef = false; public bool $variadic = false; + public bool $nullable = false; public bool $property = false; } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index d7d8342d..b44d9231 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -215,6 +215,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected ?MethodDef $methodDef = null; protected ?InterfaceDef $interfaceDef = null; protected FunctionContext $context; + protected array $superGlobalVars = [ '_GET' => self::TYPE_ARRAY, '_POST' => self::TYPE_ARRAY, @@ -1014,6 +1015,9 @@ class CompilerBase extends \PhpAot\Core\Translator $argInfo->byRef = $param->byRef; $argInfo->variadic = $param->variadic; $argInfo->property = $param->isPromoted(); + if ($param->type and $param->type instanceof Node\NullableType) { + $argInfo->nullable = true; + } if ($param->default) { $defaultValueCount++; $argInfo->default = $this->parseParamDefaultValue($param->default); @@ -1606,7 +1610,15 @@ class CompilerBase extends \PhpAot\Core\Translator return false; } if (!$this->hasNativeClass($classDef->extends)) { - $this->climate->error('Native method `' . $class . '::' . $method . '()` not found, the parent class `' . $classDef->extends . '` is not defined'); + if ($classDef->inheritedFromInternalClass) { + if (!Reflection::hasMethod($classDef->extends, $method) and !Reflection::hasMethod($classDef->extends, $method . '__call')) { + $this->fatalError($expr, 'Class `' . $classDef->getNamespacedName() . '` inherits from a internal class, but the class `' . + $classDef->extends . '` does not have a `' . $method . '` method or a `__call` magic method'); + } else { + $this->climate->cyan('Dynamically calling internal class method `' . $classDef->extends . '::' . $method . '()`'); + throw new DynamicCall; + } + } return false; } $classDef = $this->getClassDef($classDef->extends); diff --git a/src/Php/Entity/ClassDef.php b/src/Php/Entity/ClassDef.php index d6f0eba8..74591693 100644 --- a/src/Php/Entity/ClassDef.php +++ b/src/Php/Entity/ClassDef.php @@ -29,6 +29,7 @@ class ClassDef extends ClassLikeDef public bool $requireCtor = false; public bool $enum = false; public int $flags; + public bool $inheritedFromInternalClass = false; public function __construct(string $name, int $flags, string $namespace = '') { diff --git a/src/Php/Reflection.php b/src/Php/Reflection.php index ee8a29ec..25b4b5bd 100644 --- a/src/Php/Reflection.php +++ b/src/Php/Reflection.php @@ -13,6 +13,29 @@ class Reflection private static array $functions = []; private static array $classes = []; + public static function isInternalClass(string $class): bool + { + static $internalClasses = null; + + if ($internalClasses === null) { + $allClasses = get_declared_classes(); + + $internalClasses = []; + foreach ($allClasses as $className) { + try { + $ref = new \ReflectionClass($className); + if ($ref->isInternal()) { + $internalClasses[strtolower($className)] = true; + } + } catch (\ReflectionException) { + continue; + } + } + } + + return isset($internalClasses[strtolower($class)]); + } + public static function getFunction(string $fn): ?\ReflectionFunction { if (!isset(self::$functions[$fn])) { @@ -104,4 +127,13 @@ class Reflection } return $param->isPassedByReference() ? $param->getName() : null; } + + public static function hasMethod(string $extends, string $method): bool + { + $class = self::getClass($extends); + if (!$class) { + return false; + } + return $class->hasMethod($method); + } } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index fb05d9ff..534b440a 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -241,6 +241,10 @@ class Translator extends Preprocessor if (ctype_digit($this->targetName[0])) { return 'app_' . $this->targetName; } + $extensions = get_loaded_extensions(); + if (in_array($this->targetName, $extensions)) { + return $this->targetName . '_'; + } return $this->targetName; } @@ -812,11 +816,20 @@ class Translator extends Preprocessor } if ($extends) { - $this->classDef->extends = $this->getParentClass($class->extends); - if (isset($this->classes[$this->classDef->extends])) { - $parent = $this->classes[$this->classDef->extends]; + $parentClass = $this->getParentClass($class->extends); + if ($this->hasNativeClass($parentClass)) { + $parent = $this->getClassDef($parentClass); if ($parent->flags & Modifiers::FINAL) { - $this->fatalError($class, "Class `{$this->class}` cannot extend final class `{$this->classDef->extends}`"); + $this->fatalError($class, "Class `{$this->class}` cannot extend final class `{$parentClass}`"); + } + $this->classDef->extends = $parentClass; + $this->classDef->inheritedFromInternalClass = false; + } else { + if (Reflection::isInternalClass($parentClass)) { + $this->classDef->extends = $parentClass; + $this->classDef->inheritedFromInternalClass = true; + } else { + $this->fatalError($class, "Class `{$this->class}` inherits from a non-existent class `$parentClass`"); } } } @@ -886,6 +899,8 @@ class Translator extends Preprocessor } else { if ($argInfo->byRef) { $argExpr = 'php::getCallArgByRef(' . $k . ')'; + } elseif ($argInfo->nullable) { + $argExpr = 'php::getCallArg(' . $k . ', php::null)'; } else { $argExpr = 'php::getCallArg(' . $k . ')'; } diff --git a/tests/aot/extends-redis.phpt b/tests/aot/extends-redis.phpt new file mode 100644 index 00000000..e74e0b0a --- /dev/null +++ b/tests/aot/extends-redis.phpt @@ -0,0 +1,24 @@ +--TEST-- +extends redis +--FILE-- +connect('127.0.0.1', 6379); + $uuid = uniqid(); + var_dump($o->set('key', $uuid)); + var_dump($o->get('key') === $uuid); +} +?> +--EXPECT-- +bool(true) +bool(true)