diff --git a/lib/VM.php b/lib/VM.php index 8359faaa893..9cb9becd0f8 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -16741,28 +16741,11 @@ protected function initStaticCallable( $this->assertMethodCallableStatically($class, $methodLc); } } catch (\LogicException $e) { - $magicClass = $this->findMagicCallStaticClass($lcClass); - if (null === $magicClass) { - throw $e; - } - $frame->magicCallMethodName = $methodName; - $vis = $magicClass->methodVisibility['__callstatic'] ?? \PHPCfg\Func::FLAG_PUBLIC; - $callerClassLc = null; - if (null !== $frame->block->func && null !== $frame->block->func->class) { - $callerClassLc = strtolower($frame->block->func->class->value); + // Missing method → zend_std_get_static_method slow path → __callStatic (#3273). + if ($this->tryDispatchCallStatic($frame, $lcClass, $methodName)) { + return; } - MethodVisibility::assertCallable( - $vis, - $callerClassLc, - strtolower($magicClass->name), - $magicClass->name, - '__callStatic' - ); - $frame->call = $magicClass->methods['__callstatic']; - $frame->callArgs = []; - $frame->callArgEntries = []; - - return; + throw $e; } $vis = $class->methodVisibility[$methodLc] ?? \PHPCfg\Func::FLAG_PUBLIC; $callerClassLc = $this->callerClassLc($frame); @@ -16778,23 +16761,62 @@ protected function initStaticCallable( } $declaredName = $class->methodNames[$methodLc] ?? $methodName; $callerDisplay = $this->callerScopeDisplay($frame, $callerClassLc); - // zend_vm_def.h: INIT_STATIC_METHOD_CALL + CONSTRUCTOR → "Cannot call private …::__construct()" (#25663). - $staticConstructorCall = '__construct' === $methodLc; + try { + MethodVisibility::assertCallable( + $vis, + $callerClassLc, + strtolower($class->name), + $class->name, + $declaredName, + $parentScopeAllows, + fn (string $classLc, string $ancestorLc): bool => $this->isClassSameOrSubclassOf($classLc, $ancestorLc), + $callerDisplay + ); + } catch (\LogicException $e) { + // Inaccessible private/protected static → same __callStatic fallback as missing + // methods (php-src get_static_method_fallback / #25670, re-#3273). + if ($this->tryDispatchCallStatic($frame, $lcClass, $methodName)) { + return; + } + throw $e; + } + $frame->call = $class->methods[$methodLc]; + $frame->callArgs = $this->callArgsForStaticMethod($frame, $lcClass, $frame->call, $parentKeywordScope); + $frame->callArgEntries = []; + $frame->builtinCalleeQualifiedMethod = $class->name.'::'.$declaredName; + } + + /** + * Bind a static call to __callStatic when present (Zend get_static_method_fallback). + * + * Used for both missing methods (#3273) and inaccessible private/protected statics (#25670). + * + * @return bool true when the frame was bound to __callStatic + */ + private function tryDispatchCallStatic(Frame $frame, string $lcClass, string $methodName): bool + { + $magicClass = $this->findMagicCallStaticClass($lcClass); + if (null === $magicClass) { + return false; + } + $frame->magicCallMethodName = $methodName; + $vis = $magicClass->methodVisibility['__callstatic'] ?? \PHPCfg\Func::FLAG_PUBLIC; + $callerClassLc = null; + if (null !== $frame->block->func && null !== $frame->block->func->class) { + $callerClassLc = strtolower($frame->block->func->class->value); + } MethodVisibility::assertCallable( $vis, $callerClassLc, - strtolower($class->name), - $class->name, - $declaredName, - $parentScopeAllows, - fn (string $classLc, string $ancestorLc): bool => $this->isClassSameOrSubclassOf($classLc, $ancestorLc), - $callerDisplay, - $staticConstructorCall + strtolower($magicClass->name), + $magicClass->name, + '__callStatic' ); - $frame->call = $class->methods[$methodLc]; - $frame->callArgs = $this->callArgsForStaticMethod($frame, $lcClass, $frame->call, $parentKeywordScope); + $frame->call = $magicClass->methods['__callstatic']; + $frame->callArgs = []; $frame->callArgEntries = []; - $frame->builtinCalleeQualifiedMethod = $class->name.'::'.$declaredName; + + return true; } /** @@ -18098,12 +18120,6 @@ protected function inheritFromParent(ClassEntry $entry): void $this->rejectIncompatibleChildMethodSignature($entry, $parent, $name); continue; } - // Child redeclared a concrete parent method as abstract (zend_inheritance.c, #25660). - // Abstract decls live in abstractMethods, not methods — still enforce before inherit. - if (isset($entry->abstractMethods[$name])) { - $this->rejectIncompatibleChildMethodSignature($entry, $parent, $name); - continue; - } // PDO_*_Ext driver methods stay on PDO only (#21552). if (isset($parent->methodNotInherited[$name])) { continue; diff --git a/test/compliance/cases/language/magic_callstatic_private.phpt b/test/compliance/cases/language/magic_callstatic_private.phpt new file mode 100644 index 00000000000..dd391aa8201 --- /dev/null +++ b/test/compliance/cases/language/magic_callstatic_private.phpt @@ -0,0 +1,37 @@ +--TEST-- +language: inaccessible private/protected static → __callStatic (issue #25670, re-#3273) +--FILE-- +