refactor(compiler): replace output parameters with return values and adjust visibility

- Replace appendCapturedStmtLines helper with direct formatCapturedStmtLines calls
- Change createPropertyAccessResolver from private to protected for extensibility
- Change resolveNativeInstanceProperty from private to protected for extensibility
- Change resolveNativeStaticProperty from private to protected for extensibility
- Change applyNativePropertyAccessResult from private to protected for extensibility
- Change containsNullsafeChain from private to protected for extensibility
- Replace parseTypeDecl output parameter with resolveTypeDecl returning array
- Change sortFiles to getSortedFiles returning sorted array instead of modifying input
- Update name resolution to handle imported namespace prefixes correctly
- Enhance property hook backing access detection with additional conditions
- Add redis extension availability check to test skip conditions
- Document compiler self-hosting constraints regarding reference parameters across traits
pull/17/head
韩天峰 1 month ago
parent b5d5d3b259
commit c7bdfd883d
  1. 12
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 28
      docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md
  3. 11
      phpunit/src/PreprocessorTest.php
  4. 2
      src/Build/SourcePipelineTrait.php
  5. 19
      src/CompilerBase.php
  6. 3
      src/Generator/FiberGenerator.php
  7. 19
      src/Parser/LoopControlTrait.php
  8. 3
      src/Parser/NullsafeAccessTrait.php
  9. 14
      src/Parser/PropertyAccessTrait.php
  10. 9
      src/Parser/SwitchTrait.php
  11. 39
      src/Preprocessor.php
  12. 24
      src/Resolver/NameResolutionTrait.php
  13. 7
      src/gen_stub.php
  14. 15
      tests/compiler/basic/extends-redis.phpt

@ -34,6 +34,7 @@
## 调用与引用
- TypePHP 使用严格参数数量规则:非 variadic 函数不接受声明范围之外的额外参数;`func_get_args()` 不会隐式放宽签名。
- 已知签名的普通函数、普通方法和 native 直调支持引用参数及写回;不要把编译器内部跨 Trait 动态分派的限制误写成“TypePHP 不支持引用参数”。
- 闭包和箭头函数不支持引用参数。
- 引用赋值不支持从复杂静态属性表达式建立引用。
- 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()` 或等价关键词方法 `toRef()`
@ -66,3 +67,14 @@
- `Closure::bind()` 绑定静态闭包访问私有成员时,当前行为与标准 PHP 不完全一致。
- first-class callable 存入 typed nullable `Closure` 属性后,当前存在运行时稳定性限制。
- 所有源文件必须是 `UTF-8` 编码。
## 编译器自举与内部重构约束
本节描述编译器自身使用 TypePHP 编译时的约束,不是面向用户代码新增的 PHP 语义差异。
- 重构前,同一核心类内可静态解析的 `$this->method()` 会生成 native C++ 直调。引用参数会直接映射为 `php::Ref` 或 C++ 引用,写回语义正常。
- 将调用方和被调用方拆到不同 Trait 后,单独编译 Trait 本体时无法从 Trait 的 `$this` 确定最终宿主类。当前方法解析器可能将跨 Trait 调用降级为 Zend method call,例如生成 `this_.call(..., php::ArgList{value})`
- 动态 method call 的 `ArgList` 不会仅凭被调 wrapper 的 arginfo 自动把普通实参升级为引用。若被调方法声明 `&$value`,wrapper 会通过 `getCallArgByRef()` 取参;调用方传入的却是普通值,结果是 `must be passed by reference` 警告,并且被调方修改无法写回调用方。
- 因此,编译器内部跨 Trait API 禁止使用引用输出参数和“修改传入标量/数组后由调用方读取”的协议。应返回结果值、元组数组或 DTO,例如用 `[$type, $class] = resolveTypeDecl(...)` 代替 `parseTypeDecl(..., &$class)`
- 对字符串累加、数组排序、解析结果输出等内部 helper,优先设计为纯返回值:`$code .= format(...)`、`$files = sort(...)`。只有确认调用会保持 native 直调时,才允许依赖引用写回。
- 每次移动方法到 Trait、父类或独立组件后,必须使用自举产物重新编译至少一个覆盖该调用的测试;仅使用 `bin/tpc.php` 运行测试不能发现“源编译器正常、自举编译器退化”的问题。

@ -112,6 +112,34 @@ These items should be documented with the exact boundary.
| Native typed properties | Partial / Intentional Rule | Fast native paths may not preserve every PHP dynamic state transition. Unknown or incompatible values can fall back to `setProperty()`. |
| Reflection metadata | Partial | Runtime declarations exist, but some AOT-specific metadata such as promoted-property flags may be incomplete. |
## Self-hosting Compatibility Notes
The compiler itself is a TypePHP program, so an internal refactor can change
how its own calls are lowered even when the PHP source-level API is unchanged.
This is an implementation compatibility boundary, not a new user-facing rule
that reference parameters are unsupported.
| Internal pattern | Status | Boundary and required design |
|---|---|---|
| Statically resolved function or method with by-reference parameters | Supported | Native direct calls preserve reference slots and write-back semantics. This was the path used before the core methods were split into traits. |
| Cross-trait `$this->method()` with a by-reference output parameter | Self-hosting Partial | While compiling a trait body, the final consuming class may be unknown. The call can fall back to `this_.call()` with ordinary `ArgList` values, while the callee wrapper expects `getCallArgByRef()`. This produces a by-reference warning and loses write-back. |
| Cross-trait helper returning a value, tuple array or DTO | Supported / Required internally | Return data explicitly and assign it at the call site. Do not use by-reference output parameters for compiler services that may cross trait boundaries. |
| Moving an existing method into a trait | Requires bootstrap verification | Test both the PHP-source compiler and the newly bootstrapped `tpc`; source-compiler tests alone do not exercise the changed lowering path. |
The observed regression after refactoring followed this exact sequence:
1. Before extraction, calls such as file sorting, captured-statement appending and
type declaration parsing were statically resolved inside the core class.
2. After extraction, their callers and implementations lived in different
traits mixed into `Translator`, `CompilerBase` or `Preprocessor`.
3. The self-hosted compiler emitted Zend dynamic method calls for those
cross-trait edges and passed ordinary values.
4. Callee wrappers still correctly advertised and parsed reference parameters,
but they could not retroactively turn the caller's value argument into the
caller's reference slot.
5. The fixes replaced internal output-parameter protocols with explicit return
values. User-level statically known reference calls remain supported.
## Documentation Rule
When documenting a compatibility difference, use one of these labels:

@ -239,25 +239,24 @@ class PreprocessorTest extends TestCase
}
// ========================================================================
// sortFiles
// getSortedFiles
// ========================================================================
public function testSortFilesPreservesOrderForUnrelatedFiles(): void
{
$files = ['/a/file1.php', '/a/file2.php', '/a/file3.php'];
$this->compiler->sortFiles($files);
$files = $this->invokeMethod('getSortedFiles', $files);
// All original files must still be present
$this->assertContains('/a/file1.php', $files);
$this->assertContains('/a/file2.php', $files);
$this->assertContains('/a/file3.php', $files);
// Original files are preserved (sortFiles may append, not remove)
// Original files are preserved (sorting may append, not remove)
$this->assertGreaterThanOrEqual(3, count($files));
}
public function testSortFilesEmpty(): void
{
$files = [];
$this->compiler->sortFiles($files);
$files = $this->invokeMethod('getSortedFiles', []);
// Empty array stays empty or nearly empty
$this->assertIsArray($files);
}
@ -334,7 +333,7 @@ class PreprocessorTest extends TestCase
$this->compiler->prepareFile($traitFile);
$files = [$classFile, $interfaceFile, $traitUserFile, $traitFile];
$this->compiler->sortFiles($files);
$files = $this->invokeMethod('getSortedFiles', $files);
$this->assertLessThan(array_search($classFile, $files, true), array_search($interfaceFile, $files, true));
$this->assertLessThan(array_search($traitUserFile, $files, true), array_search($traitFile, $files, true));

@ -99,7 +99,7 @@ trait SourcePipelineTrait
}
}
}
$this->sortFiles($files);
$files = $this->getSortedFiles($files);
return $files;
}

@ -1434,13 +1434,6 @@ class CompilerBase implements PropertyAccessContext
throw new \LogicException('Parsed expression must be stringable');
}
protected function appendCapturedStmtLines(string &$code, array $stmts): void
{
if ($stmts) {
$code .= $this->formatCapturedStmtLines($stmts);
}
}
protected function formatCapturedStmtLines(array $stmts): string
{
if (!$stmts) {
@ -1454,11 +1447,11 @@ class CompilerBase implements PropertyAccessContext
$this->assertExprCanBeUsedAsCondition($cond);
[$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($cond);
$code = '';
$this->appendCapturedStmtLines($code, $beforeStmts);
$code .= $this->formatCapturedStmtLines($beforeStmts);
if ($afterStmts) {
$tmpVar = $this->addTmpVar(Type::VAR);
$code .= $this->getIndent() . $tmpVar . ' = ' . $condExpr . ';' . PHP_EOL;
$this->appendCapturedStmtLines($code, $afterStmts);
$code .= $this->formatCapturedStmtLines($afterStmts);
$condExpr = $tmpVar;
}
@ -3380,7 +3373,7 @@ class CompilerBase implements PropertyAccessContext
}
}
private function createPropertyAccessResolver(): PropertyAccessResolver
protected function createPropertyAccessResolver(): PropertyAccessResolver
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'PropertyAccessResolver');
return new PropertyAccessResolver($this);
@ -3413,19 +3406,19 @@ class CompilerBase implements PropertyAccessContext
|| $this->isSameOrSubclassOf($declaringClass, $scope);
}
private function resolveNativeInstanceProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult
protected function resolveNativeInstanceProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult
{
$scope = $this->class ? $this->getFullClassName() : '';
return $this->createPropertyAccessResolver()->resolveNativeInstanceProperty($expr, $property, $class, $scope);
}
private function resolveNativeStaticProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult
protected function resolveNativeStaticProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult
{
$scope = $this->class ? $this->getFullClassName() : '';
return $this->createPropertyAccessResolver()->resolveNativeStaticProperty($expr, $property, $class, $scope);
}
private function applyNativePropertyAccessResult(NodeAbstract $expr, PropertyAccessResult $result): string
protected function applyNativePropertyAccessResult(NodeAbstract $expr, PropertyAccessResult $result): string
{
$offset = $this->getPropertyOffset($result->classDef->getNamespacedName(false), $result->property);
$expr->setAttribute('nativePropertyAccess', new NativePropertyAccess($offset, $result));

@ -110,8 +110,7 @@ trait FiberGenerator
return true;
}
$class = '';
$this->parseTypeDecl($type, self::DECL_TYPE_OF_RETURN, $class);
[, $class] = $this->resolveTypeDecl($type, self::DECL_TYPE_OF_RETURN);
$class = strtolower(ltrim($class, '\\'));
return in_array($class, ['iterator', 'traversable', 'typephp\\fibergenerator'], true);
}

@ -25,7 +25,7 @@ trait LoopControlTrait
foreach ($init as $expr) {
[$initExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$initExpr = $this->stringifyParsedExpr($initExpr);
$this->appendCapturedStmtLines($code, $beforeStmts);
$code .= $this->formatCapturedStmtLines($beforeStmts);
$list_expr[] = $initExpr;
if ($afterStmts) {
$list_expr[] = implode(";\n" . $this->getIndent(), $afterStmts);
@ -56,11 +56,11 @@ trait LoopControlTrait
$condResult = $this->genTmpVarName();
$condCode .= $this->getIndent() . 'bool ' . $condResult . ' = true;' . PHP_EOL;
foreach ($list_cond as [$condExpr, $beforeStmts, $afterStmts]) {
$this->appendCapturedStmtLines($condCode, $beforeStmts);
$condCode .= $this->formatCapturedStmtLines($beforeStmts);
if ($afterStmts) {
$tmpVar = $this->addTmpVar(Type::VAR);
$condCode .= $this->getIndent() . $tmpVar . ' = ' . $condExpr . ';' . PHP_EOL;
$this->appendCapturedStmtLines($condCode, $afterStmts);
$condCode .= $this->formatCapturedStmtLines($afterStmts);
$condExpr = $tmpVar;
}
$condCode .= $this->getIndent() . $condResult . ' = ' . $this->convertBoolExpr($condExpr) . ';' . PHP_EOL;
@ -80,9 +80,9 @@ trait LoopControlTrait
$loopExpr = $this->stringifyParsedExpr($loopExpr);
if ($beforeStmts || $afterStmts) {
$loopCode = '[&]() {';
$this->appendCapturedStmtLines($loopCode, $beforeStmts);
$loopCode .= $this->formatCapturedStmtLines($beforeStmts);
$loopCode .= $this->getIndent() . $loopExpr . ';' . PHP_EOL;
$this->appendCapturedStmtLines($loopCode, $afterStmts);
$loopCode .= $this->formatCapturedStmtLines($afterStmts);
$loopCode .= $this->getIndent() . '}()';
$list_loop[] = $loopCode;
} else {
@ -115,11 +115,11 @@ trait LoopControlTrait
$code = $this->parseBeforeStmtLines() . PHP_EOL;
if ($beforeStmts || $afterStmts) {
$code .= 'while (true) {' . PHP_EOL;
$this->appendCapturedStmtLines($code, $beforeStmts);
$code .= $this->formatCapturedStmtLines($beforeStmts);
if ($afterStmts) {
$tmpVar = $this->addTmpVar(Type::VAR);
$code .= $this->getIndent() . $tmpVar . ' = ' . $cond . ';' . PHP_EOL;
$this->appendCapturedStmtLines($code, $afterStmts);
$code .= $this->formatCapturedStmtLines($afterStmts);
$cond = $tmpVar;
}
$code .= $this->getIndent() . 'if (!(' . $cond . ')) { break; }' . PHP_EOL;
@ -141,11 +141,11 @@ trait LoopControlTrait
[$cond, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($v->cond);
if ($beforeStmts || $afterStmts) {
$condCode = '[&]() -> bool {';
$this->appendCapturedStmtLines($condCode, $beforeStmts);
$condCode .= $this->formatCapturedStmtLines($beforeStmts);
if ($afterStmts) {
$tmpVar = $this->addTmpVar(Type::VAR);
$condCode .= $this->getIndent() . $tmpVar . ' = ' . $cond . ';' . PHP_EOL;
$this->appendCapturedStmtLines($condCode, $afterStmts);
$condCode .= $this->formatCapturedStmtLines($afterStmts);
$cond = $tmpVar;
}
$condCode .= $this->getIndent() . 'return ' . $this->convertBoolExpr($cond) . ';';
@ -216,4 +216,3 @@ trait LoopControlTrait
}
}

@ -109,7 +109,7 @@ trait NullsafeAccessTrait
return "{$tmpFn}()";
}
private function containsNullsafeChain(NodeAbstract $expr): bool
protected function containsNullsafeChain(NodeAbstract $expr): bool
{
while ($expr instanceof Expr\PropertyFetch
|| $expr instanceof Expr\MethodCall
@ -161,4 +161,3 @@ trait NullsafeAccessTrait
}
}

@ -839,7 +839,19 @@ trait PropertyAccessTrait
protected function isPropertyHookBackingAccess(NodeAbstract $expr): bool
{
return $expr->getAttribute(PropertyHookLowering::BACKING_ACCESS_ATTRIBUTE, false) === true;
if ($expr->getAttribute(PropertyHookLowering::BACKING_ACCESS_ATTRIBUTE, false) === true) {
return true;
}
if (!$expr instanceof Expr\PropertyFetch
|| !$expr->var instanceof Expr\Variable
|| $expr->var->name !== 'this'
|| !$expr->name instanceof Node\Identifier) {
return false;
}
$property = $expr->name->toString();
return $this->method === PropertyHookLowering::getterName($property)
|| $this->method === PropertyHookLowering::setterName($property);
}
protected function getPropertyHookGetter(NodeAbstract $expr): ?string

@ -24,9 +24,9 @@ trait SwitchTrait
}
[$condExpr, $condBeforeStmts, $condAfterStmts] = $this->parseExprWithCapturedStmts($cond);
$var_def = '';
$this->appendCapturedStmtLines($var_def, $condBeforeStmts);
$var_def .= $this->formatCapturedStmtLines($condBeforeStmts);
$var_def .= $type . ' ' . $tmp_var . ' = ' . $condExpr . ';' . PHP_EOL;
$this->appendCapturedStmtLines($var_def, $condAfterStmts);
$var_def .= $this->formatCapturedStmtLines($condAfterStmts);
// 保存作用域,switch 可能会解析失败,在这个过程中会增加变量,需重置
$localVars = $this->context->localVars;
@ -118,11 +118,11 @@ trait SwitchTrait
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $caseAfterStmtCount);
$code .= $this->getIndent() . 'if (!' . $switchMatched . ' && !' . $groupMatched . ') {' . PHP_EOL;
$this->appendCapturedStmtLines($code, $caseBeforeStmts);
$code .= $this->formatCapturedStmtLines($caseBeforeStmts);
if ($caseAfterStmts) {
$caseTmpVar = $this->addTmpVar(Type::VAR);
$code .= $this->getIndent() . $caseTmpVar . ' = ' . $caseCondExpr . ';' . PHP_EOL;
$this->appendCapturedStmtLines($code, $caseAfterStmts);
$code .= $this->formatCapturedStmtLines($caseAfterStmts);
$caseCondExpr = $caseTmpVar;
}
$code .= $this->getIndent() . $groupMatched . ' = php::equals(' . $tmp_var . ', ' . $caseCondExpr . ');' . PHP_EOL;
@ -163,4 +163,3 @@ trait SwitchTrait
}
}

@ -31,7 +31,7 @@ use PhpParser\NodeTraverser;
class Preprocessor extends CompilerBase
{
public function sortFiles(array &$list): void
protected function getSortedFiles(array $list): array
{
$sorter = new StringSort();
$fileDeps = [];
@ -61,9 +61,8 @@ class Preprocessor extends CompilerBase
}
}
$list = $sortedFiles;
$this->climate->lightBlue('prepare completed: ' . count($list) . ' source files in total');
$this->climate->lightBlue('prepare completed: ' . count($sortedFiles) . ' source files in total');
return $sortedFiles;
}
protected function genArgumentDeclaration(ArgInfo $argInfo): string
@ -269,8 +268,7 @@ class Preprocessor extends CompilerBase
if ($param->byRef) {
return Type::REF;
}
$class = '';
$type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class);
[$type, $class] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM);
$argInfo->undeclared = $param->type === null;
if (
$param->type !== null
@ -434,8 +432,7 @@ class Preprocessor extends CompilerBase
}
$fnName = $this->parseIdentifier($v->name);
$class = '';
$returnType = $this->parseTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN, $class);
[$returnType, $class] = $this->resolveTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN);
// 构造、析构、克隆方法不能有返回值
if ($this->method and in_array($this->method, ['__construct', '__destruct', '__clone'])) {
$returnType = Type::VOID;
@ -587,13 +584,18 @@ class Preprocessor extends CompilerBase
}
}
// 将 trait 方法参数中的类名升级为 FullyQualified,避免 gen_stub 时上下文丢失
// Trait members are later injected into the consuming class for stub
// generation. Fully qualify every declared class type while the
// trait's own namespace/import context is still active.
if ($class instanceof Node\Stmt\Trait_) {
foreach ($class->stmts as $v) {
if ($v instanceof Node\Stmt\ClassMethod) {
$v->returnType = $this->upgradeToFullyQualifiedName($v->returnType);
foreach ($v->params as $param) {
$param->type = $this->upgradeToFullyQualifiedName($param->type);
}
} elseif ($v instanceof Node\Stmt\Property || $v instanceof Node\Stmt\ClassConst) {
$v->type = $this->upgradeToFullyQualifiedName($v->type);
}
}
}
@ -702,9 +704,9 @@ class Preprocessor extends CompilerBase
{
$this->resetFunction();
$flags = $this->parseModifiers($v->flags);
$class = '';
$declaredType = $v->type ? $this->parseTypeDecl($v->type, self::DECL_TYPE_OF_CONST, $class) : null;
[$declaredType, $class] = $v->type
? $this->resolveTypeDecl($v->type, self::DECL_TYPE_OF_CONST)
: [null, ''];
foreach ($v->consts as $const) {
$type = $declaredType;
@ -752,8 +754,7 @@ class Preprocessor extends CompilerBase
protected function addClassProperty(string $name, int $flags, ?NodeAbstract $typeNode, $defaultNode, bool $nullable, NodeAbstract $errorNode, bool $promoted = false): PropertyDef
{
$flags = $this->parseModifiers($flags);
$class = '';
$type = $this->parseTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY, $class);
[$type, $class] = $this->resolveTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY);
$default = null;
$arrayInitPlan = null;
@ -927,14 +928,16 @@ class Preprocessor extends CompilerBase
if ($this->interfaceDef->hasConstant($constName)) {
$this->fatalError($stmt, "Duplicate constant `{$constName}`");
}
$class = '';
$type = $stmt->type
? $this->parseTypeDecl($stmt->type, self::DECL_TYPE_OF_CONST, $class)
: match ($const->value->getType()) {
if ($stmt->type) {
[$type, $class] = $this->resolveTypeDecl($stmt->type, self::DECL_TYPE_OF_CONST);
} else {
$class = '';
$type = match ($const->value->getType()) {
'Expr_Array' => Type::ARRAY,
'Scalar_String' => Type::STR,
default => Type::VAR,
};
}
$constInfo = $this->parseClassLikeConstant($const, $this->parseModifiers($stmt->flags), $type, $class);
$this->interfaceDef->constants[$constName] = $constInfo;
}

@ -103,10 +103,21 @@ trait NameResolutionTrait
if (isset($this->zendTypeMap[strtolower($typeName)]) || in_array(strtolower($typeName), ['self', 'static', 'parent'], true)) {
return $type;
}
if ($type->isQualified()) {
return new Node\Name\FullyQualified($typeName, $type->getAttributes());
$resolved = $typeName;
$firstSegment = explode('\\', $typeName, 2)[0];
$hasImportedPrefix = isset($this->useAliases[$firstSegment]);
if (!$hasImportedPrefix) {
foreach ($this->useNamespaces as $useNamespace) {
$segments = explode('\\', trim($useNamespace, '\\'));
if (strcasecmp($segments[array_key_last($segments)], $firstSegment) === 0) {
$hasImportedPrefix = true;
break;
}
}
}
if (!$type->isQualified() || $hasImportedPrefix) {
$resolved = $this->getNamespacedClassName($typeName);
}
$resolved = $this->getNamespacedClassName($typeName);
return new Node\Name\FullyQualified($resolved, $type->getAttributes());
}
return $type;
@ -129,6 +140,13 @@ trait NameResolutionTrait
/**
* @param string $class 一定是带有命名空间的完整类名
*/
protected function resolveTypeDecl(?NodeAbstract $type, int $what): array
{
$class = '';
$declaredType = $this->parseTypeDecl($type, $what, $class);
return [$declaredType, $class];
}
protected function parseTypeDecl(?NodeAbstract $type, int $what, string &$class): string
{
// 未定义类型视为 var (mixed, any)

@ -245,7 +245,12 @@ class SimpleType {
return new SimpleType('mixed', true);
}
$class = $node->isFullyQualified() ? $node->toString() : getTranslator()->getNamespacedClassName($node->toString());
$resolvedName = $node->getAttribute('resolvedName');
if ($resolvedName instanceof Node\Name) {
$class = $resolvedName->toString();
} else {
$class = $node->isFullyQualified() ? $node->toString() : getTranslator()->getNamespacedClassName($node->toString());
}
return new SimpleType($class, false);
}

@ -1,5 +1,20 @@
--TEST--
extends redis
--SKIPIF--
<?php
if (!extension_loaded('redis')) {
die('skip redis extension is not available');
}
try {
$redis = new Redis();
if (!$redis->connect('127.0.0.1', 6379, 0.2)) {
die('skip redis server is not available');
}
$redis->close();
} catch (Throwable) {
die('skip redis server is not available');
}
?>
--FILE--
<?php
class MyRedis extends \redis

Loading…
Cancel
Save