From 8b425effc99e0d0f120948accd2076024432f987 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sat, 1 Aug 2026 00:07:11 +0800 Subject: [PATCH 1/2] Fix GH-14156: Enforce inherited private trait requirements --- NEWS | 4 ++++ Zend/tests/traits/gh14156.phpt | 18 ++++++++++++++++ Zend/tests/traits/gh14156_2.phpt | 36 ++++++++++++++++++++++++++++++++ Zend/zend_inheritance.c | 8 +++++-- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/traits/gh14156.phpt create mode 100644 Zend/tests/traits/gh14156_2.phpt diff --git a/NEWS b/NEWS index 4364d69650e5..e75fc00dba0b 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0beta1 +- Core: + . Fixed bug GH-14156 (Inherited private methods incorrectly satisfied abstract + trait requirements). (Matthias Görgens) + - GMP: . Added optional $definitely_prime output parameter to gmp_prevprime(). (Weilin Du) diff --git a/Zend/tests/traits/gh14156.phpt b/Zend/tests/traits/gh14156.phpt new file mode 100644 index 000000000000..464443622404 --- /dev/null +++ b/Zend/tests/traits/gh14156.phpt @@ -0,0 +1,18 @@ +--TEST-- +GH-14156 (Inherited private method does not satisfy abstract trait requirement) +--FILE-- + +--EXPECTF-- +Fatal error: Class C contains 1 abstract method and must therefore be declared abstract or implement the remaining method (C::test) in %s on line %d diff --git a/Zend/tests/traits/gh14156_2.phpt b/Zend/tests/traits/gh14156_2.phpt new file mode 100644 index 000000000000..7045c9c71f21 --- /dev/null +++ b/Zend/tests/traits/gh14156_2.phpt @@ -0,0 +1,36 @@ +--TEST-- +GH-14156 (Abstract trait requirement replaces inherited private method) +--FILE-- +test(); + } +} + +class P { + private function test(): void {} +} + +abstract class C extends P { + use T; +} + +$method = new ReflectionMethod(C::class, 'test'); +var_dump($method->isAbstract()); +var_dump($method->getDeclaringClass()->getName()); + +class D extends C { + public function test(): void { + echo "implemented\n"; + } +} + +(new D())->run(); +?> +--EXPECT-- +bool(true) +string(1) "C" +implemented diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 4424c9a1a3ab..9041bcce5127 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2377,8 +2377,12 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_ return; } - /* Abstract method signatures from the trait must be satisfied. */ - if (fn->common.fn_flags & ZEND_ACC_ABSTRACT) { + /* Abstract method signatures from the trait must be satisfied. An inherited + * private method is not accessible from the using class, so allow the abstract + * trait method to replace it below instead of treating it as an implementation. */ + if ((fn->common.fn_flags & ZEND_ACC_ABSTRACT) + && (!(existing_fn->common.fn_flags & ZEND_ACC_PRIVATE) + || fixup_trait_scope(existing_fn, ce) == ce)) { /* "abstract private" methods in traits were not available prior to PHP 8. * As such, "abstract protected" was sometimes used to indicate trait requirements, * even though the "implementing" method was private. Do not check visibility From 49606a603f167c18ea3c8436f3185c215a104d78 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 09:58:31 +0800 Subject: [PATCH 2/2] Clarify the inherited-private comment --- Zend/zend_inheritance.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 9041bcce5127..94e18f780828 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2378,8 +2378,8 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_ } /* Abstract method signatures from the trait must be satisfied. An inherited - * private method is not accessible from the using class, so allow the abstract - * trait method to replace it below instead of treating it as an implementation. */ + * private method is not accessible from the using class, so it does not + * satisfy the requirement; only a private method from the class itself does. */ if ((fn->common.fn_flags & ZEND_ACC_ABSTRACT) && (!(existing_fn->common.fn_flags & ZEND_ACC_PRIVATE) || fixup_trait_scope(existing_fn, ce) == ce)) {