PHP 8.4 modernization batch: native lazy objects, property hooks, str_*/array_any + latent strpos bug fixes - #612
Merged
Conversation
Container: materializeService() now creates services registered via addLazyService() as native lazy proxies (ReflectionClass::newLazyProxy), so retrieval hands out typed, instanceof-correct objects whose factory only runs on the first real interaction. Classes PHP cannot make lazy (internal classes and their non-stdClass subclasses, abstract classes, enums, readonly classes before PHP 8.5, and classes without instance properties - PHP creates those pre-initialized, which would silently skip the factory) keep the previous eager construction path. Aspect registration by class name gains an eager validation step (implements Aspect, default-constructibility) that runs when the lazy object materializes, so misconfiguration still surfaces on retrieval while construction stays deferred until first use. CachedAspectLoader: the @Property + __get() ghost pattern for the inner loader is replaced with a hooked property that memoizes the container lookup in its backing store; the magic method, its RuntimeException fallthrough and #[AllowDynamicProperties] are gone. ReflectionConstructorInvocation::proceed() (item 3 of the issue) is deliberately left as newInstanceWithoutConstructor() + explicit constructor invocation: a lazy-ghost sequence would either have to be initialized immediately (no gain) or defer constructor side effects, changing observable behavior, and newLazyGhost() rejects classes (internal, property-less) that the current sequence handles. docs/php84-limitations.md now accurately describes what the container does with lazy objects. Partially fixes #606 (items 1, 2 and 4; item 3 evaluated and skipped) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
StreamMetaData: the magic __get/__set pair for the virtual 'source' property becomes a native hooked property - reading still rebuilds the source from the token stream, writing still triggers the existing E_USER_DEPRECATED notice and re-tokenizes. The @property-read docblock is gone; new StreamMetaDataTest covers both hook paths. LazyAdvisorAccessor: #[AllowDynamicProperties] and the dynamic-property __get cache are replaced by a typed private array cache behind the existing getInterceptor() accessor (its only call site, InterceptorInjector, already uses getInterceptor()). AbstractAttribute: __get/__set existed only to throw BadMethodCallException for unknown properties; nothing relies on that behavior, so both are removed - attribute classes now expose only their promoted readonly properties. CachedAspectLoader's __get was converted separately as part of the lazy objects work (#606). Fixes #607 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
Bug fixes: - Enumerator::getInPaths() used strpos(...) === false - a substring-anywhere test - as a prefix check, so an include path that merely contained the root directory in the middle (e.g. '/somewhere/base/other' with root '/base') was wrongly accepted instead of rejected; now str_starts_with() with a regression test. - WeavingTransformerTest asserted assertFalse(strpos(...)), which also passes for a match at offset 0; now assertStringNotContainsString(). Modernizations (no behavior change): - Enumerator: PHP_OS 'WIN' prefix sniff -> PHP_OS_FAMILY === 'Windows'; filter root check -> str_starts_with(); include/exclude fnmatch loops -> array_any() with a shared pattern matcher. - PathResolver / AdviceMatcher: strpos(...) !== false -> str_contains(). - OrPointcut / AndPointcut: matches() loops -> array_any()/array_all(). - AbstractInterceptedPropertyGenerator: union-type array scan -> array_any(). - ReflectionFilenameTest: single-argument ReflectionProperty::setValue() (deprecated) -> two-argument form for the static property; the suite now finishes with zero PHP deprecations on 8.4, 8.5 and 8.6. WeavingTransformer.php is deliberately untouched - it is being rewritten on another branch, so its strpos/end() sites are out of scope here. Fixes #609 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Modernization batch (issues #606, #607, #609)
Three commits, one per issue. Gates on every commit:
php8.5 vendor/bin/phpunitgreen,php8.4 vendor/bin/phpunitgreen, PHPStan level 10 clean (no new baseline entries).Task 1 — native lazy objects (Partially fixes #606)
Container:materializeService()now materializes services registered viaaddLazyService()as native lazy proxies (ReflectionClass::newLazyProxy()). Retrieval hands out a typed,instanceof-correct instance of the service class; the registered factory only runs on the first real interaction with the object.registerAspect()by class name gains an eager validation step (implementsAspect, default-constructibility) that runs when the entry materializes, so misconfiguration still surfaces on retrieval while construction stays deferred.CachedAspectLoader: the@property+__get('loader')ghost pattern becomes a hooked property that memoizes the container lookup in its backing store; the magic method, itsRuntimeExceptionfallthrough and#[AllowDynamicProperties]are removed.Behavior-parity notes for the lazy-object change:
add/addLazyService/getService/getValue/has/getServicesByInterface/registerAspect);has()and interface/tag introspection behave as before — materialization still tags by interface and registers the class file as a resource, without running the factory.stdClasssubclasses, abstract classes, enums, readonly classes before PHP 8.5, non-class ids — and notably classes without instance properties, because PHP creates lazy objects of property-less classes as already initialized, which would silently skip the factory (covered by a dedicated test).registerAspect(stdClass::class), missing factory for required ctor args) still throwAspectExceptionon retrieval, not later.AspectException("is not properly registered" / "returned an incompatible object"), now at initialization time.FilterInjectorTransformerregistering the stream filter,CachedAspectLoaderreading kernel options) run at first use of the service instead of first retrieval — always before the service can do anything.Item 3 of #606 (
ReflectionConstructorInvocation::proceed()) was evaluated and skipped, as the issue anticipated: a lazy-ghost sequence there would either have to be initialized immediately (no gain over the current explicit sequence) or defer constructor side effects and change observable behavior aroundgetThis()/Before advices;newLazyGhost()also rejects classes (internal, property-less) thatnewInstanceWithoutConstructor()+ explicit ctor invoke handles today. Left for a follow-up, hence "Partially fixes".docs/php84-limitations.mdnow accurately describes what the container does with lazy objects.Tests:
ContainerTestgained laziness-semantics coverage — factory not run at registration/retrieval/enumeration,instanceofandisUninitializedLazyObject()correct before initialization, first method call initializes and delegates, property-less services construct eagerly through their (possibly re-registered) factory.Task 2 — property hooks instead of internal
__get/__set(Fixes #607)StreamMetaData: the magicsourcepair becomes a native hooked property —getstill rebuilds the source from the token stream,setstill triggers the existingE_USER_DEPRECATEDnotice and re-tokenizes.@property-readdocblock removed; all in-repo accesses are reads and pass unchanged; newStreamMetaDataTestcovers both hook paths.LazyAdvisorAccessor:#[AllowDynamicProperties]+__getdynamic-property cache replaced by a typedarray<string, Interceptor>cache behind the existinggetInterceptor(); its only call site (InterceptorInjector) already usedgetInterceptor(), no->{$name}magic access exists in the repo.AbstractAttribute:__get/__setexisted only to throwBadMethodCallException; nothing in the repo relies on that, both removed.CachedAspectLoader.__getwas intentionally handled in Task 1 (per issue scoping).Task 3 —
str_*/array_any/array_all+ latent strpos bugs (Fixes #609)Bug fixes with regression tests:
Enumerator::getInPaths()usedstrpos($path, $root, 0) === false— a substring-anywhere test — as a prefix check, so an include path merely containing the root elsewhere (e.g./somewhere/base/otherfor root/base) was wrongly accepted. Now!str_starts_with(...);EnumeratorTest::testIncludePathMerelyContainingRootDirectoryIsRejectedproves the fixed behavior (plus a positive prefix sanity test).WeavingTransformerTestassertFalse(strpos($proxyContent, '\\\\Exception'))(also passes on a match at offset 0) →assertStringNotContainsString().Modernizations:
PHP_OS'WIN'sniff →PHP_OS_FAMILY === 'Windows';Enumeratorfilter prefix check →str_starts_with(); include/excludefnmatchloops →array_any();PathResolver/AdviceMatcher→str_contains();OrPointcut/AndPointcutmatches()→array_any()/array_all();AbstractInterceptedPropertyGeneratorunion-type scan →array_any().Deprecation cleanup:
ReflectionFilenameTestsingle-argumentReflectionProperty::setValue()→ two-argument form for the static property.Test evidence
php8.4 vendor/bin/phpunitphp8.5 vendor/bin/phpunitphp8.6 (8.6.0beta2) vendor/bin/phpunit(informational)php8.5 vendor/bin/phpstan analyze(level 10)Before this batch the suite ended with 1 deprecation (the
setValue()call); it now ends with zero on all three PHP versions.Partially fixes #606 (items 1, 2, 4; item 3 evaluated and deliberately skipped)
Fixes #607
Fixes #609
Generated by Claude Code