Skip to content

Replace lifetime docblock tags with method attributes - #741

Merged
binaryfire merged 36 commits into
0.4from
feature/hypervel-attributes
Nov 12, 2026
Merged

binaryfire merged 36 commits into
0.4from
feature/hypervel-attributes

Conversation

@binaryfire

@binaryfire binaryfire commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

This adds hypervel/attributes, a small package of method attributes, and uses it to replace the "Boot-only.", "Tests only." and "Boot or tests only." docblock warnings across the framework.

Hypervel workers are long-lived, so some methods are only safe to call while the application boots or in tests. Until now, those methods said so in prose at the start of a docblock paragraph. That works for someone reading the source, but it isn't reliable for tools. The worker-state analysis matched the tags with a regex, which also matched conditional prose such as "Boot-only when setting", and there was no way to say that only some calls to a method are restricted. Attributes carry the same information as exact metadata that PHPStan reads through reflection.

The attributes don't change runtime behavior. In this PR, the existing worker-state analysis uses them to skip writes in marked methods, as it did with the tags. Checking calls to marked methods is left to a follow-up that builds on this.

The attributes

BootOnly, TestsOnly and BootOrTestsOnly live in Hypervel\Support\Attributes. Each takes optional arguments for methods where only some calls are restricted:

  • whenNotDefault: 'parameter' restricts calls that pass that parameter a value other than its default. Relation::morphMap() uses it, so calling it without a map to read the current map is still fine at runtime.
  • whenProvided: 'parameter' restricts any call that passes that parameter. This is for methods that treat an explicit default differently from an omitted argument, such as Passport::rememberConsentFor(), where an explicit null clears the duration.
  • whenShared: true restricts calls only on the worker-shared instance, for methods that change only their own object. An HTTP client factory you create yourself can set its own global options; the application's shared factory should only be configured while booting.

A parameter condition can be combined with whenShared, in which case a call is restricted only when both apply.

Unsupported marks a retained public method that always throws, such as a Symfony or PSR method that doesn't fit Hypervel's runtime. It takes an optional message pointing to the supported alternative:

#[Unsupported('Return the response from your route or controller so the server can send it.')]
public function send(bool $flush = true): never

Base methods that drivers override, extension hooks, deliberate control-flow throws and methods that only reject some calls aren't marked.

The attributes are a separate package, rather than part of hypervel/support, because PHPStan drops an attribute that has arguments when the attribute class isn't installed. Every package using one has to depend on it, and several of those packages, such as container, contracts and collections, don't depend on Support. The package requires nothing but PHP and shares the Hypervel\Support namespace, the same way hypervel/macroable and hypervel/conditionable do.

Converting the existing warnings

Each tagged method was checked against what it actually changes instead of being converted mechanically. Most keep their restriction, but the conversion also corrected a number of them:

  • Methods that only change their own instance use whenShared, and their docblocks now say they affect the shared instance rather than every instance.
  • Caches that are rebuilt on demand, such as the view finder and routing reflection caches, are no longer restricted. Neither are the broadcast, queue, mail and filesystem manager purge() methods, whose documented use is recovering broken pooled resources at runtime.
  • Methods that forward to an injected object, such as event registration wrappers that receive their dispatcher, are left unmarked. Whether they touch shared state depends on the object passed in, and the target's own mutators carry the marker.
  • Methods that directly call a marked method carry the same marker, including facade fake() methods and service provider registration helpers.
  • Contracts and their implementations carry matching markers, since PHP attributes aren't inherited.
  • Registration that happens lazily on first use with stable definitions, such as Pennant class features and Eloquent model boot hooks, stays unmarked.

Each docblock keeps its explanation of what goes wrong; only the tag phrase is gone.

Context state owned by the right object

Checking each marker against its real behavior turned up places where coroutine context state was keyed by a fixed name instead of by the object that owns it. Independent instances could then read, replace or clear each other's state:

  • Container: scoped instances and resolution state used fixed context keys. A second container could return or clear the first container's scoped instances, and resolving through another container inside a binding could throw a CircularDependencyException. Each container now has its own lazily allocated key prefix. A cloned container gets a new prefix and starts with copies of the scoped instances already resolved in the current coroutine.
  • Guards: all six guards stored per-request users, credentials and caches under keys built from the guard name. A guard created later with the same name, for example after AuthManager::forgetGuards(), picked up the previous guard's user and cached credentials. Guard keys now belong to the guard instance. Session keys and remember cookies are unchanged, so a replacement guard still reauthenticates from a valid session.
  • Cache: managers with the same store name shared one memo() repository, and forgetDriver() evicted another manager's memo. Memo keys now belong to the manager. Cloning a cache repository also cloned its ArrayStore, which kept the original context keys, so writes through the clone changed the original. Cloned array stores now get their own keys, starting with copies of the current values and locks. A cloned tagged cache likewise keeps its tags on its cloned store, so flushing the copy no longer resets the original's tags.
  • Redis: pinned connections and deferred-release owners were keyed by connection name alone, so proxies on different pool managers shared a pin and could release each other's connections. The keys now include the owning pool manager. Proxies on the same manager still share their pin.

Only the keys change, not how values are stored, so child coroutines inherit context as before. The fixed-key constants (Container::SCOPED_CONTEXT_PREFIX and RESOLUTION_STATE_CONTEXT_KEY, JwtGuard::GUARD_CONTEXT_KEY_PREFIX, RedisProxy::CONNECTION_CONTEXT_PREFIX) and the getContextStateKey() helpers on SessionGuard and JwtGuard are removed.

Other changes

  • The Queue facade's popUsing() accepts null to remove a callback, matching Worker::popUsing().
  • Engine\Http\Stream::seek() has native int parameter types, matching PSR-7 v2.
  • The worker-state analysis reads the attributes through PHPStan reflection instead of matching docblock text.
  • The Static Analysis documentation has a new Method Attributes section, and AGENTS.md describes the attributes in place of the tags.
  • docs/todo.md lists the hypervel/attributes split repository and Packagist entry, which need to exist before release.

Validation

Formatting, PHPStan and the analyzer test suite pass. A standalone copy of the package was installed on its own and every attribute instantiated from it, and each package that uses an attribute declares the dependency. The context-key fixes have regression tests covering independent instances, invalidation, cloning, child coroutines and reauthentication after a guard is replaced.

Note

Replace lifetime docblock tags with BootOnly/TestsOnly method attributes

  • Adds a new hypervel/attributes package with BootOnly, BootOrTestsOnly, TestsOnly, and Unsupported method attributes, and replaces textual "Boot only"/"Tests only" docblock wording across nearly every framework package. Attributes carry conditions such as whenShared, whenNotDefault, and whenProvided, and expose the metadata through PHP reflection. hypervel/attributes ^0.4 is added as a dependency everywhere, and the PHPStan coroutine rules in WorkerStateHelper.php now detect lifetime restrictions via reflected attributes instead of docblock tag matching.
  • Coroutine context keys become per-instance instead of name-based. Container, CacheManager, PoolManager, RedisProxy, and the auth guards (GuardHelpers::contextKey, SessionGuard, TokenGuard, JwtGuard, Sanctum/Passport guards) now generate unique context-key prefixes per instance. A new guard with the same name no longer inherits the previous guard's user.
  • Container::__clone and ArrayStore/TaggedCache/MemoizedTaggedCache clone hooks copy scoped instances and memoized state into fresh context keys, so clones start with current state but stay independent.
  • Adds TagSet::withStore(), makes Queue::popUsing() accept null to remove the callback, and makes MemoizedTaggedCache::flush() clear its own memoized values.
  • Behavioral Change: context keys derived from guard/pool/container names change, so code reading coroutine context directly with the old hard-coded keys (as several tests previously did) breaks; CacheManager::forgetDriver() now string-casts numeric configured store names; flushState-adjacent detection in PHPStan rules keys off attributes, so any out-of-tree methods relying on docblock tags for detection are no longer recognized.

Macroscope summarized 8db14b5.

Introduce hypervel/attributes with BootOnly, TestsOnly, BootOrTestsOnly and Unsupported markers under Hypervel\Support\Attributes. Lifetime markers can describe argument-dependent restrictions and methods that only affect a shared receiver.

Wire the package into framework installation and Components autoloading without adding runtime enforcement or dependencies beyond PHP.
Give each container a lazily allocated context prefix so independent containers cannot adopt or invalidate each other’s scoped services or share circular-resolution stacks. Retain the existing context copying behavior and number of context lookups.

Cloned containers start with shallow copies of currently resolved scoped values while owning their cache entries independently. Cover independent resolution, invalidation, cloning and child-coroutine inheritance, and mark container mutators according to their corrected ownership.
Replace prose-tag matching with PHPStan attribute reflection for method bodies and promoted constructor parameters. Preserve the existing exemption for restricted setters, including attributes with argument conditions.

Convert the rule fixtures and cover a conditional request setter alongside the existing unmarked write. Remove obsolete lifetime tags from the configuration stub.
Convert core package lifetime warnings to attributes while retaining useful explanations of shared state. Distinguish instance-owned state from global effects, leave target-dependent forwarding methods unmarked, and declare the attributes dependency in each using package.

Mark unsupported stream operations and Str cache clearing, cover facade fake and registration wrappers, and allow Queue::popUsing(null) to remove a worker callback as its underlying API supports. Record the remaining conversions and the ArrayStore clone correction in the implementation plan.
Give each guard its own coroutine context keys so replacement guards do not adopt cached users, clients or credentials from an earlier instance. Preserve session and remember-cookie identifiers so persisted authentication still reloads normally.

Convert auth, session, cookie, encryption and hashing lifecycle restrictions to attributes and declare their dependency. Cover replacement guards across authentication drivers, HTTP test state synchronization and session reauthentication.
Allocate memoized cache keys per manager so independent managers cannot reuse or evict each other's repositories. Give cloned array stores fresh context keys and shallow copies of their current values and locks.

Convert cache lifecycle restrictions to attributes, mark unsupported any-mode reads and declare the attribute dependency. Regression coverage checks manager isolation and independent mutations through cloned repositories.
Build pinned-connection and deferred-release keys from the owning pool manager, caching the resulting keys on each proxy. Proxies sharing a manager and connection name continue to share their pin; independent managers no longer release or reuse each other's connections.

Convert Redis and object-pool lifecycle restrictions to attributes and declare their dependencies. Update context-key inspection in existing tests and cover separate pins, shared pins and deferred cleanup.
Replace lifecycle prose tags with method attributes, distinguishing receiver-owned configuration from shared resource mutation. Mark intentionally unsupported filesystem accessors and declare the attribute dependencies.

Update the active implementation plan with the completed package group and the context ownership corrections, retaining the remaining conversion and documentation work.
Convert broadcasting, bus, mail, queue, Horizon and pipeline lifetime restrictions to method attributes, with receiver conditions for instance-owned state. Preserve runtime recovery through pooled manager purges and exclusive borrowed-object configuration.

Mark the queue connection contract and after-commit dispatcher setter consistently, declare the attribute dependencies, and update queue package metadata coverage.
Replace console, scheduling, concurrency and process-factory lifetime tags with attributes. Shared mutex configuration remains restricted while process recording helpers retain their supported conditional runtime behavior.

Mark scheduled callback background execution as unsupported, declare the package dependencies and update console metadata coverage.
Mark process registration, server binding, signal listener setup and shared server configuration with lifecycle attributes. Keep ProcessManager running-state updates unmarked because both startup and shutdown legitimately own them.

Add the corresponding contract marker and package dependencies, preserving existing behavior and checking Reverb and WebSocket server metadata.
Leave protected provider implementation helpers unmarked when only startup hooks call them, while retaining markers on generally callable registration APIs. Preserve useful existing explanations on contracts and mark the inherited application environment resolver consistently.

Update the active plan with exclusive pool-borrow semantics, legitimate startup and shutdown calls, completed conversions and remaining work.
Replace database, schema, migration and Eloquent lifetime tags with method attributes. Preserve read-only morph-map calls and lazy model boot hooks, while marking event registration consistently across ordinary and soft-delete events.

Describe dispatcher and transaction-manager restrictions on reusable physical connections, mark retained unsupported database APIs, and declare the attribute dependency with metadata coverage. Runtime behavior and signatures are unchanged.
Convert search engine configuration and Scout callbacks to lifecycle attributes, applying receiver conditions to engine-owned state. Keep model boot registration available during lazy initialization.

Mark index creation methods that always reject calls, declare the attribute dependency and update package metadata coverage without changing engine behavior.
Replace lifecycle tags with attributes for data morph maps, pagination resolvers and permission configuration. Distinguish receiver-owned state from setters that update shared configuration and bindings.

Mark retained unsupported data APIs, declare package dependencies, and update the implementation plan with completed conversions and the remaining package groups.
Explore replacing the manual static-reset registry with discovered method attributes. Record discovery, ordering, inheritance and optional-package constraints while keeping test execution free of repeated scanning.
Replace lifetime prose tags with attributes across application bootstrap, exception handling, middleware configuration, console registration and Vite. Mark configuration wrappers and align the application contract with its implementation.

Limit instance-owned settings to shared receivers while retaining unconditional restrictions for global configuration. Keep lifecycle hooks and stable lazy initialization unrestricted, and declare the attributes dependency.
Convert shared HTTP factory, middleware, resource and routing configuration to lifetime attributes. Preserve runtime-safe reflection and routing cache resets without restrictions, and retain conditional getter behavior.

Mark retained unsupported HTTP and transport methods, including stream rewind paths that always reject seeking. Add direct attributes dependencies to each affected package without changing runtime behavior.
Convert DI registries, proxy generation, server initialization and test infrastructure to method attributes. Align server initialization with its contract and declare the package dependencies.

Leave aspect-chain and Eloquent caster cache clearing unrestricted because subsequent calls rebuild them from unchanged configuration. Update the implementation plan with these cache boundaries, lifecycle exclusions and remaining conversion work.
Allow Unsupported to carry an optional message while preserving bare markers. Supply verified alternatives for rejected cache, database, filesystem, HTTP, validation and transport methods.

Keep runtime exceptions unchanged. The metadata gives static analysis the guidance needed to direct callers to supported APIs without inspecting method bodies.
Convert AI manager configuration, fake gateways and provider setters to lifetime attributes with shared-receiver conditions. Mark MCP route registration and client configuration according to the state each method owns.

Describe unsupported provider and MCP content operations with useful guidance where an alternative exists. Declare attributes dependencies and update the implementation plan and approved guidance wording.
Qualify HTTP factory, cookie middleware, migration hook, pool callback and Redis configuration warnings for shared receivers. Independently constructed instances retain their own state, matching the conditional lifetime attributes.
Convert compiler, finder, head registry and Inertia configuration warnings to attributes, preserving receiver-specific restrictions and runtime-safe cache clearing.

Keep forwarding methods unmarked where caller-supplied dependencies determine the effect. Document shared dispatcher and registry behavior without treating every independently built object as application-global. Declare the attributes dependencies.
Mark shared loader and translator mutations, validation factory configuration and DNS test overrides. Preserve getter calls to validation defaults through conditional attributes and keep injected-loader forwarders unrestricted.

Declare the attributes dependencies and update validation package metadata coverage.
Convert process-wide prompt theme, cancellation, reset and test configuration to attributes while retaining runtime getter behavior.

Add supported alternatives to the unsupported prompt methods on progress, spinner, stream and task objects. Declare the attributes package and preserve coroutine-local prompt settings.
Remove unconditional restrictions from exception configuration and event registration wrappers whose handlers or dispatchers can be independently constructed. Retain the target mutators and static-target restrictions, with accurate shared-instance guidance.

Record the forwarding boundary and completed package conversions in the implementation plan.
…tributes

Replace prose lifetime tags across Fortify, JWT, Passkeys, Passport, Sanctum and Socialite with method attributes and declare their package dependency. Preserve conditional getter behavior for Passport settings and Fortify feature options, including the distinction between omitted and explicit null consent durations.

Restrict instance configuration only on shared receivers and mark the Socialite fake as test-only. Leave guard listener forwarders unmarked because the injected dispatcher owns the registration. Update the Passkeys dependency assertion alongside its manifest.
Convert lifetime tags in Doctor, gRPC, Image, Pennant, RateLimiter and Saloon to attributes, with direct dependencies and matching metadata assertions. Keep configuration writes and global descriptor registration unconditional, while owned instance registries are restricted only when shared.

Mark explicit Pennant definitions in both argument forms without restricting its guarded lazy registration callers. Preserve unknown dispatcher ownership for Connection::listen(), mark global mock cleanup as test-only, and add actionable Unsupported guidance for health watches and cursor paginator pooling.
Replace lifetime tags in OpenTelemetry, Sentry and Telescope with attributes that distinguish shared instance configuration from global registrations. Mark Telescope startup and listener registration, retain conditional ownership explanations for injected forwarders, and describe the exception recorder handoff precisely.

Declare the attributes dependency and complete Telescope direct-dependency assertions. Update the active plan to record the completed source conversion and Unsupported inventory, leaving guidance, documentation and final verification as the remaining work.
Explain the lifetime markers, argument conditions and Unsupported guidance in the static analysis documentation. Distinguish metadata and worker-state body exemptions from runtime enforcement and call-site checking, and link the existing worker-state section to the new examples.

Update the authoring guide to use attributes instead of prose tags, retain the requirement to explain runtime harm, and keep flushState cleanup hooks unmarked. Document the exclusions for intentionally unsupported methods and require useful alternative guidance when available.
Add the attributes split repository and Packagist registration to release preparation. Record the Channel capability investigation without changing the existing unsupported methods.

Mark the attributes implementation and verification complete in the active plan. Keep private consumer conversion dependent on the merge and the subsequent PHPStan rule work separate.
@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@binaryfire

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 12, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@binaryfire I have started the AI code review. It will take a few minutes to complete.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Informational

1. The static analysis package omits the new attributes dependency 🐞 Bug ⚙ Maintainability
Description
WorkerStateHelper now imports BootOnly, TestsOnly and BootOrTestsOnly from
hypervel/attributes, but src/phpstan/composer.json does not list hypervel/attributes in
require, though every other package that uses the attributes was updated. The package only gets
the attributes indirectly through container, contracts and support, so the PR's statement that
every package using an attribute declares the dependency is not true for this one, and a later
change to those packages' requirements could drop it without anyone noticing.
Code

src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php[R13-15]

+use Hypervel\Support\Attributes\BootOnly;
+use Hypervel\Support\Attributes\BootOrTestsOnly;
+use Hypervel\Support\Attributes\TestsOnly;
Evidence
WorkerStateHelper imports the three lifetime attributes and compares reflected attribute names
against them. The phpstan package's composer.json require block lists other internal packages at
^0.4 but has no hypervel/attributes entry. All other packages that use the attributes declare it.

src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php[13-15]
src/phpstan/composer.json[32-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/phpstan` imports `Hypervel\Support\Attributes\{BootOnly,TestsOnly,BootOrTestsOnly}` in `WorkerStateHelper`, but its composer.json does not require `hypervel/attributes`. Every other package that uses the attributes declares this dependency.
## Fix Focus Areas
- src/phpstan/composer.json[32-49]
## Recommended Fix
Add `"hypervel/attributes": "^0.4"` to the `require` section of `src/phpstan/composer.json`, in alphabetical order next to the other `hypervel/*` entries.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can open the rule or relationship behind a finding in one click

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 543 files

Confidence score: 4/5

  • GuardHelpers clones share the context prefix, so the clone and original can read and clear the same user state. Reset the context in __clone().
  • The lifetime attributes in Broadcaster.php, HasAttributes.php, SessionGuard.php, and AspectManager.php no longer match the intended boot/test restrictions, which can mislead worker-state analysis. Restore the appropriate attributes.
  • EncryptCookies::except() is marked boot-only, but MercureBroadcaster can call it at request time when its worker-cached instance is built lazily. Align the attribute with that runtime use.
  • BootOrTestsOnly accepts whenNotDefault and whenProvided together even though they are meant to be mutually exclusive. Reject that combination in the constructor.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/attributes/src/Attributes/BootOrTestsOnly.php">

<violation number="1" location="src/attributes/src/Attributes/BootOrTestsOnly.php:20">
P3: The constructor accepts `whenNotDefault` and `whenProvided` together, though the plan doc says they are mutually exclusive. Reject the combination in the constructor so an invalid attribute fails fast instead of being interpreted ambiguously by the analyzer.</violation>
</file>

<file name="src/ai/src/Contracts/Providers/RerankingProvider.php">

<violation number="1" location="src/ai/src/Contracts/Providers/RerankingProvider.php:29">
P3: The restriction on `useRerankingGateway()` has no explanation in this interface docblock. AGENTS.md asks for a concise docblock stating the runtime harm, so add the sentence from `HasRerankingGateway::useRerankingGateway()`: "Changing a shared provider's gateway affects subsequent operations."</violation>
</file>

<file name="src/broadcasting/src/Broadcasters/Broadcaster.php">

<violation number="1" location="src/broadcasting/src/Broadcasters/Broadcaster.php:100">
P2: `formatChannelsUsing()` and `authorizeChannelsUsing()` document a test-time reset by passing `null`, and the suite does this at BroadcasterTest.php:453 and :512. Plain `#[BootOnly]` marks that cleanup as boot-only. Use `#[BootOrTestsOnly]` on both methods so the marker matches the documented behavior.</violation>
</file>

<file name="src/cookie/src/Middleware/EncryptCookies.php">

<violation number="1" location="src/cookie/src/Middleware/EncryptCookies.php:202">
P2: `EncryptCookies::except()` is marked `#[BootOnly]`, but `MercureBroadcaster` calls it from its constructor on a lazily built, worker-cached broadcaster, which can run at request time. Either drop the attribute here, or document this path as a supported exception to the boot-only rule. The attribute is metadata for the worker-state analysis, so the runtime behavior is unchanged.</violation>
</file>

<file name="src/auth/src/GuardHelpers.php">

<violation number="1" location="src/auth/src/GuardHelpers.php:48">
P2: Cloning a guard leaves its context prefix shared with the original. The clone and the source then read and clear the same user state, which is the sharing this PR is meant to prevent. Add a `__clone()` that resets `contextKeyPrefix`, matching Container and ArrayStore. Guards with `readonly` context keys like SessionGuard will also need their keys recomputed.</violation>
</file>

<file name="src/database/src/Eloquent/Concerns/HasAttributes.php">

<violation number="1" location="src/database/src/Eloquent/Concerns/HasAttributes.php:1880">
P2: `flushCasterCache()` lost its replacement lifetime attribute. Add `#[BootOrTestsOnly]` and its import so reflection-based worker-state analysis preserves this boot/tests-only restriction.</violation>
</file>

<file name="src/auth/src/SessionGuard.php">

<violation number="1" location="src/auth/src/SessionGuard.php:710">
P2: `attempting()` lost its lifetime restriction when its docblock tag was removed. Add `#[BootOnly(whenShared: true)]` so worker-shared dispatcher registrations remain boot-only to the analyzer.</violation>
</file>

<file name="src/di/src/Aop/AspectManager.php">

<violation number="1" location="src/di/src/Aop/AspectManager.php:42">
P2: `clear()` loses its lifetime restriction when the docblock tag is removed without adding an attribute. Add `#[BootOrTestsOnly]` (and its import) so the worker-state analyzer continues to exempt its static cache reset.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

View guided diff | Turn on auto-fix | Re-trigger cubic

* The list persists in a static property for the worker lifetime and
* applies to every subsequent request.
*/
#[BootOnly]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: EncryptCookies::except() is marked #[BootOnly], but MercureBroadcaster calls it from its constructor on a lazily built, worker-cached broadcaster, which can run at request time. Either drop the attribute here, or document this path as a supported exception to the boot-only rule. The attribute is metadata for the worker-state analysis, so the runtime behavior is unchanged.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/cookie/src/Middleware/EncryptCookies.php, line 202:

<comment>`EncryptCookies::except()` is marked `#[BootOnly]`, but `MercureBroadcaster` calls it from its constructor on a lazily built, worker-cached broadcaster, which can run at request time. Either drop the attribute here, or document this path as a supported exception to the boot-only rule. The attribute is metadata for the worker-state analysis, so the runtime behavior is unchanged.</comment>

<file context>
@@ -194,9 +196,10 @@ public function isDisabled(string $name): bool
+     * The list persists in a static property for the worker lifetime and
+     * applies to every subsequent request.
      */
+    #[BootOnly]
     public static function except(array|string $cookies): void
     {
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as is. except() changes a list that every request shares, so calling it directly while handling a request is still a mistake, and the marker should say so.

The Mercure broadcaster is a deliberate exception. When it's first built, it exempts the hub's cookie from encryption. The cookie name comes from config, so every call adds the same entry, and except() drops duplicates. That's lazy registration of a fixed value, not request state leaking into the list.

* resolved them, so a guard created later with the same name, such as
* after AuthManager::forgetGuards(), starts without them.
*/
private ?string $contextKeyPrefix = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Cloning a guard leaves its context prefix shared with the original. The clone and the source then read and clear the same user state, which is the sharing this PR is meant to prevent. Add a __clone() that resets contextKeyPrefix, matching Container and ArrayStore. Guards with readonly context keys like SessionGuard will also need their keys recomputed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/auth/src/GuardHelpers.php, line 48:

<comment>Cloning a guard leaves its context prefix shared with the original. The clone and the source then read and clear the same user state, which is the sharing this PR is meant to prevent. Add a `__clone()` that resets `contextKeyPrefix`, matching Container and ArrayStore. Guards with `readonly` context keys like SessionGuard will also need their keys recomputed.</comment>

<file context>
@@ -28,6 +29,24 @@ trait GuardHelpers
+     * resolved them, so a guard created later with the same name, such as
+     * after AuthManager::forgetGuards(), starts without them.
+     */
+    private ?string $contextKeyPrefix = null;
+
     /**
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in the framework clones a guard, and none of the guards define __clone(), so no supported path reaches this. Before this change, a cloned guard also shared its state with the original, through the guard name, so this isn't a regression either. Leaving it as is.

*
* Boot or tests only. Clears the worker-wide caster instance cache shared
* by every coroutine; next attribute access rebuilds the casters.
* Clears the worker-wide caster instance cache shared by every coroutine;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: flushCasterCache() lost its replacement lifetime attribute. Add #[BootOrTestsOnly] and its import so reflection-based worker-state analysis preserves this boot/tests-only restriction.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/database/src/Eloquent/Concerns/HasAttributes.php, line 1880:

<comment>`flushCasterCache()` lost its replacement lifetime attribute. Add `#[BootOrTestsOnly]` and its import so reflection-based worker-state analysis preserves this boot/tests-only restriction.</comment>

<file context>
@@ -1875,8 +1877,8 @@ protected function resolveCasterClass(string $key): mixed
      *
-     * Boot or tests only. Clears the worker-wide caster instance cache shared
-     * by every coroutine; next attribute access rebuilds the casters.
+     * Clears the worker-wide caster instance cache shared by every coroutine;
+     * next attribute access rebuilds the casters.
      */
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. Clearing the caster cache is safe at runtime: the next attribute access rebuilds the casters from the same definitions. A marker would flag a call that does no harm, so the tag was replaced with a docblock that says what the method affects.

Comment on lines +710 to 712
* When the dispatcher is the application's worker-shared dispatcher,
* listener registrations persist and affect every subsequent request.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: attempting() lost its lifetime restriction when its docblock tag was removed. Add #[BootOnly(whenShared: true)] so worker-shared dispatcher registrations remain boot-only to the analyzer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/auth/src/SessionGuard.php, line 710:

<comment>`attempting()` lost its lifetime restriction when its docblock tag was removed. Add `#[BootOnly(whenShared: true)]` so worker-shared dispatcher registrations remain boot-only to the analyzer.</comment>

<file context>
@@ -705,8 +707,8 @@ protected function rehashUserPasswordForDeviceLogout(#[SensitiveParameter] strin
      *
-     * Boot-only. Listener registrations persist on the worker-lifetime dispatcher
-     * and affect every subsequent request.
+     * When the dispatcher is the application's worker-shared dispatcher,
+     * listener registrations persist and affect every subsequent request.
      */
</file context>
Suggested change
* When the dispatcher is the application's worker-shared dispatcher,
* listener registrations persist and affect every subsequent request.
*/
* When the dispatcher is the application's worker-shared dispatcher,
* listener registrations persist and affect every subsequent request.
*/
#[BootOnly(whenShared: true)]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. attempting() only forwards to the guard's event dispatcher, and the guard doesn't own that dispatcher: it's injected. Whether the listener ends up shared by the whole worker depends on which dispatcher was passed in.

The dispatcher's own listen() carries #[BootOnly(whenShared: true)], which is where that restriction belongs. The docblock here says when the registration affects every request.

*
* Boot or tests only. Clearing this worker-wide cache makes subsequent
* calls compile their chains from the current aspect rules.
* Clearing this worker-wide cache makes subsequent calls compile their

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: clear() loses its lifetime restriction when the docblock tag is removed without adding an attribute. Add #[BootOrTestsOnly] (and its import) so the worker-state analyzer continues to exempt its static cache reset.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/di/src/Aop/AspectManager.php, line 42:

<comment>`clear()` loses its lifetime restriction when the docblock tag is removed without adding an attribute. Add `#[BootOrTestsOnly]` (and its import) so the worker-state analyzer continues to exempt its static cache reset.</comment>

<file context>
@@ -39,8 +39,8 @@ public static function set(string $class, string $method, Closure $value): void
      *
-     * Boot or tests only. Clearing this worker-wide cache makes subsequent
-     * calls compile their chains from the current aspect rules.
+     * Clearing this worker-wide cache makes subsequent calls compile their
+     * chains from the current aspect rules.
      */
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional, for the same reason as the caster cache. Clearing the compiled aspect chains is safe at runtime: the next call compiles them again from the unchanged aspect rules, so there's nothing to restrict.

* @param bool $whenShared whether the restriction applies only to a worker-shared receiver
*/
public function __construct(
public ?string $whenNotDefault = null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The constructor accepts whenNotDefault and whenProvided together, though the plan doc says they are mutually exclusive. Reject the combination in the constructor so an invalid attribute fails fast instead of being interpreted ambiguously by the analyzer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/attributes/src/Attributes/BootOrTestsOnly.php, line 20:

<comment>The constructor accepts `whenNotDefault` and `whenProvided` together, though the plan doc says they are mutually exclusive. Reject the combination in the constructor so an invalid attribute fails fast instead of being interpreted ambiguously by the analyzer.</comment>

<file context>
@@ -0,0 +1,25 @@
+     * @param bool $whenShared whether the restriction applies only to a worker-shared receiver
+     */
+    public function __construct(
+        public ?string $whenNotDefault = null,
+        public ?string $whenProvided = null,
+        public bool $whenShared = false,
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A check here would never run. PHPStan reads attribute arguments straight from the source without calling the constructor, and nothing creates these attributes at runtime.

The follow-up that adds the call-site rules also adds a rule that reports a bad marker: both conditions set, a parameter that doesn't exist or has no default, or whenShared on a static method. That's where the check can actually catch something, so the constructor stays as is.

/**
* Set the provider's reranking gateway.
*/
#[BootOrTestsOnly(whenShared: true)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The restriction on useRerankingGateway() has no explanation in this interface docblock. AGENTS.md asks for a concise docblock stating the runtime harm, so add the sentence from HasRerankingGateway::useRerankingGateway(): "Changing a shared provider's gateway affects subsequent operations."

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/ai/src/Contracts/Providers/RerankingProvider.php, line 29:

<comment>The restriction on `useRerankingGateway()` has no explanation in this interface docblock. AGENTS.md asks for a concise docblock stating the runtime harm, so add the sentence from `HasRerankingGateway::useRerankingGateway()`: "Changing a shared provider's gateway affects subsequent operations."</comment>

<file context>
@@ -25,6 +26,7 @@ public function rerankingGateway(): RerankingGateway;
     /**
      * Set the provider's reranking gateway.
      */
+    #[BootOrTestsOnly(whenShared: true)]
     public function useRerankingGateway(RerankingGateway $gateway): self;
 
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. On contracts, we add the attribute and leave the explanation to the implementation, which is where the harm happens. All of the AI provider contracts follow that pattern. Repeating the implementation's sentence on each interface wouldn't add anything the attribute doesn't already say.

@greptile-apps

greptile-apps Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: plus

[High impact] The PR appears safe to merge; no blocking issues remain from this review.

Summary

Adds hypervel/attributes and replaces lifetime docblock tags with method attributes that PHPStan reads.

  • Method lifetime rules now use attributes that PHPStan can read.
  • Each container keeps its scoped state separate from other containers.
  • Each guard keeps its request state separate from same-named guards.
  • Cache managers and clones keep their context state separate.
  • Redis connection pins stay with their owning pool manager.
  • The queue facade can remove a job-pop callback by passing null.
  • The stream seek method now declares integer parameters.

Reviews (3) · Last reviewed commit: "Normalize numeric cache store names duri..." · Reviewed by Greptile

Comment thread src/cache/src/ArrayStore.php Resolved
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Replace lifetime docblock tags with method attributes

✨ Enhancement 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Adds the hypervel/attributes package with BootOnly, TestsOnly, BootOrTestsOnly and
 Unsupported, and uses them to replace the "Boot-only." / "Tests only." docblock tags across the
 framework.
• Worker-state PHPStan analysis now reads the attributes through reflection instead of matching
 docblock text with a regex.
• Fixes coroutine context keys that were shared between independent objects. Containers, auth
 guards, cache managers, cloned ArrayStores and Redis pool managers now each use their own key
 prefix.
• Small fixes: Queue::popUsing() accepts null, and Stream::seek() has native int types. Docs
 and AGENTS.md are updated.
Diagram

graph TD
 A["hypervel/attributes"] --> B["Framework packages"] --> C["PHPStan WorkerStateHelper"]
 B --> D["Container prefix"] --> E[("CoroutineContext")]
 B --> F["Guards, Cache, Redis keys"] --> E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Put attributes in hypervel/support
  • ➕ No new package or split repository
  • ➖ Packages such as container, contracts and collections don't depend on Support
  • ➖ PHPStan drops attributes with arguments when the class isn't installed
2. Split context-key fixes into a separate PR
  • ➕ Smaller diff per review
  • ➕ Easier to revert the behavioral changes on their own
  • ➖ More coordination, since the marker conversion exposed these bugs

Recommendation: The PR's approach is sound. A standalone package is needed because PHPStan drops attributes whose class isn't installed, and not every package depends on Support. The context-key fixes bundled in this PR are separable. They could have gone in their own PR to make review easier.

Files changed (25) +1309 / -439

Enhancement (9) +237 / -51
BootOnly.phpAdd BootOnly attribute +25/-0

Add BootOnly attribute

• New method attribute with optional whenNotDefault, whenProvided and whenShared conditions. It has no runtime effect.

src/attributes/src/Attributes/BootOnly.php

BootOrTestsOnly.phpAdd BootOrTestsOnly attribute +25/-0

Add BootOrTestsOnly attribute

• Method attribute for methods that are only safe at boot or in tests.

src/attributes/src/Attributes/BootOrTestsOnly.php

TestsOnly.phpAdd TestsOnly attribute +25/-0

Add TestsOnly attribute

• Method attribute for test-only methods.

src/attributes/src/Attributes/TestsOnly.php

Unsupported.phpAdd Unsupported attribute +21/-0

Add Unsupported attribute

• Marks retained public methods that always throw, with an optional message naming the supported alternative.

src/attributes/src/Attributes/Unsupported.php

WorkerStateHelper.phpDetect lifetime attributes via reflection +16/-8

Detect lifetime attributes via reflection

• Replaces the docblock regex with hasLifetimeAttribute(), which checks PHPStan attribute reflection.

src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php

WorkerStateClassCollector.phpUse attributes for constructor exemption +4/-2

Use attributes for constructor exemption

• Constructors marked with a lifetime attribute keep their promoted parameters, as they did with the tags.

src/phpstan/src/Collectors/WorkerStateClassCollector.php

Queue.phpQueue::popUsing accepts null; mark fakes +19/-1

Queue::popUsing accepts null; mark fakes

• popUsing() takes a nullable callback to match Worker::popUsing(). Fake methods are marked TestsOnly.

src/support/src/Facades/Queue.php

Application.phpConvert tags to attributes in Application +81/-40

Convert tags to attributes in Application

• Replaces lifetime docblock tags with attributes on application mutators.

src/foundation/src/Application.php

Container.phpMark container contract methods +21/-0

Mark container contract methods

• Adds matching attributes to the contract, since PHP attributes are not inherited.

src/contracts/src/Container/Container.php

Bug fix (8) +341 / -177
Container.phpPer-container context key prefix +139/-81

Per-container context key prefix

• Scoped instances and resolution state use a lazily allocated per-container prefix. The fixed-key constants are removed, and mutators get BootOnly(whenShared) markers.

src/container/src/Container.php

GuardHelpers.phpPer-guard context key prefix +30/-2

Per-guard context key prefix

• Adds a contextKey() helper with a unique per-instance prefix, so a replacement guard with the same name does not inherit the old guard's state.

src/auth/src/GuardHelpers.php

SessionGuard.phpSession guard uses instance-owned keys +18/-21

Session guard uses instance-owned keys

• Replaces the name-based context keys and removes getContextStateKey(). Adds attributes to the setters.

src/auth/src/SessionGuard.php

CacheManager.phpManager-owned memo context keys +60/-26

Manager-owned memo context keys

• memo() and forgetDriver() key memoized repositories by a per-manager prefix, so managers with the same store name no longer share or evict each other's memo repository.

src/cache/src/CacheManager.php

ArrayStore.phpGive cloned ArrayStore its own keys +20/-0

Give cloned ArrayStore its own keys

• Adds __clone(), which allocates new context keys and copies the current values and locks.

src/cache/src/ArrayStore.php

PoolManager.phpAdd per-manager contextKey() +32/-4

Add per-manager contextKey()

• Adds a unique prefix per pool manager, and marks purge methods BootOrTestsOnly.

src/redis/src/Pool/PoolManager.php

RedisProxy.phpKey pinned connections by pool manager +26/-19

Key pinned connections by pool manager

• Pinned connection and deferred-release keys now include the owning pool manager. The fixed prefix constants are removed.

src/redis/src/RedisProxy.php

JwtGuard.phpJWT guard uses instance-owned keys +16/-24

JWT guard uses instance-owned keys

• Removes GUARD_CONTEXT_KEY_PREFIX and getContextStateKey(), and uses the per-instance context key helper.

src/jwt/src/JwtGuard.php

Refactor (1) +9 / -11
Stream.phpNative int types on Stream::seek +9/-11

Native int types on Stream::seek

• seek() uses native int parameter types, matching PSR-7 v2.

src/engine/src/Http/Stream.php

Tests (3) +329 / -187
ContainerTest.phpContainer isolation regression tests +93/-8

Container isolation regression tests

• Covers independent containers, invalidation, cloning and child coroutines.

tests/Container/ContainerTest.php

RedisProxyTest.phpUpdate Redis proxy tests for new keys +188/-131

Update Redis proxy tests for new keys

• Adapts the tests to pool-manager-owned context keys.

tests/Redis/RedisProxyTest.php

WorkerStateRuleTest.phpUpdate worker-state rule tests +48/-48

Update worker-state rule tests

• Rule fixtures now use attributes instead of docblock tags.

tests/PHPStan/Rules/Coroutine/WorkerStateRuleTest.php

Documentation (2) +350 / -13
static-analysis.mdDocument method attributes +54/-13

Document method attributes

• Adds a Method Attributes section and replaces the docblock-tag guidance.

src/docs/static-analysis.md

2026-11-12-hypervel-attributes.mdAdd attributes design plan +296/-0

Add attributes design plan

• Planning document for the attributes package.

docs/plans/2026-11-12-hypervel-attributes.md

Other (2) +43 / -0
composer.jsonNew hypervel/attributes package manifest +41/-0

New hypervel/attributes package manifest

• Requires only PHP and autoloads the Hypervel\Support\ namespace.

src/attributes/composer.json

composer.jsonRegister hypervel/attributes in root composer +2/-0

Register hypervel/attributes in root composer

• Adds the new package to the root manifest.

composer.json

Make the PoolManager mock partial so RedisProxy can derive its instance-owned context keys while the test still controls pool borrowing. Preserve the existing assertions for command failures, event dispatch and connection release.
Copy tag sets onto the cloned store so flushing an array-backed tagged repository invalidates the copy without rotating tag identifiers in the original. Preserve concrete tag-set classes and their state without reconstructing their constructors.

Clone memoized tagged repositories through their delegate and reuse its store and tags. Clear the wrapper own memo before flushing because cloned wrappers are not registered with the memoized store. Cover independent writes, direct and fresh-tag reads after flush, and preservation of the original backing values.
Allow both boot configuration and test resets for the shared channel formatter and authorizer. Mark view extension registration when the factory is shared, retaining the explanation of its separate effect on the injected engine resolver.

Update the attributes plan to describe the final view restriction and tagged-cache clone ownership, and replace the completed reporting step with the remaining PR verification work.
PHP converts numeric string configuration keys to integers, so names obtained from array_keys must be normalized before deriving string-based memo context keys. Preserve enum normalization and the existing public signatures.

Exercise both ordinary string names and numeric names from configuration in the memo invalidation test. Verify that forgetting either name rebuilds the memoized repository and its underlying store.
@binaryfire
binaryfire merged commit d3d115e into 0.4 Nov 12, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant