diff --git a/AGENTS.md b/AGENTS.md index 37f009d9d8..2ad8957b28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,6 +215,7 @@ Build complete, long-term solutions, not MVPs or local workarounds. A broad chan - **Prefer union types over `mixed` when all types are known** — `mixed` is only for truly unconstrained values or cases that cannot be safely narrowed after control-flow analysis. - **Type decisions must be evidence-based** — check corresponding Laravel/Hyperf signatures and docblocks as a reference, then trace the real control flow through method bodies across all callers and callees to confirm the types are correct. - **Fail fast with framework and PHP exceptions** — don't add guards, wrapping, or defensive checks unless they handle a reachable invalid case or materially improve the error, and never swallow exceptions. If code would fail anyway (e.g. null passed to a typed parameter), let it fail naturally instead of adding a check that throws a custom exception — the stack trace is enough to diagnose. +- **Mark intentionally unsupported methods** — when a public method must be retained but intentionally rejects every call, mark it with `#[Unsupported]` from `Hypervel\Support\Attributes`, passing the supported alternative as its message when one exists. Exclude base defaults that supporting implementations override, extension hooks, deliberate control-flow throws, and conditional rejections. - **Use semantic column types in migrations** — `uuid()` / `foreignUuid()` / `uuidMorphs()` for UUIDs, `ipAddress()` / `macAddress()` for network addresses, and `jsonb()` for JSON the database queries, indexes or validates. Use `text()` for opaque serialized values that are only read back whole. PostgreSQL gets the real types (`jsonb`, `uuid`, `inet`, `macaddr`); the other supported databases fall back automatically to compatible types. - **No arbitrary string lengths** — use `->string('name')`, not `->string('name', 100)`. Don't invent limits; specify a length only when the domain or protocol defines one — exact (UUID: 36, ULID: 26, sha-256 hex token: 64) or a defined maximum (IPv6 address: 45). - **No database enums** — use string columns plus PHP enums. Adding a value to a database enum requires a migration. @@ -389,12 +390,12 @@ Decide where state lives before writing code: - **Name static cache properties for what they store** — not with a `Cache` suffix; static properties in Swoole workers are caches by nature. Exception: matching an existing Laravel-ported pattern in the same class (e.g. `$classCastCache`, `$attributeCastCache` on `HasAttributes`). - **Bound worker-lifetime lookup caches** — any internal lookup cache retained across requests in a worker—for example, in static properties or singleton instances—must have a naturally limited set of keys or discard entries that are safe to recompute. Do not add a size limit merely to hide growth from request- or user-derived keys. This governs framework-derived caches, not application-owned cache stores such as the `worker-array` driver. - **Review worker-lifetime state explicitly** — before introducing or modifying static properties/caches, singletons or other long-lived state, assess the Swoole persistence impact (memory leaks, cross-request behavior) and reach consensus on the design through a second-opinion loop with your peer agent. -- **Document worker-lifetime mutators** — when adding or touching a public method that mutates static state, singleton-held configuration, manager registries, cached drivers, global callbacks, or other worker-lifetime state, add a short warning to the method docblock if the method is intended only for boot-time configuration or tests. Use the tag-first format so humans and LLMs can recognize it quickly: - - `Boot-only.` — for startup configuration methods - - `Tests only.` — for test fakes, swaps, and resolver overrides - - `Boot or tests only.` — for cache/registry clearing methods used during boot reconfiguration or test cleanup +- **Mark restricted worker-state mutators** — when adding or changing a public mutator intended only for startup or tests, use an attribute from `Hypervel\Support\Attributes`: + - `#[BootOnly]` — startup configuration + - `#[TestsOnly]` — test fakes, swaps, and overrides + - `#[BootOrTestsOnly]` — configuration or cleanup allowed in either context - The second sentence should name the concrete failure mode, e.g. "The callback persists in a static property for the worker lifetime and affects every subsequent request." Do not add these warnings to methods that are genuinely safe for normal runtime/per-request use. If a method is commonly expected to be used dynamically but mutates shared worker-lifetime state, treat that as a coroutine-safety bug and reach consensus on the fix through a second-opinion loop with your peer agent. Never just document it. + Use `whenNotDefault: 'parameter'` only when the restriction applies to values differing from that parameter's declared default, `whenProvided: 'parameter'` when it applies whenever the argument is passed, and `whenShared: true` when it applies only to a worker-shared receiver. Keep a concise docblock explaining the runtime harm. If an API is intended for runtime use, fix unsafe shared-state behavior through a peer second opinion rather than restricting it to boot or tests. - **Assess static caching opportunities** — if a path repeatedly computes expensive stable metadata and worker-lifetime static caching would be a clear win, reach consensus with your peer agent through a second-opinion loop on what to cache, the expected benefit, bounded memory use, and correctness across requests before implementing. Classes that use static caching need a `flushState()` method for test cleanup — see "Static state and test cleanup" under Writing Tests. @@ -574,7 +575,7 @@ Use the standard title docblock for `flushState()` methods: */ ``` -Do not add `Boot-only.`, `Tests only.`, or `Boot or tests only.` warning paragraphs to `flushState()` docblocks. Those warnings belong on public mutators and registrars that userland might call incorrectly, not on this test cleanup hook that is only registered in `AfterEachTestSubscriber`. +Leave `flushState()` unmarked; PHPStan recognizes this cleanup hook by name. Keep the docblock to the title only — no extra paragraphs. If the method body has a non-obvious WHY worth explaining (ordering constraints, late-static-binding subtleties, etc.), put it as an inline comment above the relevant line inside the method, not as an extra paragraph under the title. diff --git a/composer.json b/composer.json index 741144d71c..2a67c99f60 100644 --- a/composer.json +++ b/composer.json @@ -154,6 +154,7 @@ "replace": { "hypervel/ai": "self.version", "hypervel/api-client": "self.version", + "hypervel/attributes": "self.version", "hypervel/auth": "self.version", "hypervel/broadcasting": "self.version", "hypervel/bus": "self.version", @@ -325,6 +326,7 @@ "Hypervel\\Signal\\": "src/signal/src/", "Hypervel\\Socialite\\": "src/socialite/src/", "Hypervel\\Support\\": [ + "src/attributes/src/", "src/collections/src/", "src/conditionable/src/", "src/macroable/src/", diff --git a/docs/plans/2026-11-12-hypervel-attributes.md b/docs/plans/2026-11-12-hypervel-attributes.md new file mode 100644 index 0000000000..85f9f749c9 --- /dev/null +++ b/docs/plans/2026-11-12-hypervel-attributes.md @@ -0,0 +1,298 @@ +# Hypervel Attributes (PR 1) + +Branch: `feature/hypervel-attributes` (from `0.4`). This PR is complete on its own: it adds the `hypervel/attributes` package, replaces the docblock lifetime tags (`Boot-only.`, `Tests only.`, `Boot or tests only.`) with attributes, adds `#[Unsupported]` markers, gives each container its own coroutine context keys (the container markers depend on it), and switches the existing PHPStan worker-state analysis to the attributes, so current analysis keeps working. The new call-site rules, the facade lifetime fix and the other PHPStan work are PR 2 (`docs/plans/2026-11-12-hypervel-phpstan-rules.md`), which builds on this PR after it merges. + +Owner-approved: the new package, and the `AGENTS.md` wording below (agreed with the peer reviewer). + +## Why + +- Docblock tags are prose. The worker-state helper matches them with a regex (`WorkerStateHelper::LIFETIME_TAG_PATTERN`, which also matches conditional prose such as "Boot-only when setting"), and PR 2's call-site rules need exact, machine-readable metadata, including the conditions under which a call is restricted. +- PHPStan's `AttributeReflectionFactory::fromNameAndArgumentExpressions()` drops an attribute that has arguments when the attribute class isn't installed. The attributes must therefore live in a real package that every using package declares (AGENTS "Declare only real dependencies"). That's why they don't go in `hypervel/support`, which several marked packages (`collections`, `container`, `contracts`, `core`, `coroutine`, `macroable`, `prompts`) don't require. +- Laravel precedent: `illuminate/conditionable` and `illuminate/macroable` are separate tiny packages sharing the `Illuminate\Support` namespace. `hypervel/conditionable` and `hypervel/macroable` already follow this with `Hypervel\Support\`. + +## 1. The `hypervel/attributes` package + +`src/attributes/`: + +- `composer.json`: copy `src/conditionable/composer.json` and change the name to `hypervel/attributes`, the description to "The Hypervel Attributes package.", and the keywords to `php`, `attributes`, `hypervel`. Keep `"php": "^8.4"` as the only requirement, and keep PSR-4 `"Hypervel\\Support\\": "src/"`. +- `LICENSE.md`: `cp src/api-client/LICENSE.md` (a Hypervel-original MIT license). +- `README.md`: + ``` + Attributes for Hypervel + === + + [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/attributes) + + Documentation: https://hypervel.org/docs/static-analysis#method-attributes + ``` +- `src/Attributes/BootOnly.php`, `TestsOnly.php` and `BootOrTestsOnly.php` are identical apart from the class name. They stay open (AGENTS "Don't make classes final by default"; no invariant needs `final`), with no class docblock: + ```php + globalOptions(...)`. Adjust prose that claims a worker-lifetime effect for every instance, e.g. "persists on the shared factory for the worker lifetime". +- **external:** stays unconditional, even on instance methods. Examples: `Router::resourceParameters()` writes `ResourceRegistrar`'s statics, and manager purges close shared pools. These instance methods are unconditional although the script sees only `$this` writes, because they change state other objects own: `DatabaseManager::purge()`/`whenQueryingForLongerThan()`; the database, Redis and object-pool `PoolManager` purges; `RedisManager::purge()`/`enableEvents()`/`disableEvents()`; `ConfigMutationTracker::observe()`/`applyAndRecord()`/`replay()`; `MercureBroadcaster::setHub()`; Mcp `Registrar::web()`; `PoolRecycler::start()`/`stop()`; `QueueManager::setApplication()`; `Router::setRoutes()`/`setCompiledRoutes()`/`compileAndWarm()`; `Route::flushController()`; `BladeCompiler::anonymousComponentPath()`; Sentry `HttpClientIntegration::onBoot()`. +- **none:** review it, and either correct the tag (the method isn't a mutator, so drop the attribute and the tag) or document the delegated effect. +- **Safe at runtime:** a tagged method whose runtime use is supported, such as clearing a cache that is rebuilt on demand, gets no attribute. Its docblock keeps the effect sentences without the tag (see the view and pooled-manager rows below). Examples: the routing reflection, signature, enum and middleware-sort cache flushes, `ControllerDispatcher::warmReflection()` (the same fill dispatch does lazily), `HasAttributes::flushCasterCache()` and `AspectManager::clear()` (chains recompile from unchanged `AspectCollector` rules and resolve aspects at invocation). +- **Condition not expressible:** when the restriction depends on something the bounded arguments can't name, such as which name a call passes, the method gets no attribute and keeps accurate prose. Never invent an unconditional restriction. `Process\Factory::recordIfRecording()` stays unmarked: it calls `record()` only after `fake()`. `ProcessManager::setRunning()` stays unmarked: server startup and SIGTERM shutdown both own it. +- **Reads:** a getter returning a shared object gets no attribute. `SaloonManager::middleware()` only returns the pipeline; its prose says that changing the returned pipeline affects later requests. +- **Forwarders:** a method forwarding to a static or application-global target is unconditional. One forwarding to a nested object gets `whenShared: true` only where the source shows the receiver owns that object. `View\Factory` owns its finder: `view.finder` is bound transient, so each factory builds its own, and `__clone()` clones it (`Mail\Markdown` relies on that for render-time `replaceNamespace()`). Holding an object in a property doesn't prove ownership. Otherwise the target's lifetime decides, so the method gets no attribute and its docblock says so (`NullDispatcher`, which `HasEvents` wraps around the shared dispatcher, `EventFake`, `BusFake`, the session decorator, the Sentry filesystem decorator), e.g. "This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher." `DatabaseManager::extend()`/`forgetExtension()` are unconditional: pools resolve `db.factory` from the container, so extensions only work on the application's factory. A constructor-injected target is unknown even when the application binds it as a singleton, since independently built instances are supported: `Translator`'s loader, `HeadManager`'s registry, the event dispatcher of the View factory, `Log\Logger`, the context `Repository`, `Router::matched()`, the guards' `attempting()` and `Connection::listen()`, and the handler of `Configuration\Exceptions` (and so Sentry's `Integration::handles()`). Those forwarders stay unmarked; the targets' own mutators carry their markers (`FileLoader`/`Loader`, `TagRegistry`, `Dispatcher`, `Handler`). A target verified as worker-shared and fetched from the application or a static, in the method or the constructor, stays restricted (`BladeCompiler::anonymousComponentPath()`, `Schedule::useCache()`). +- **Direct wrappers:** a method that directly calls a marked method gets the same marker and its own harm sentence, whether or not it was tagged and whatever its visibility. Classify it by its lifecycle contract; a runtime guard that already throws is no reason to omit the marker. Examples: `Lottery::determineResultsNormally()`, the facade `fake*()` methods, `Facade::createFreshMockInstance()`, `Facades\Queue::popUsing()`, `ServiceProvider::commands()`/`aspects()`/`classMap()`/`mergeConfigFrom()`/`replaceConfigRecursivelyFrom()`, `Socialite::fake()`, `AuthManager::viaRequest()`, `GrpcRouter::syncMiddlewareFrom()`, `AbstractProcess::bind()`, `Application::register()`/`configure()`, and every `ApplicationBuilder` and `Configuration\Middleware` method that registers on the application or a middleware's static settings. Constructors, startup/shutdown lifecycle hooks (provider `register()`/`boot()`, application bootstrappers, server boot listeners and shutdown handlers), their protected/private implementation helpers (provider `register*()` helpers, Horizon's `authorization()`/`gate()`/`configure()`) and lifecycle-hook bases (Sentry `Feature::onBoot()`, Telescope `Watcher::register()`) stay unmarked unless tagged. Test-case methods and traits, PHPUnit extensions and subscribers, and Testbench only run in tests and also stay unmarked unless tagged. This does not exempt request/job/operation listeners or generally callable registration APIs. +- **Supported lazy registration:** a runtime path that registers stable definitions on first use isn't a wrapper and stays unmarked: `Container::getConcreteBindingFromAttributes()` (`#[Bind]` attributes), the `Date` facade's default install (replaced by PR 2's `date` binding), Pennant's `resolveFeature()` → `ensureDynamicFeatureIsDefined()` → `define()` (`pennant.md` promises class features need no boot registration), `MercureBroadcaster::exemptCookieFromEncryption()` (run by the lazily built broadcaster's constructor; `EncryptCookies::except()` deduplicates the configured cookie name), Eloquent model and trait boot hooks (`booted()`, `bootHasGlobalScopes()`, `bootSoftDeletes()`, Scout's `bootSearchable()`), which register scopes, observers and macros once per model class on first boot, and `Application::configurationIsCached()`/`routesAreCached()`/`eventsAreCached()`, which bind a stable flag on first call. Never split out a duplicate internal write to get around a marker. Pennant's `Decorator::define()` and `discover()` are `#[BootOrTestsOnly(whenShared: true)]`: a call in either form can replace a definition (a one-argument call re-reads the class's name), and `discover()` adds every discovered class to `all()`. The unmarked lazy callers check the existing definitions before calling `define()`. +- **Fresh objects:** a `whenShared` setter that framework code also calls on an object it has just built stays marked; PR 2's receiver analysis treats such locals as non-shared. Examples: `CreatesUserProviders::createEloquentProvider()` → `EloquentUserProvider::enableCache()` (its worker-wide invalidation registration is deduplicated, so `enableCache()`/`setModel()` are `whenShared`), `CacheManager::createRedisDriver()` → `RedisStore::setTagMode()`/`setLockConnection()`, `FilesystemManager`'s lazy disk creation → `shouldServeSignedUrls()`/`servingRoute()`. Pool proxies set `whenShared` state on exclusively borrowed objects (`BroadcastPoolProxy`/`QueuePoolProxy::configureBorrowed()`, the queue proxy's release callback); those object parameters carry no shared-receiver evidence. `Connection`'s dispatcher and transaction-manager setters and unsetters are `#[TestsOnly(whenShared: true)]`: a reusable physical connection keeps them for later borrowers (`resetForPool()` doesn't reset them), while the manager and pool set them on connections they own (`PooledConnection::newLease()`/`attachLease()`). `Redis\Console\BenchmarkCommand::runSuite()` changing the cached store's tag mode, and the Horizon and watcher restart strategies calling `DotenvManager::reload()` in their supervising CLI process, are PR 2 context-test cases, not reasons for a console exemption. +- **Properties:** attributes target methods only. The property tags (Queue `Worker` ×7, `Http\Client\Response::$defaultJsonDecodingFlags`, Sentry ×2) lose the tag and keep corrected prose, e.g. "Setting this mutates…". +- **Contracts and their implementations.** Attributes aren't inherited, so check them as a set (for example `Config\Repository::set()`/`prepend()`/`push()`, `Http\Kernel` middleware mutators, `Translator::setFallback()`, `ObjectPool\Factory::purgeAll()`, `Recycler::start()`/`stop()`, `CoroutineInterface::set()`/`getContextFor()`, `Image\Driver::transformUsing()`, `ModelIdentifier::useMorphMap()`): + - every implementation of a marked contract method carries the same attribute unless its own behavior differs (the queue fakes' no-op `setConnectionName()` stays unmarked). Include inherited implementations: a parent that doesn't implement the interface can supply the method (`Queue::setConnectionName()` for `Contracts\Queue\Queue`, `Container::resolveEnvironmentUsing()` for `Contracts\Foundation\Application`); + - a marked implementation whose contract method is unmarked is reviewed the same way, and the contract method gets the implementation's attribute (for example `ServerInterface::init()` → `#[BootOnly]`, Foundation `Application::flush()` → `#[TestsOnly]`, `Contracts\View\Factory::addNamespace()`/`replaceNamespace()` → `#[BootOnly(whenShared: true)]`); + - a contract method that gains an attribute gets no new prose; the implementation owns the harm explanation, unless the restriction can't be understood from the signature and attribute. Explanations a contract already had stay. +- **Constructors.** Tagged constructors (only the test fixture today) get the attribute the same way. +- **Bulk edits.** Do the mechanical conversion with the script, as text edits from the bottom of each file up, then review the complete diff (CLAUDE.md bulk-edit rule; this plan is the agreed replacement pattern). Hand-edit the conditional methods below. + +**Conditional tags (36):** + +| Method(s) | Result | +|---|---| +| `Relation::morphMap(?array $map = null, bool $merge = true)` | `#[BootOnly(whenNotDefault: 'map')]` | +| `ResourceRegistrar::verbs(array $verbs = [])`, `Router::resourceVerbs(array $verbs = [])` | `#[BootOnly(whenNotDefault: 'verbs')]` (`Router` writes `ResourceRegistrar`'s statics, so there's no `whenShared`) | +| Fortify `Features::twoFactorAuthentication()`, `passkeys()` (`array $options = []`) | `#[BootOnly(whenNotDefault: 'options')]` | +| `Passport::defaultScopes()` | `#[BootOnly(whenNotDefault: 'scopes')]` | +| `Passport::tokensExpireIn()`, `refreshTokensExpireIn()`, `personalAccessTokensExpireIn()`, `clientCredentialsTokensExpireIn()`, `dpopTokensExpireIn()` | `#[BootOnly(whenNotDefault: 'date')]` | +| `Passport::cookie()` | `#[BootOnly(whenNotDefault: 'cookie')]` | +| `Passport::rememberConsentFor()` | `#[BootOnly(whenProvided: 'duration')]`. It branches on `func_num_args()`, so an explicit `null` clears the duration; no runtime change. | +| `Prompts\Concerns\Themes::theme()` | `#[BootOnly(whenNotDefault: 'name')]` | +| Validation `Email::defaults()`, `Password::defaults()`, `File::defaults()` | `#[BootOnly(whenNotDefault: 'callback')]` | +| `Str::freezeUuids()`, `Str::freezeUlids()` | `#[TestsOnly]`. Replace "Tests only unless a callback is supplied" prose: both forms install a worker-wide factory (the callback form only until it returns) and aren't coroutine-safe. No runtime caller exists. | +| `View\Component::flushCache()`, `CompilerEngine::forgetCompiledOrNotExpired()` | No attribute: clearing is safe at runtime (`view:clear` does it), since the next render or compile check rebuilds the cache. Replace the tag sentence; keep the effect sentence and "The view:clear command owns runtime invalidation." | +| `View\Factory::flushFinderCache()`, `FileViewFinder::flush()` (unconditional tags) | No attribute: `find()` rebuilds the cleared `$views` from unchanged paths, hints and extensions. Remove the stale warning. `ViewFinderInterface` gets `#[BootOnly(whenShared: true)]` on its other six methods and leaves `flush()` unmarked; `Contracts\View\Factory::flushFinderCache()` stays unmarked. | +| `View\Factory::addLocation()`, `prependLocation()`, `addNamespace()`, `prependNamespace()`, `replaceNamespace()`, `addExtension()` (unconditional tags) | `#[BootOnly(whenShared: true)]`: they change the factory's own finder and, for `addExtension()`, its extension map (see Forwarders). Prose names "the shared factory's finder". `addExtension()`'s resolver argument is forwarded to the injected engine resolver, whose `register()` carries its own marker; its prose explains that clones share that resolver. | +| `AiManager::forgetInstance()`, `AiManager::purge()` | No attribute: forgetting an on-demand provider only clears `CoroutineContext`, while forgetting a configured one affects the worker, and the marker can't express which name a call passes. Replace the tag sentence with "Forgetting a configured provider affects every request on the worker; on-demand providers belong to the current operation." The inherited `MultipleInstanceManager` methods keep their attribute. | +| `Ai\Providers\Concerns\Has{Text,Image,Audio,Transcription,Embedding,Reranking,File,Store,Classification}Gateway::use*Gateway()` | `#[BootOrTestsOnly(whenShared: true)]`: configured providers are shared, while providers built per request are not. | +| `BroadcastManager::purge()`, `QueueManager::purge()`, `MailManager::purge()`, `FilesystemManager::purge()` | No attribute. Their documented contract (`queues.md:189`, `mail.md:362`, `filesystem.md:335`) is safe runtime recovery: other users transparently acquire a fresh pool. Replace the tag sentence with "Use this to recover broken pooled resources." and keep the effect sentences. `DatabaseManager`, `RedisManager`, `CacheManager` and `MultipleInstanceManager` purges keep `#[BootOrTestsOnly]`, since their documented effects on concurrent coroutines are unsafe. | + +**Container context keys.** The container markers rely on each container owning its coroutine context entries. Scoped instances (`'__container.scoped.' . $abstract`) and resolution state (`'__container.resolution_state'`) used fixed keys, so a second container returned, reported, replaced and cleared the first's scoped instances, and resolving through another container inside a binding threw `CircularDependencyException`. Fix in `Container`: +- `contextKey(string $name)` returns `$this->contextKeyPrefix ??= '__container.' . ++self::$contextKeyPrefixCount . '.'` plus the name: `'scoped.' . $abstract` or `'resolution_state'`. The two key constants go. The counter is never reset, including by `flushState()`, so prefixes are never reused. Not `spl_object_id()`, which is reused after GC, and no registry or shared map. Values stay direct context entries, so fork copying (`ReplicableContext`/`NonCopyableContext`) is unchanged. +- `__clone()`: the clone takes a new prefix and copies the current coroutine's resolved scoped instances, found through `$scopedInstances`, into its own entries, like its shallow-copied singleton arrays. Its resolution state starts empty. +- Tests: `ContainerTest` covers two containers isolated for `make()`/`resolvedScoped()`/`isScopedInstance()`; invalidation through `forgetInstance()`, `forgetInstances()`, `forgetScopedInstances()`, `bind()`, `instance()` and `extend()` against a sibling's populated entry; nested cross-container resolution; a clone's starting instances, independent invalidation, and a clone taken before resolving. `CoroutineSafetyTest` covers a forked child inheriting scoped instances. The inspection stubs use `getResolutionState()`, and the transient-lifetime test uses `resolvedScoped()`. +- Markers: the binding and instance-cache group keeps `whenShared: true`; `forgetScopedInstances()` becomes `#[BootOrTestsOnly(whenShared: true)]`. + +**CacheManager memo keys.** `memo()` stored its repository at `'__cache.memoized.' . $driver`, so independent managers with the same store name shared one memoized repository and `forgetDriver()` evicted another manager's memo. Each manager now takes a prefix lazily from a never-reset static sequence (lazy, so constructor-less partial mocks still work), used by `memo()` and `forgetDriver()`. Laravel binds the memo on the manager's container; per-manager keys follow ownership, since the memo wraps that manager's stores. `forgetDriver()` keeps `whenShared: true`. `CacheManagerTest::testMemoizedRepositoriesBelongToTheirManager` covers values and `forgetDriver()` isolation. + +**Redis pinned-connection keys.** `RedisProxy` kept pinned connections and deferred-release owners at `'__redis.connection.' . $pool` and `'__redis.deferred_release_owner.' . $pool`, so proxies on different `PoolManager`s with the same connection name shared one pin, and releasing or discarding through one ended the other's. The internal `PoolManager::contextKey()` now builds both keys from a lazy, never-reset per-manager prefix, with role-prefixed names (`connection.`, `deferred_release_owner.`) so the two can't collide. Each proxy caches its two keys. Proxies for the same manager and name still share a pin. `CONNECTION_CONTEXT_PREFIX` goes, and tests read keys through the proxy. `RedisProxyTest` covers separate pins and discard isolation across managers, shared pins on one manager, and deferred cleanup releasing both managers' connections. + +**Guard context keys.** All six guards (`SessionGuard`, `RequestGuard`, auth `TokenGuard`, `JwtGuard`, `SanctumGuard`, Passport `TokenGuard`) kept per-request users, credential caches and state at `"__auth.guards.{$name}…"`. A guard created later with the same name, such as after `AuthManager::forgetGuards()`, therefore adopted the previous guard's explicit user and client and its unseeded caches, including entries the test HTTP sync copies back. +- `GuardHelpers::contextKey()` now builds every guard key from a lazy prefix: the using class plus that class's own never-reset sequence. Trait statics are separate per using class, so the class keeps prefixes unique. +- `SessionGuard` keeps its readonly keys. +- The `SessionGuard`/`JwtGuard` `getContextStateKey()` wrappers and `JwtGuard::GUARD_CONTEXT_KEY_PREFIX` go. +- Token hashing, session keys, remember cookies, logout and persisted credentials are unchanged, so a replacement guard still reauthenticates from a valid session. +- Tests: + - each guard suite covers a same-name replacement: explicit users, Passport's explicit client, the request and bearer caches, and JWT token state; + - `InteractsWithAuthenticationTest` covers `forgetGuards()` dropping an `actingAs()` user through the HTTP sync, and reloading the user from a valid session. + +**ArrayStore cloning.** `Cache\Repository::__clone()` clones its store, but `ArrayStore` currently copies its context keys unchanged, so writes through the clone change the original's values and locks. Add `ArrayStore::__clone()` to allocate fresh keys from the existing sequence and shallow-copy the current context's storage and lock records. Keep the readonly key properties; PHP supports reinitializing them during cloning. Cover initial values and independent subsequent value and lock mutations through a cloned repository. Check other context-key holders only where their supported clone paths require the same isolation. + +Tagged caches inherit `Repository::__clone()`, so a cloned tagged cache must keep its tag set on its cloned store. `TagSet::withStore()` returns a copy of the set on another store, keeping its class and names, and `TaggedCache::__clone()` rebinds its tags with it. `MemoizedTaggedCache::__clone()` clones its delegate and takes the delegate's store and tag set, rather than cloning the store a second time. Its `flush()` clears its own memo first, since a clone isn't registered with the `MemoizedStore`. `CacheTaggedCacheTest` and `CacheMemoizedStoreTest` cover flushing a clone without affecting the original. + +**Check.** After converting, `grep -rnE '\* (Boot-only|Tests only|Boot or tests only)\b' src tests` returns nothing. Historical `docs/plans/*` are immutable and keep their text. + +## 3. `#[Unsupported]` markers + +**Criteria.** Mark a retained public method that rejects every call and is unsupported for every instance of its declaring class. + +Excluded: +- base defaults that supporting implementations override (Query and Schema grammar `compile*` defaults, `Schema\Builder::dropAll*()`, the `Connection` driver hooks, `Lock::refresh()`/`getRemainingLifetime()`); +- extension hooks (`Prunable::prunable()`, `MassPrunable::prunable()`); +- deliberate control-flow throws (`ResponseTrait::throwResponse()`, `SocialiteManager::getDefaultDriver()`); +- conditional rejections; +- test fakes (`FakeTransporter::run()`); +- `offsetSet()`/`offsetUnset()` on read-only `ArrayAccess`, which is reached through `[]=` syntax. + +Engine `Channel::hasProducers()`/`hasConsumers()`/`isReadable()`/`isWritable()` are excluded by owner direction (section 6 TODO). + +**Mark these 72 methods.** Each keeps its existing throw and message; check each body is still an unconditional throw. Also mark retained public methods that always reach one of them, such as `Engine\Http\Stream::rewind()` (→ `seek(0)`), unless a supported override resolves to its own declaration. Where a marked PSR-7 method lacks native parameter types, use the PSR-7 v2 types (`Stream::seek(int $offset, int $whence = SEEK_SET)`); check `CurlStreamingBody::seek()` the same way. + +**Messages.** Pass guidance as the message when source or documentation establishes a supported alternative or behavior: one short imperative sentence, without repeating that the method is unsupported, reusing the exception's guidance where it's accurate. Never invent a replacement; leave the marker bare otherwise (engine limits, provider capabilities, stream seeking, session bags, scoped filesystem internals). Established alternatives include returning responses rather than calling `Response::send*()` (`responses.md`), `request()` or injection rather than `Request::createFromGlobals()` (`requests.md`), and the typed constructors of the Data validation attributes. Raw cache reads are `@internal`, so `AnyModeTaggedCache` raw methods point at `Cache::get()`/`many()`. Remaining packages: +- grpc `HealthService::watch()`: "Use check() to poll the health status." +- Saloon `CursorPaginator::pool()`: "Iterate over the paginator sequentially; each cursor depends on the previous response." +- Prompts `prompt()`: `Spinner` "Use spin() instead.", `Progress` "Use map(), or start(), advance() and finish().", `Task` "Use run() instead.", `Stream` "Use append() and close() instead." + +- `Session\SymfonySessionDecorator`: `registerBag()`, `getBag()`, `getMetadataBag()` +- `Cache\AnyModeTaggedCache`: `get()`, `getRaw()`, `getAuthoritativeRaw()`, `many()`, `manyRaw()`, `has()`, `pull()`, `forget()`, `touch()` +- `Database\DatabaseManager`: `build()`, `connectUsing()`. `Database\Schema\ColumnDefinition::whereNotNull()`. `Query\Grammars\MariaDbGrammar::compileJoinLateral()`. `Schema\Grammars\SQLiteGrammar`: `compileSpatialIndex()`, `compileDropSpatialIndex()` +- `Filesystem\Concerns\InteractsWithPooledFilesystem`: `getDriver()`, `getAdapter()`, `getClient()`. `Filesystem\ScopedFilesystemProxy`: `getDriver()`, `getAdapter()`, `getClient()`, `serveUsing()`, `buildTemporaryUrlsUsing()`, `buildTemporaryUploadUrlsUsing()`. Keep one truthful shared `getClient()` message; don't move or duplicate the method. +- Scout `AlgoliaEngine`, `TypesenseEngine`, `TurbopufferEngine`: `createIndex()` +- `Grpc\Health\HealthService::watch()`. Saloon `Pagination\CursorPaginator::pool()`. `Engine\Http\Stream`: `tell()`, `seek()`. `ConnectionPool\KeepaliveConnection::getConnection()` +- `Support\Str::flushCache()` +- Prompts `Stream`, `Progress`, `Spinner`, `Task`: `prompt()` +- `Console\Scheduling\CallbackEvent::runInBackground()`. ApiClient `PendingRequest::setClient()`. The HTTP client's `PendingRequest::setClient()` is supported and stays unmarked. +- `Http\Response`: `send()`, `sendHeaders()`, `sendContent()`. `Http\Request::createFromGlobals()`. `Http\Resources\DelegatesToResource`: `resolveRouteBinding()`, `resolveChildRouteBinding()`. `Http\Resources\JsonApi\JsonApiResource`: `wrap()`, `withoutWrapping()`. `Http\Client\CurlStreamingBody`: `seek()`, `write()` +- Mcp `Server\Content\ResourceLink::toResource()`, `Notification::toTool()`/`toPrompt()`/`toResource()`, `Blob::toTool()`/`toPrompt()` +- Ai `Gateway\Anthropic\AnthropicGateway` and `Gateway\Xai\XaiGateway`: `generateImage()`, `generateAudio()`, `generateTranscription()`, `generateEmbeddings()`. `Files\S3Document::content()`, `Files\ProviderDocument::content()` +- Data `Concerns\PaginatedDataCollectable::withoutWrapping()`. `Attributes\Validation\AnyOf::create()`, `Can::create()`, `Password::create()` + +## 4. PHPStan: read attributes instead of tags + +`src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php`: +- Remove `LIFETIME_TAG_PATTERN` and `hasLifetimeTag()`. +- Add the attribute list, imported from `Hypervel\Support\Attributes`, and the method: + ```php + /** + * The attributes that restrict a method to boot time or tests. + */ + private const array LIFETIME_ATTRIBUTES = [BootOnly::class, TestsOnly::class, BootOrTestsOnly::class]; + + /** + * Determine whether the function is marked as running only while the application boots or in tests. + */ + public function hasLifetimeAttribute(FunctionReflection|ExtendedMethodReflection $function): bool + { + return array_any( + $function->getAttributes(), + static fn (AttributeReflection $attribute): bool => in_array($attribute->getName(), self::LIFETIME_ATTRIBUTES, true), + ); + } + ``` +- `getFunctionFacts()` calls `hasLifetimeAttribute($function)` in place of the docblock check, and its docblock says "marked" instead of "documented". +- The exemption deliberately ignores the attribute arguments. Conditional methods' writes are their restricted setter writes, which the inventory verifies, as the tag exemption did before. +- `Scope::getFunction()` returns `?PhpFunctionFromParserNodeReflection`, which implements `FunctionReflection`; its method subclass implements `ExtendedMethodReflection`. Both declare `getAttributes()`. + +`src/phpstan/src/Collectors/WorkerStateClassCollector.php:66`: a constructor marked with a lifetime attribute keeps its parameters by design. Read it through `$class->getNativeMethod('__construct')->getAttributes()` when the class declares its own constructor (the AST `getMethod('__construct')` check already establishes that), using `hasLifetimeAttribute()`. Update the comment to "A constructor marked as boot-time or test-only keeps its parameters by design." + +Tests, in `tests/PHPStan/Rules/Coroutine/data/worker-state.php` and `WorkerStateRuleTest`: +- Convert the fixture tags to attributes, keeping their expected results: + - `setRequestForTesting()` (line 105) → `#[TestsOnly]`; + - `TestingRequestHolder::__construct()` (line 205) → `#[TestsOnly]`; + - `configure()` (line 727) → `#[BootOnly]`. +- Add a `#[TestsOnly(whenNotDefault: 'request')]` method on the fixture's `SharedService` that stores its `?Request $request = null` parameter in `$this->request`. It isn't reported, while the existing unmarked `remember()` write to the same property is reported as `hypervel.workerSharedState`. This proves attributes with arguments are read. During implementation, confirm that removing the attribute makes the diagnostic appear. +- Run `vendor/bin/phpunit -c phpunit.phpstan.xml.dist --no-progress tests/PHPStan/Rules/Coroutine/WorkerStateRuleTest.php` immediately. + +## 5. Guidance and documentation + +`AGENTS.md` (agreed wording): + +1. Replace the "Document worker-lifetime mutators" bullet, lines 392–397, with: + > - **Mark restricted worker-state mutators** — when adding or changing a public mutator intended only for startup or tests, use an attribute from `Hypervel\Support\Attributes`: + > - `#[BootOnly]` — startup configuration + > - `#[TestsOnly]` — test fakes, swaps, and overrides + > - `#[BootOrTestsOnly]` — configuration or cleanup allowed in either context + > + > Use `whenNotDefault: 'parameter'` only when the restriction applies to values differing from that parameter's declared default, `whenProvided: 'parameter'` when it applies whenever the argument is passed, and `whenShared: true` when it applies only to a worker-shared receiver. Keep a concise docblock explaining the runtime harm. If an API is intended for runtime use, fix unsafe shared-state behavior through a peer second opinion rather than restricting it to boot or tests. +2. Replace the `flushState()` paragraph at line 577 ("Do not add `Boot-only.`, …") with: + > Leave `flushState()` unmarked; PHPStan recognizes this cleanup hook by name. +3. Insert after the "Fail fast" Code conventions bullet (line 217): + > - **Mark intentionally unsupported methods** — when a public method must be retained but intentionally rejects every call, mark it with `#[Unsupported]` from `Hypervel\Support\Attributes`, passing the supported alternative as its message when one exists. Exclude base defaults that supporting implementations override, extension hooks, deliberate control-flow throws, and conditional rejections. + +`src/docs/static-analysis.md`, in Laravel-docs prose: +- New `### Method Attributes` section (anchor `method-attributes`) at the end of "What the Analysis Knows", after "Custom Builders and Collections": + - Mark a public method that changes worker-wide state and should only run while the application boots or in tests with `#[BootOnly]`, `#[TestsOnly]` or `#[BootOrTestsOnly]` from `Hypervel\Support\Attributes`, keeping a short docblock explaining the harm. The worker state checks don't report writes in these methods. Example: the current Worker State `setRequestForTesting()` example, with `#[TestsOnly]` above it and the docblock keeping only the title and the harm sentence. + - When only some calls are restricted, the attribute says which: `whenNotDefault` restricts calls passing the named parameter a value other than its default, so the method can still be read at runtime; `whenProvided` restricts any call passing it; `whenShared` restricts calls only on a worker-shared receiver, the instance the container keeps for the whole worker (not a scoped or fresh one), for methods that change just their own object. Example signatures: `#[BootOnly(whenNotDefault: 'map')] morphMap(?array $map = null, bool $merge = true)` and `#[BootOnly(whenShared: true)] globalOptions(array|Closure $options)`. + - Mark a public method that must exist but always throws, such as an interface method an implementation can't support, with `#[Unsupported]`. Pass the supported alternative as its message when there is one: `#[Unsupported('Use call() instead.')]`. +- Worker State (lines 885–897): replace the docblock-tag paragraph and its example with one sentence: "Writes in methods marked as [boot-time or test-only](#method-attributes), and in `flushState` methods, are not reported." + +`docs/todo.md`: +- In "Release preparation", add `hypervel/attributes` to the repositories and Packagist registrations to create, alongside `hypervel/workbench` and `hypervel/sentinel`. +- Add an "Engine" section with one entry: + > - `Hypervel\Engine\Channel::hasProducers()`, `hasConsumers()`, `isReadable()` and `isWritable()` are inherited Hyperf stubs that always throw. Implement them with Swoole's `Channel::stats()` (`producer_num`, `consumer_num`, `queue_num`), or remove them from `ChannelInterface`, after checking their close, read and write semantics. + +## 6. Monorepo consumers (after this PR merges) + +`packages/hypervel/src` in the monorepo has 29 files using the tags: +- tenancy 10, workflow 7, webhooks 4, activity-log 2, kafka 2, ssh 2, jmespath 1, oidc 1. + +It reaches components through a path repository, so `hypervel/attributes` resolves locally. + +After this PR merges to `0.4`: +- convert those files on monorepo `main` with the same rules; +- add `"hypervel/attributes": "*"` to each affected sub-package `composer.json` (`src/{tenancy,workflow,…}/composer.json`; direct edit, no lockfile); +- run `composer require "hypervel/attributes:*"` in `packages/hypervel`, whose root manifest lists the components it uses; +- run that package's `composer analyse` and affected tests; +- get peer review, then commit on `main`. CLAUDE.md's "Consumer and sibling checks" workflow authorizes this. + +This follow-up isn't part of the PR's completion. + +## 7. Verification + +Run sequentially from the components root: +1. Each changed or new test file as it's edited: `WorkerStateRuleTest` and each touched `PackageMetadataTest`. +2. One-off packaging smoke check, not committed: + - copy `src/attributes` to a scratch directory, `composer dump-autoload` there; + - run a script that requires its `vendor/autoload.php` and instantiates every attribute with its constructor arguments (`new BootOnly(whenNotDefault: 'map', whenShared: true)`, `new BootOnly(whenProvided: 'duration')`, `new Unsupported`, `new Unsupported('Use call() instead.')`), independent of the root mappings; + - delete the directory. +3. A one-off script check that every package except `phpstan` whose `src` uses an attribute declares `hypervel/attributes`. +4. `composer analyse`. The conversion changes no runtime behavior, so this, rather than the runtime suites, verifies the converted files. +5. `composer test:phpstan`, since the worker-state helper changed. +6. `COMPOSER_PROCESS_TIMEOUT=0 composer lint:fix` before review. + +## Status + +Plan signed off by the peer and implementation approved by the owner. + +- Done: sections 1 to 5 and 7; the container, CacheManager memo, Redis pinned-connection and guard context keys; the ArrayStore and tagged-cache clone corrections. +- Next step: resolve PR review and CI feedback before merge. Section 6 follows the merge. diff --git a/docs/todo.md b/docs/todo.md index d77e824763..97a2d8dd51 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -2,7 +2,7 @@ ## Release preparation -- Before releasing Hypervel 0.4, ensure every split repository has a current `0.4` branch and a Packagist entry. Create the missing `hypervel/workbench` and `hypervel/sentinel` repositories and registrations; Horizon and Telescope require Sentinel. Check these prerequisites before running `bin/release.sh`, which pushes the main repository's tag before checking out the component branches. +- Before releasing Hypervel 0.4, ensure every split repository has a current `0.4` branch and a Packagist entry. Create the missing `hypervel/attributes`, `hypervel/workbench` and `hypervel/sentinel` repositories and registrations; Horizon and Telescope require Sentinel. Check these prerequisites before running `bin/release.sh`, which pushes the main repository's tag before checking out the component branches. - Once the 0.4 splits are published, verify a full Composer installation of the `hypervel/hypervel` skeleton, re-enable its manually disabled `tests` workflow, and confirm the first CI run passes. - Replace `psy/psysh` `dev-main` in the root and `src/tinker` manifests with the first stable release containing PsySH #951–#954. The latest stable release, v0.12.24, predates those changes. @@ -88,6 +88,8 @@ Remove older ways of doing things once a modern equivalent fully replaces them, ## Testing +- Investigate a method attribute such as `#[ResetAfterTest]` to replace the manual static-reset list in `AfterEachTestSubscriber`. Discover methods once per test process and reuse the list after each test, covering classes loaded later without eagerly loading optional packages. Preserve required cleanup ordering, avoid duplicate inherited calls, and keep special lifecycle cleanup explicit. Reuse existing test-state registration where appropriate; avoid per-test scanning or a complex discovery system. + - Adopt a Swoole release fixing the intermittent AIO shutdown hang in [swoole-src#6297](https://github.com/swoole/swoole-src/issues/6297). On 6.3.0RC1, ordinary coroutine scheduler teardown can wait forever for an idle native thread that missed its shutdown notification, including between tests. Verify the released fix with the standalone reproduction and affected suites; do not add a Hypervel workaround or a timing-dependent regression test. - Add the required title docblocks to the untitled non-test methods in `tests/`: about 7,600 methods in 1,500 files, mostly helpers, fixtures, and methods of anonymous classes in older tests. `AGENTS.md` requires a title on every method except test methods, but most existing tests omit them, and agents follow the surrounding code over the written rule, so new tests keep reproducing the gap until review catches it. Make the change as one approved sweep (short imperative titles only), then add an automated check, such as a PHPStan rule, so the code and the rule cannot drift apart again. Decide whether the check also covers `src/`. - Add native return types to the `test*` methods that lack them: about 5,600 methods in 490 files. `AGENTS.md` requires `: void` on test methods; a test that returns a value for `#[Depends]` takes its actual return type instead. As with method titles, untyped methods in existing files lead new tests to copy them, so make the change as one approved sweep and enforce it with the same automated check. @@ -112,6 +114,10 @@ Remove older ways of doing things once a modern equivalent fully replaces them, - Require a Swoole release that resets signal-listener state in forked server workers before releasing Hypervel 0.4. In Swoole 6.3.0 RC1 and earlier, a worker forked after the manager calls `Process::signal()` inherits the listener count, so `Coroutine\System::waitSignal()` fails in it. Hypervel's SIGINT shutdown handling registers a manager callback in both server modes, so after a reload, `max_request` recycling or a crash restart, replacement workers stop receiving configured signal handlers and Artisan traps. Once a fixed release is verified, raise the `ext-swoole` constraint and remove the version skip from `ShutdownOnInterruptListenerTest::testReplacementWorkersKeepTheirSignalHandlers()`. - Remove trailer-stream one-chunk lookahead once the minimum supported Swoole release includes [swoole-src#6124](https://github.com/swoole/swoole-src/pull/6124). Current releases send an empty `END_STREAM` DATA frame before trailer HEADERS when `end()` receives no body after `write()`, so `ResponseBridge` retains the final chunk for `end($chunk)` and delays delivery by one chunk. Once fixed, raise the `ext-swoole` constraint, write every chunk immediately, emit trailers, call bare `end()`, invert the deterministic bridge ordering tests, and add real gRPC incremental-delivery coverage. +## Engine + +- `Hypervel\Engine\Channel::hasProducers()`, `hasConsumers()`, `isReadable()` and `isWritable()` are inherited Hyperf stubs that always throw. Implement them with Swoole's `Channel::stats()` (`producer_num`, `consumer_num`, `queue_num`), or remove them from `ChannelInterface`, after checking their close, read and write semantics. + ## Routing - Correct `CompiledRouteCollection`'s 405 method aggregation when cached routes and routes added at runtime share a path but allow different methods. If the compiled matcher rejects the request method, the dynamic collection's `MethodNotAllowedHttpException` currently replaces the compiled matcher's allowed-method set, so the response's `Allow` header omits methods supplied by the cached routes. Laravel has the same catch structure, but Hypervel should retain, merge, and de-duplicate both method sets before producing the 405 response. Add focused coverage with cached and dynamic methods in both registration directions, including GET/HEAD behavior. diff --git a/src/ai/composer.json b/src/ai/composer.json index 7c3d24915d..a37ed32b17 100644 --- a/src/ai/composer.json +++ b/src/ai/composer.json @@ -28,6 +28,7 @@ "ext-filter": "*", "ext-swoole": "^6.3.0RC1", "guzzlehttp/psr7": "^3.1", + "hypervel/attributes": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", diff --git a/src/ai/src/AiManager.php b/src/ai/src/AiManager.php index f682c47d57..acf3b83403 100644 --- a/src/ai/src/AiManager.php +++ b/src/ai/src/AiManager.php @@ -48,6 +48,7 @@ use Hypervel\Ai\Providers\XaiProvider; use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\MultipleInstanceManager; use InvalidArgumentException; use LogicException; @@ -174,8 +175,8 @@ public function resolveOnDemandProviders(array $providers): array /** * Forget resolved providers while retaining their configuration. * - * Boot or tests only for configured providers. Their instances are shared - * by all requests; on-demand providers belong to the current operation. + * Forgetting a configured provider affects every request on the worker; + * on-demand providers belong to the current operation. */ public function forgetInstance(array|string|null $name = null): static { @@ -194,8 +195,8 @@ public function forgetInstance(array|string|null $name = null): static /** * Remove a resolved provider. * - * Boot or tests only for configured providers. Their instances are shared - * by all requests; on-demand providers belong to the current operation. + * Forgetting a configured provider affects every request on the worker; + * on-demand providers belong to the current operation. */ public function purge(?string $name = null): void { @@ -650,8 +651,9 @@ public function getDefaultInstance(): string /** * Set the default instance name. * - * Boot-only. This changes the worker's default provider for every request. + * This changes the worker's default provider for every request. */ + #[BootOnly] public function setDefaultInstance(string $name): void { $this->config->set('ai.default', $name); @@ -690,10 +692,11 @@ public function getInstanceConfig(string $name): array * Configured providers are rebuilt on every lookup, including those built * by custom creators. Keep custom creators inexpensive and stateless. * - * Boot-only. The callback is shared by every subsequent AI operation. + * On the shared manager, the callback applies to every later AI operation. * * @param Closure(string, array): array $resolver */ + #[BootOnly(whenShared: true)] public function resolveProviderConfigUsing(Closure $resolver): void { if ($this->providerConfigResolver !== null) { @@ -706,10 +709,12 @@ public function resolveProviderConfigUsing(Closure $resolver): void /** * Configure the conversation row partition resolver. * - * Boot-only. The column and callback apply to every subsequent conversation operation. + * On the shared manager, the column and callback apply to every subsequent + * conversation operation. * * @param Closure(): (null|int|string) $resolver */ + #[BootOnly(whenShared: true)] public function resolveConversationPartitionUsing(string $column, Closure $resolver): void { if ($this->conversationPartitionResolver !== null) { @@ -753,10 +758,12 @@ public function conversationPartition(): int|string|null /** * Configure the embeddings cache scope resolver. * - * Boot-only. The callback applies to every subsequent embeddings cache operation. + * On the shared manager, the callback applies to every subsequent + * embeddings cache operation. * * @param Closure(): ?string $resolver */ + #[BootOnly(whenShared: true)] public function resolveEmbeddingsCacheScopeUsing(Closure $resolver): void { if ($this->embeddingsCacheScopeResolver !== null) { @@ -788,10 +795,11 @@ public function embeddingsCacheScope(): ?string * and inexpensive to re-enter when its context is already active. Return * a runner for a captured empty context too; null means no wrapper. * - * Boot-only. The callback is shared by every subsequent AI operation. + * On the shared manager, the callback applies to every later AI operation. * * @param Closure(): ?Closure $capture */ + #[BootOnly(whenShared: true)] public function captureContextUsing(Closure $capture): void { if ($this->contextCapture !== null) { diff --git a/src/ai/src/Audio.php b/src/ai/src/Audio.php index 3cefb353e1..84ad7b1cca 100644 --- a/src/ai/src/Audio.php +++ b/src/ai/src/Audio.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Ai\Gateway\FakeAudioGateway; use Hypervel\Ai\PendingResponses\PendingAudioGeneration; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; class Audio @@ -24,8 +25,9 @@ public static function of(string $text): PendingAudioGeneration /** * Fake audio generation. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeAudioGateway { return Ai::fakeAudio($responses); diff --git a/src/ai/src/Classification.php b/src/ai/src/Classification.php index d0a750eed5..2d52fe8817 100644 --- a/src/ai/src/Classification.php +++ b/src/ai/src/Classification.php @@ -9,6 +9,7 @@ use Hypervel\Ai\Gateway\FakeClassificationGateway; use Hypervel\Ai\PendingResponses\PendingClassification; use Hypervel\Http\UploadedFile; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; class Classification @@ -29,8 +30,9 @@ public static function of(string|array $state, array $attachments = []): Pending /** * Fake classification operations. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeClassificationGateway { return Ai::fakeClassification($responses); diff --git a/src/ai/src/Concerns/InteractsWithFakeAgents.php b/src/ai/src/Concerns/InteractsWithFakeAgents.php index 88ce8f3d38..2cdf9c1cc4 100644 --- a/src/ai/src/Concerns/InteractsWithFakeAgents.php +++ b/src/ai/src/Concerns/InteractsWithFakeAgents.php @@ -9,6 +9,7 @@ use Hypervel\Ai\Gateway\FakeTextGateway; use Hypervel\Ai\Prompts\AgentPrompt; use Hypervel\Ai\QueuedAgentPrompt; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use InvalidArgumentException; @@ -34,8 +35,9 @@ trait InteractsWithFakeAgents /** * Fake the responses returned by the given agent. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeAgent(string $agent, Closure|array $responses = []): FakeTextGateway { return tap( @@ -68,8 +70,9 @@ public function fakeGatewayFor(Agent $agent): FakeTextGateway /** * Record the given prompt for the faked agent. * - * Tests only. Recorded prompts remain on the worker-shared manager. + * Recorded prompts remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordPrompt(AgentPrompt|QueuedAgentPrompt $prompt): self { if ($prompt instanceof QueuedAgentPrompt) { diff --git a/src/ai/src/Concerns/InteractsWithFakeAudio.php b/src/ai/src/Concerns/InteractsWithFakeAudio.php index 3cab25bde2..1d3d2de710 100644 --- a/src/ai/src/Concerns/InteractsWithFakeAudio.php +++ b/src/ai/src/Concerns/InteractsWithFakeAudio.php @@ -8,6 +8,7 @@ use Hypervel\Ai\Gateway\FakeAudioGateway; use Hypervel\Ai\Prompts\AudioPrompt; use Hypervel\Ai\Prompts\QueuedAudioPrompt; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -31,8 +32,9 @@ trait InteractsWithFakeAudio /** * Fake audio generation. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeAudio(Closure|array $responses = []): FakeAudioGateway { return $this->fakeAudioGateway = new FakeAudioGateway($responses); @@ -41,8 +43,9 @@ public function fakeAudio(Closure|array $responses = []): FakeAudioGateway /** * Record an audio generation. * - * Tests only. Recorded prompts remain on the worker-shared manager. + * Recorded prompts remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordAudioGeneration(AudioPrompt|QueuedAudioPrompt $prompt): self { if ($prompt instanceof QueuedAudioPrompt) { diff --git a/src/ai/src/Concerns/InteractsWithFakeClassification.php b/src/ai/src/Concerns/InteractsWithFakeClassification.php index 5845a26689..372ccd820e 100644 --- a/src/ai/src/Concerns/InteractsWithFakeClassification.php +++ b/src/ai/src/Concerns/InteractsWithFakeClassification.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Ai\Gateway\FakeClassificationGateway; use Hypervel\Ai\Prompts\ClassificationPrompt; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -25,8 +26,9 @@ trait InteractsWithFakeClassification /** * Fake classification operations. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeClassification(Closure|array $responses = []): FakeClassificationGateway { return $this->fakeClassificationGateway = new FakeClassificationGateway($responses); @@ -35,8 +37,9 @@ public function fakeClassification(Closure|array $responses = []): FakeClassific /** * Record a classification. * - * Tests only. Recorded prompts remain on the worker-shared manager. + * Recorded prompts remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordClassification(ClassificationPrompt $prompt): self { $this->recordedClassifications[] = $prompt; diff --git a/src/ai/src/Concerns/InteractsWithFakeEmbeddings.php b/src/ai/src/Concerns/InteractsWithFakeEmbeddings.php index 7c4257a42d..92b13113ac 100644 --- a/src/ai/src/Concerns/InteractsWithFakeEmbeddings.php +++ b/src/ai/src/Concerns/InteractsWithFakeEmbeddings.php @@ -8,6 +8,7 @@ use Hypervel\Ai\Gateway\FakeEmbeddingGateway; use Hypervel\Ai\Prompts\EmbeddingsPrompt; use Hypervel\Ai\Prompts\QueuedEmbeddingsPrompt; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -31,8 +32,9 @@ trait InteractsWithFakeEmbeddings /** * Fake embeddings generation. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeEmbeddings(Closure|array $responses = []): FakeEmbeddingGateway { return $this->fakeEmbeddingGateway = new FakeEmbeddingGateway($responses); @@ -41,8 +43,9 @@ public function fakeEmbeddings(Closure|array $responses = []): FakeEmbeddingGate /** * Record an embeddings generation. * - * Tests only. Recorded prompts remain on the worker-shared manager. + * Recorded prompts remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordEmbeddingsGeneration(EmbeddingsPrompt|QueuedEmbeddingsPrompt $prompt): self { if ($prompt instanceof QueuedEmbeddingsPrompt) { diff --git a/src/ai/src/Concerns/InteractsWithFakeFiles.php b/src/ai/src/Concerns/InteractsWithFakeFiles.php index 203425b85b..06c532fbe1 100644 --- a/src/ai/src/Concerns/InteractsWithFakeFiles.php +++ b/src/ai/src/Concerns/InteractsWithFakeFiles.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Ai\Contracts\Files\StorableFile; use Hypervel\Ai\Gateway\FakeFileGateway; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -30,8 +31,9 @@ trait InteractsWithFakeFiles /** * Fake file operations. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeFiles(Closure|array $responses = []): FakeFileGateway { return $this->fakeFileGateway = new FakeFileGateway($responses); @@ -40,8 +42,9 @@ public function fakeFiles(Closure|array $responses = []): FakeFileGateway /** * Record a file upload. * - * Tests only. Recorded uploads remain on the worker-shared manager. + * Recorded uploads remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordFileUpload(StorableFile $file): self { $this->recordedFileUploads[] = [ @@ -54,8 +57,9 @@ public function recordFileUpload(StorableFile $file): self /** * Record a file deletion. * - * Tests only. Recorded deletions remain on the worker-shared manager. + * Recorded deletions remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordFileDeletion(string $fileId): self { $this->recordedFileDeletions[] = $fileId; diff --git a/src/ai/src/Concerns/InteractsWithFakeImages.php b/src/ai/src/Concerns/InteractsWithFakeImages.php index 8837514358..7bf32fd255 100644 --- a/src/ai/src/Concerns/InteractsWithFakeImages.php +++ b/src/ai/src/Concerns/InteractsWithFakeImages.php @@ -8,6 +8,7 @@ use Hypervel\Ai\Gateway\FakeImageGateway; use Hypervel\Ai\Prompts\ImagePrompt; use Hypervel\Ai\Prompts\QueuedImagePrompt; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -31,8 +32,9 @@ trait InteractsWithFakeImages /** * Fake image generation. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeImages(Closure|array $responses = []): FakeImageGateway { return $this->fakeImageGateway = new FakeImageGateway($responses); @@ -41,8 +43,9 @@ public function fakeImages(Closure|array $responses = []): FakeImageGateway /** * Record an image generation. * - * Tests only. Recorded prompts remain on the worker-shared manager. + * Recorded prompts remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordImageGeneration(ImagePrompt|QueuedImagePrompt $prompt): self { if ($prompt instanceof QueuedImagePrompt) { diff --git a/src/ai/src/Concerns/InteractsWithFakeReranking.php b/src/ai/src/Concerns/InteractsWithFakeReranking.php index 448072f528..4d888677b3 100644 --- a/src/ai/src/Concerns/InteractsWithFakeReranking.php +++ b/src/ai/src/Concerns/InteractsWithFakeReranking.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Ai\Gateway\FakeRerankingGateway; use Hypervel\Ai\Prompts\RerankingPrompt; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -25,8 +26,9 @@ trait InteractsWithFakeReranking /** * Fake reranking operations. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeReranking(Closure|array $responses = []): FakeRerankingGateway { return $this->fakeRerankingGateway = new FakeRerankingGateway($responses); @@ -35,8 +37,9 @@ public function fakeReranking(Closure|array $responses = []): FakeRerankingGatew /** * Record a reranking. * - * Tests only. Recorded prompts remain on the worker-shared manager. + * Recorded prompts remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordReranking(RerankingPrompt $prompt): self { $this->recordedRerankings[] = $prompt; diff --git a/src/ai/src/Concerns/InteractsWithFakeStores.php b/src/ai/src/Concerns/InteractsWithFakeStores.php index 1f6bf18db2..6f0dcff34d 100644 --- a/src/ai/src/Concerns/InteractsWithFakeStores.php +++ b/src/ai/src/Concerns/InteractsWithFakeStores.php @@ -12,6 +12,7 @@ use Hypervel\Ai\Gateway\FakeStoreGateway; use Hypervel\Ai\Stores; use Hypervel\Http\UploadedFile; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -45,8 +46,9 @@ trait InteractsWithFakeStores /** * Fake store operations. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeStores(Closure|array $responses = []): FakeStoreGateway { return $this->fakeStoreGateway = new FakeStoreGateway($responses); @@ -55,8 +57,9 @@ public function fakeStores(Closure|array $responses = []): FakeStoreGateway /** * Record a store creation. * - * Tests only. Recorded creations remain on the worker-shared manager. + * Recorded creations remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordStoreCreation( string $name, ?string $description = null, @@ -76,8 +79,9 @@ public function recordStoreCreation( /** * Record a store deletion. * - * Tests only. Recorded deletions remain on the worker-shared manager. + * Recorded deletions remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordStoreDeletion(string $storeId): self { $this->recordedStoreDeletions[] = $storeId; @@ -88,8 +92,9 @@ public function recordStoreDeletion(string $storeId): self /** * Record a file addition to a store. * - * Tests only. Recorded additions remain on the worker-shared manager. + * Recorded additions remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordFileAddition( string $storeId, string $fileId, @@ -107,8 +112,9 @@ public function recordFileAddition( /** * Record a file removal from a store. * - * Tests only. Recorded removals remain on the worker-shared manager. + * Recorded removals remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordFileRemoval(string $storeId, string $fileId): self { $this->recordedFileRemovals[] = [ diff --git a/src/ai/src/Concerns/InteractsWithFakeTranscriptions.php b/src/ai/src/Concerns/InteractsWithFakeTranscriptions.php index 1f38fafade..fef01bdf22 100644 --- a/src/ai/src/Concerns/InteractsWithFakeTranscriptions.php +++ b/src/ai/src/Concerns/InteractsWithFakeTranscriptions.php @@ -8,6 +8,7 @@ use Hypervel\Ai\Gateway\FakeTranscriptionGateway; use Hypervel\Ai\Prompts\QueuedTranscriptionPrompt; use Hypervel\Ai\Prompts\TranscriptionPrompt; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use PHPUnit\Framework\Assert as PHPUnit; @@ -31,8 +32,9 @@ trait InteractsWithFakeTranscriptions /** * Fake transcription generation. * - * Tests only. The fake gateway is shared by all requests in the worker. + * On the shared manager, the fake gateway affects every request. */ + #[TestsOnly(whenShared: true)] public function fakeTranscriptions(Closure|array $responses = []): FakeTranscriptionGateway { return $this->fakeTranscriptionGateway = new FakeTranscriptionGateway($responses); @@ -41,8 +43,9 @@ public function fakeTranscriptions(Closure|array $responses = []): FakeTranscrip /** * Record a transcription generation. * - * Tests only. Recorded prompts remain on the worker-shared manager. + * Recorded prompts remain on the worker-shared manager. */ + #[TestsOnly(whenShared: true)] public function recordTranscriptionGeneration(TranscriptionPrompt|QueuedTranscriptionPrompt $prompt): self { if ($prompt instanceof QueuedTranscriptionPrompt) { diff --git a/src/ai/src/Contracts/Providers/AudioProvider.php b/src/ai/src/Contracts/Providers/AudioProvider.php index 2bf64c4c74..ea8943210b 100644 --- a/src/ai/src/Contracts/Providers/AudioProvider.php +++ b/src/ai/src/Contracts/Providers/AudioProvider.php @@ -6,6 +6,7 @@ use Hypervel\Ai\Contracts\Gateway\AudioGateway; use Hypervel\Ai\Responses\AudioResponse; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface AudioProvider extends Provider { @@ -31,6 +32,7 @@ public function audioGateway(): AudioGateway; /** * Set the provider's audio gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useAudioGateway(AudioGateway $gateway): self; /** diff --git a/src/ai/src/Contracts/Providers/ClassificationProvider.php b/src/ai/src/Contracts/Providers/ClassificationProvider.php index 07bceed379..3a0c846b2e 100644 --- a/src/ai/src/Contracts/Providers/ClassificationProvider.php +++ b/src/ai/src/Contracts/Providers/ClassificationProvider.php @@ -9,6 +9,7 @@ use Hypervel\Ai\Files\File; use Hypervel\Ai\Responses\ClassificationResponse; use Hypervel\Http\UploadedFile; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface ClassificationProvider extends Provider { @@ -30,6 +31,7 @@ public function classificationGateway(): ClassificationGateway; /** * Set the provider's classification gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useClassificationGateway(ClassificationGateway $gateway): self; /** diff --git a/src/ai/src/Contracts/Providers/EmbeddingProvider.php b/src/ai/src/Contracts/Providers/EmbeddingProvider.php index ecbed5ba07..170b0e0634 100644 --- a/src/ai/src/Contracts/Providers/EmbeddingProvider.php +++ b/src/ai/src/Contracts/Providers/EmbeddingProvider.php @@ -10,6 +10,7 @@ use Hypervel\Ai\Files\Image; use Hypervel\Ai\Files\Video; use Hypervel\Ai\Responses\EmbeddingsResponse; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface EmbeddingProvider extends Provider { @@ -29,6 +30,7 @@ public function embeddingGateway(): EmbeddingGateway; /** * Set the provider's embedding gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useEmbeddingGateway(EmbeddingGateway $gateway): self; /** diff --git a/src/ai/src/Contracts/Providers/FileProvider.php b/src/ai/src/Contracts/Providers/FileProvider.php index 5397cd3c8c..c0c0d4ce3e 100644 --- a/src/ai/src/Contracts/Providers/FileProvider.php +++ b/src/ai/src/Contracts/Providers/FileProvider.php @@ -8,6 +8,7 @@ use Hypervel\Ai\Contracts\Gateway\FileGateway; use Hypervel\Ai\Responses\FileResponse; use Hypervel\Ai\Responses\StoredFileResponse; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface FileProvider extends Provider { @@ -34,5 +35,6 @@ public function fileGateway(): FileGateway; /** * Set the provider's file gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useFileGateway(FileGateway $gateway): self; } diff --git a/src/ai/src/Contracts/Providers/ImageProvider.php b/src/ai/src/Contracts/Providers/ImageProvider.php index 713ecdef05..9609303cc7 100644 --- a/src/ai/src/Contracts/Providers/ImageProvider.php +++ b/src/ai/src/Contracts/Providers/ImageProvider.php @@ -7,6 +7,7 @@ use Hypervel\Ai\Contracts\Gateway\ImageGateway; use Hypervel\Ai\Files\Image; use Hypervel\Ai\Responses\ImageResponse; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface ImageProvider extends Provider { @@ -35,6 +36,7 @@ public function imageGateway(): ImageGateway; /** * Set the provider's image gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useImageGateway(ImageGateway $gateway): self; /** diff --git a/src/ai/src/Contracts/Providers/RerankingProvider.php b/src/ai/src/Contracts/Providers/RerankingProvider.php index b8fb1ab201..712b534b86 100644 --- a/src/ai/src/Contracts/Providers/RerankingProvider.php +++ b/src/ai/src/Contracts/Providers/RerankingProvider.php @@ -6,6 +6,7 @@ use Hypervel\Ai\Contracts\Gateway\RerankingGateway; use Hypervel\Ai\Responses\RerankingResponse; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface RerankingProvider extends Provider { @@ -25,6 +26,7 @@ public function rerankingGateway(): RerankingGateway; /** * Set the provider's reranking gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useRerankingGateway(RerankingGateway $gateway): self; /** diff --git a/src/ai/src/Contracts/Providers/StoreProvider.php b/src/ai/src/Contracts/Providers/StoreProvider.php index b5baa05b53..693444e2fb 100644 --- a/src/ai/src/Contracts/Providers/StoreProvider.php +++ b/src/ai/src/Contracts/Providers/StoreProvider.php @@ -8,6 +8,7 @@ use Hypervel\Ai\Contracts\Files\HasProviderId; use Hypervel\Ai\Contracts\Gateway\StoreGateway; use Hypervel\Ai\Store; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; interface StoreProvider extends FileProvider @@ -52,5 +53,6 @@ public function storeGateway(): StoreGateway; /** * Set the provider's store gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useStoreGateway(StoreGateway $gateway): self; } diff --git a/src/ai/src/Contracts/Providers/TextProvider.php b/src/ai/src/Contracts/Providers/TextProvider.php index 8badd499a8..6a1d3ae163 100644 --- a/src/ai/src/Contracts/Providers/TextProvider.php +++ b/src/ai/src/Contracts/Providers/TextProvider.php @@ -9,6 +9,7 @@ use Hypervel\Ai\Prompts\AgentPrompt; use Hypervel\Ai\Responses\AgentResponse; use Hypervel\Ai\Responses\StreamableAgentResponse; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface TextProvider extends Provider { @@ -25,6 +26,7 @@ public function stream(AgentPrompt $prompt): StreamableAgentResponse; /** * Set the provider's text gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useTextGateway(StepTextGateway $gateway): self; /** diff --git a/src/ai/src/Contracts/Providers/TranscriptionProvider.php b/src/ai/src/Contracts/Providers/TranscriptionProvider.php index 044f339b52..1e14371bd0 100644 --- a/src/ai/src/Contracts/Providers/TranscriptionProvider.php +++ b/src/ai/src/Contracts/Providers/TranscriptionProvider.php @@ -7,6 +7,7 @@ use Hypervel\Ai\Contracts\Files\TranscribableAudio; use Hypervel\Ai\Contracts\Gateway\TranscriptionGateway; use Hypervel\Ai\Responses\TranscriptionResponse; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface TranscriptionProvider extends Provider { @@ -32,6 +33,7 @@ public function transcriptionGateway(): TranscriptionGateway; /** * Set the provider's transcription gateway. */ + #[BootOrTestsOnly(whenShared: true)] public function useTranscriptionGateway(TranscriptionGateway $gateway): self; /** diff --git a/src/ai/src/Embeddings.php b/src/ai/src/Embeddings.php index ffa2c9ee8f..392e155ccb 100644 --- a/src/ai/src/Embeddings.php +++ b/src/ai/src/Embeddings.php @@ -11,6 +11,7 @@ use Hypervel\Ai\Files\Video; use Hypervel\Ai\Gateway\FakeEmbeddingGateway; use Hypervel\Ai\PendingResponses\PendingEmbeddingsGeneration; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; use RuntimeException; @@ -31,8 +32,9 @@ public static function for(array $inputs): PendingEmbeddingsGeneration /** * Fake embeddings generation. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeEmbeddingGateway { return Ai::fakeEmbeddings($responses); diff --git a/src/ai/src/Files.php b/src/ai/src/Files.php index 3c8f28059c..6c947ca01f 100644 --- a/src/ai/src/Files.php +++ b/src/ai/src/Files.php @@ -15,6 +15,7 @@ use Hypervel\Ai\Responses\FileResponse; use Hypervel\Ai\Responses\StoredFileResponse; use Hypervel\Http\UploadedFile; +use Hypervel\Support\Attributes\TestsOnly; class Files { @@ -79,8 +80,9 @@ public static function delete(string $fileId, Provider|Lab|string|null $provider /** * Fake file operations. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeFileGateway { return Ai::fakeFiles($responses); diff --git a/src/ai/src/Files/ProviderDocument.php b/src/ai/src/Files/ProviderDocument.php index b9960dee4f..4a44f36931 100644 --- a/src/ai/src/Files/ProviderDocument.php +++ b/src/ai/src/Files/ProviderDocument.php @@ -7,6 +7,7 @@ use Hypervel\Ai\Contracts\Files\HasProviderId; use Hypervel\Ai\Files\Concerns\CanBeRetrievedOrDeletedFromProvider; use Hypervel\Contracts\Support\Arrayable; +use Hypervel\Support\Attributes\Unsupported; use InvalidArgumentException; use JsonSerializable; @@ -34,6 +35,7 @@ public function id(): string * * @throws InvalidArgumentException */ + #[Unsupported] public function content(): string { throw new InvalidArgumentException( diff --git a/src/ai/src/Files/S3Document.php b/src/ai/src/Files/S3Document.php index 0bb8c960ab..13ed3bcf2e 100644 --- a/src/ai/src/Files/S3Document.php +++ b/src/ai/src/Files/S3Document.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Files; use Hypervel\Contracts\Support\Arrayable; +use Hypervel\Support\Attributes\Unsupported; use InvalidArgumentException; use JsonSerializable; use Override; @@ -38,6 +39,7 @@ public function name(): ?string * * @throws InvalidArgumentException */ + #[Unsupported('Use StoredDocument or RemoteDocument to send the file contents inline.')] public function content(): string { throw new InvalidArgumentException( diff --git a/src/ai/src/Files/UntrustedUrl.php b/src/ai/src/Files/UntrustedUrl.php index b0806499f2..24f79ec92a 100644 --- a/src/ai/src/Files/UntrustedUrl.php +++ b/src/ai/src/Files/UntrustedUrl.php @@ -13,6 +13,7 @@ use Hypervel\Http\Client\Destinations\DestinationResolutionException; use Hypervel\Http\Client\Destinations\DisallowedDestinationException; use Hypervel\Http\Client\Response; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Facades\Http; use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; @@ -121,10 +122,11 @@ public static function fetch(string $url): Response /** * Resolve hostnames with the given callback, or the system resolver when null. * - * Boot or tests only. The callback persists for the worker lifetime and affects every subsequent fetch. + * The callback persists for the worker lifetime and affects every subsequent fetch. * * @param null|(Closure(string): list) $resolver */ + #[BootOrTestsOnly] public static function resolveUsing(?Closure $resolver): void { static::$resolver = $resolver; diff --git a/src/ai/src/Gateway/Anthropic/AnthropicGateway.php b/src/ai/src/Gateway/Anthropic/AnthropicGateway.php index 2c417f4623..ad4ebd0c32 100644 --- a/src/ai/src/Gateway/Anthropic/AnthropicGateway.php +++ b/src/ai/src/Gateway/Anthropic/AnthropicGateway.php @@ -31,6 +31,7 @@ use Hypervel\Ai\Responses\ImageResponse; use Hypervel\Ai\Responses\TranscriptionResponse; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Support\Attributes\Unsupported; use LogicException; class AnthropicGateway implements Gateway, StepTextGateway @@ -138,6 +139,7 @@ public function generateStreamStep( * * @throws LogicException */ + #[Unsupported] public function generateImage( ImageProvider $provider, string $model, @@ -156,6 +158,7 @@ public function generateImage( * * @throws LogicException */ + #[Unsupported] public function generateAudio( AudioProvider $provider, string $model, @@ -173,6 +176,7 @@ public function generateAudio( * * @throws LogicException */ + #[Unsupported] public function generateTranscription( TranscriptionProvider $provider, string $model, @@ -190,6 +194,7 @@ public function generateTranscription( * * @throws LogicException */ + #[Unsupported] public function generateEmbeddings( EmbeddingProvider $provider, string $model, diff --git a/src/ai/src/Gateway/FakeAudioGateway.php b/src/ai/src/Gateway/FakeAudioGateway.php index 79064f1801..b56fcf551a 100644 --- a/src/ai/src/Gateway/FakeAudioGateway.php +++ b/src/ai/src/Gateway/FakeAudioGateway.php @@ -11,6 +11,7 @@ use Hypervel\Ai\Responses\AudioResponse; use Hypervel\Ai\Responses\Data\Meta; use Hypervel\Ai\Responses\Data\Usage; +use Hypervel\Support\Attributes\TestsOnly; use RuntimeException; class FakeAudioGateway implements AudioGateway @@ -97,8 +98,10 @@ protected function marshalResponse( /** * Indicate that an exception should be thrown if any audio generation is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayAudio(bool $prevent = true): self { $this->preventStrayGenerations = $prevent; diff --git a/src/ai/src/Gateway/FakeClassificationGateway.php b/src/ai/src/Gateway/FakeClassificationGateway.php index ae999d50d7..75a482a975 100644 --- a/src/ai/src/Gateway/FakeClassificationGateway.php +++ b/src/ai/src/Gateway/FakeClassificationGateway.php @@ -21,6 +21,7 @@ use Hypervel\Ai\Responses\Data\ScoreAnswer; use Hypervel\Ai\Responses\Data\TextUsage; use Hypervel\Http\UploadedFile; +use Hypervel\Support\Attributes\TestsOnly; use RuntimeException; class FakeClassificationGateway implements ClassificationGateway @@ -183,8 +184,10 @@ protected function randomDistribution(array $keys): array /** * Indicate that an exception should be thrown if any classification is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayClassifications(bool $prevent = true): self { $this->preventStrayClassifications = $prevent; diff --git a/src/ai/src/Gateway/FakeEmbeddingGateway.php b/src/ai/src/Gateway/FakeEmbeddingGateway.php index fbfa69972b..5f9bb46090 100644 --- a/src/ai/src/Gateway/FakeEmbeddingGateway.php +++ b/src/ai/src/Gateway/FakeEmbeddingGateway.php @@ -16,6 +16,7 @@ use Hypervel\Ai\Responses\Data\Meta; use Hypervel\Ai\Responses\Data\Usage; use Hypervel\Ai\Responses\EmbeddingsResponse; +use Hypervel\Support\Attributes\TestsOnly; use RuntimeException; class FakeEmbeddingGateway implements EmbeddingGateway @@ -125,8 +126,10 @@ protected function generateFakeEmbeddings(int $count, int $dimensions): array /** * Indicate that an exception should be thrown if any embeddings generation is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayEmbeddings(bool $prevent = true): self { $this->preventStrayGenerations = $prevent; diff --git a/src/ai/src/Gateway/FakeFileGateway.php b/src/ai/src/Gateway/FakeFileGateway.php index 8d86767b98..d9e631a0d4 100644 --- a/src/ai/src/Gateway/FakeFileGateway.php +++ b/src/ai/src/Gateway/FakeFileGateway.php @@ -11,6 +11,7 @@ use Hypervel\Ai\Files; use Hypervel\Ai\Responses\FileResponse; use Hypervel\Ai\Responses\StoredFileResponse; +use Hypervel\Support\Attributes\TestsOnly; use RuntimeException; class FakeFileGateway implements FileGateway @@ -97,8 +98,10 @@ public function deleteFile(FileProvider $provider, string $fileId): void /** * Indicate that an exception should be thrown if any file operation is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayOperations(bool $prevent = true): self { $this->preventStrayOperations = $prevent; diff --git a/src/ai/src/Gateway/FakeImageGateway.php b/src/ai/src/Gateway/FakeImageGateway.php index e880dfd44d..5d2f518562 100644 --- a/src/ai/src/Gateway/FakeImageGateway.php +++ b/src/ai/src/Gateway/FakeImageGateway.php @@ -13,6 +13,7 @@ use Hypervel\Ai\Responses\Data\ImageUsage; use Hypervel\Ai\Responses\Data\Meta; use Hypervel\Ai\Responses\ImageResponse; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use RuntimeException; @@ -107,8 +108,10 @@ protected function marshalResponse( /** * Indicate that an exception should be thrown if any image generation is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayImages(bool $prevent = true): self { $this->preventStrayGenerations = $prevent; diff --git a/src/ai/src/Gateway/FakeRerankingGateway.php b/src/ai/src/Gateway/FakeRerankingGateway.php index df149f36f0..c728941caa 100644 --- a/src/ai/src/Gateway/FakeRerankingGateway.php +++ b/src/ai/src/Gateway/FakeRerankingGateway.php @@ -13,6 +13,7 @@ use Hypervel\Ai\Responses\Data\RerankingUsage; use Hypervel\Ai\Responses\RerankingResponse; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\TestsOnly; use RuntimeException; class FakeRerankingGateway implements RerankingGateway @@ -132,8 +133,10 @@ protected function generateFakeRanking(array $documents, ?int $limit = null): ar /** * Indicate that an exception should be thrown if any reranking is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayRerankings(bool $prevent = true): self { $this->preventStrayRerankings = $prevent; diff --git a/src/ai/src/Gateway/FakeStoreGateway.php b/src/ai/src/Gateway/FakeStoreGateway.php index 236fa8c21f..8e600b20ce 100644 --- a/src/ai/src/Gateway/FakeStoreGateway.php +++ b/src/ai/src/Gateway/FakeStoreGateway.php @@ -12,6 +12,7 @@ use Hypervel\Ai\Responses\Data\StoreFileCounts; use Hypervel\Ai\Store; use Hypervel\Ai\Stores; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use RuntimeException; @@ -141,8 +142,10 @@ public function deleteStore(StoreProvider $provider, string $storeId): bool /** * Indicate that an exception should be thrown if any store operation is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayOperations(bool $prevent = true): self { $this->preventStrayOperations = $prevent; diff --git a/src/ai/src/Gateway/FakeTextGateway.php b/src/ai/src/Gateway/FakeTextGateway.php index 7aafe55c10..49e41654e4 100644 --- a/src/ai/src/Gateway/FakeTextGateway.php +++ b/src/ai/src/Gateway/FakeTextGateway.php @@ -28,6 +28,7 @@ use Hypervel\Ai\Streaming\Events\ToolCall as ToolCallEvent; use Hypervel\JsonSchema\Types\ObjectType; use Hypervel\JsonSchema\Types\Type; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use RuntimeException; @@ -278,8 +279,10 @@ protected function marshalResponse( /** * Indicate that an exception should be thrown if any prompt is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayPrompts(bool $prevent = true): self { $this->preventStrayPrompts = $prevent; diff --git a/src/ai/src/Gateway/FakeTranscriptionGateway.php b/src/ai/src/Gateway/FakeTranscriptionGateway.php index 22de6ac0f6..6b24828f57 100644 --- a/src/ai/src/Gateway/FakeTranscriptionGateway.php +++ b/src/ai/src/Gateway/FakeTranscriptionGateway.php @@ -13,6 +13,7 @@ use Hypervel\Ai\Responses\Data\TranscriptionSegment; use Hypervel\Ai\Responses\Data\TranscriptionUsage; use Hypervel\Ai\Responses\TranscriptionResponse; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use RuntimeException; @@ -107,8 +108,10 @@ protected function marshalResponse( /** * Indicate that an exception should be thrown if any transcription generation is not faked. * - * Tests only. This setting affects the fake gateway shared by requests in the worker. + * When the shared manager holds this fake gateway, the setting affects + * every request in the worker. */ + #[TestsOnly(whenShared: true)] public function preventStrayTranscriptions(bool $prevent = true): self { $this->preventStrayGenerations = $prevent; diff --git a/src/ai/src/Gateway/Xai/XaiGateway.php b/src/ai/src/Gateway/Xai/XaiGateway.php index e6cf43d5e4..881c7b8444 100644 --- a/src/ai/src/Gateway/Xai/XaiGateway.php +++ b/src/ai/src/Gateway/Xai/XaiGateway.php @@ -24,6 +24,7 @@ use Hypervel\Ai\Responses\ImageResponse; use Hypervel\Ai\Responses\TranscriptionResponse; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Support\Attributes\Unsupported; use LogicException; class XaiGateway implements Gateway, StepTextGateway @@ -114,6 +115,7 @@ public function generateStreamStep( * * @throws LogicException */ + #[Unsupported('Use XaiImageGateway for image generation.')] public function generateImage( ImageProvider $provider, string $model, @@ -132,6 +134,7 @@ public function generateImage( * * @throws LogicException */ + #[Unsupported] public function generateAudio( AudioProvider $provider, string $model, @@ -149,6 +152,7 @@ public function generateAudio( * * @throws LogicException */ + #[Unsupported] public function generateTranscription( TranscriptionProvider $provider, string $model, @@ -166,6 +170,7 @@ public function generateTranscription( * * @throws LogicException */ + #[Unsupported] public function generateEmbeddings( EmbeddingProvider $provider, string $model, diff --git a/src/ai/src/Image.php b/src/ai/src/Image.php index 4fbb4fcf4a..31aba9e1a1 100644 --- a/src/ai/src/Image.php +++ b/src/ai/src/Image.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Ai\Gateway\FakeImageGateway; use Hypervel\Ai\PendingResponses\PendingImageGeneration; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; class Image @@ -24,8 +25,9 @@ public static function of(string $prompt): PendingImageGeneration /** * Fake image generation. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeImageGateway { return Ai::fakeImages($responses); diff --git a/src/ai/src/Promptable.php b/src/ai/src/Promptable.php index 7bcf471042..8c091ae5c3 100644 --- a/src/ai/src/Promptable.php +++ b/src/ai/src/Promptable.php @@ -45,6 +45,7 @@ use Hypervel\Broadcasting\Channel; use Hypervel\Container\Container; use Hypervel\Queue\SerializesModels; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\ClassMetadataCache; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Event; @@ -645,8 +646,9 @@ protected function getTimeout(?int $timeout): int /** * Fake the responses returned by the agent. * - * Tests only. The fake affects subsequent calls for this agent in the worker. + * The fake affects subsequent calls for this agent in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeTextGateway { return Ai::fakeAgent(static::class, $responses); diff --git a/src/ai/src/Providers/Concerns/HasAudioGateway.php b/src/ai/src/Providers/Concerns/HasAudioGateway.php index f81ee7f69b..d364be129f 100644 --- a/src/ai/src/Providers/Concerns/HasAudioGateway.php +++ b/src/ai/src/Providers/Concerns/HasAudioGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\AudioGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasAudioGateway { @@ -21,8 +22,9 @@ public function audioGateway(): AudioGateway /** * Set the provider's audio gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useAudioGateway(AudioGateway $gateway): self { $this->audioGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasClassificationGateway.php b/src/ai/src/Providers/Concerns/HasClassificationGateway.php index 91c0c68f86..5a3680425a 100644 --- a/src/ai/src/Providers/Concerns/HasClassificationGateway.php +++ b/src/ai/src/Providers/Concerns/HasClassificationGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\ClassificationGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasClassificationGateway { @@ -21,8 +22,9 @@ public function classificationGateway(): ClassificationGateway /** * Set the provider's classification gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useClassificationGateway(ClassificationGateway $gateway): self { $this->classificationGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasEmbeddingGateway.php b/src/ai/src/Providers/Concerns/HasEmbeddingGateway.php index 51e56c576b..5a1632b7e4 100644 --- a/src/ai/src/Providers/Concerns/HasEmbeddingGateway.php +++ b/src/ai/src/Providers/Concerns/HasEmbeddingGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\EmbeddingGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasEmbeddingGateway { @@ -21,8 +22,9 @@ public function embeddingGateway(): EmbeddingGateway /** * Set the provider's embedding gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useEmbeddingGateway(EmbeddingGateway $gateway): self { $this->embeddingGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasFileGateway.php b/src/ai/src/Providers/Concerns/HasFileGateway.php index 79ab65b5db..f03f265a0f 100644 --- a/src/ai/src/Providers/Concerns/HasFileGateway.php +++ b/src/ai/src/Providers/Concerns/HasFileGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\FileGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasFileGateway { @@ -21,8 +22,9 @@ public function fileGateway(): FileGateway /** * Set the provider's file gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useFileGateway(FileGateway $gateway): self { $this->fileGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasImageGateway.php b/src/ai/src/Providers/Concerns/HasImageGateway.php index 79d41894b5..9bd5f4043f 100644 --- a/src/ai/src/Providers/Concerns/HasImageGateway.php +++ b/src/ai/src/Providers/Concerns/HasImageGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\ImageGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasImageGateway { @@ -21,8 +22,9 @@ public function imageGateway(): ImageGateway /** * Set the provider's image gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useImageGateway(ImageGateway $gateway): self { $this->imageGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasRerankingGateway.php b/src/ai/src/Providers/Concerns/HasRerankingGateway.php index 667980ab70..fa5fb9d793 100644 --- a/src/ai/src/Providers/Concerns/HasRerankingGateway.php +++ b/src/ai/src/Providers/Concerns/HasRerankingGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\RerankingGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasRerankingGateway { @@ -21,8 +22,9 @@ public function rerankingGateway(): RerankingGateway /** * Set the provider's reranking gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useRerankingGateway(RerankingGateway $gateway): self { $this->rerankingGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasStoreGateway.php b/src/ai/src/Providers/Concerns/HasStoreGateway.php index 02d76cbc2f..708dfdf29b 100644 --- a/src/ai/src/Providers/Concerns/HasStoreGateway.php +++ b/src/ai/src/Providers/Concerns/HasStoreGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\StoreGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasStoreGateway { @@ -21,8 +22,9 @@ public function storeGateway(): StoreGateway /** * Set the provider's store gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useStoreGateway(StoreGateway $gateway): self { $this->storeGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasTextGateway.php b/src/ai/src/Providers/Concerns/HasTextGateway.php index 59fc830d52..4ec28fa57a 100644 --- a/src/ai/src/Providers/Concerns/HasTextGateway.php +++ b/src/ai/src/Providers/Concerns/HasTextGateway.php @@ -6,6 +6,7 @@ use Hypervel\Ai\Contracts\Gateway\StepTextGateway; use Hypervel\Ai\Gateway\TextGenerationLoop; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasTextGateway { @@ -24,8 +25,9 @@ public function textGateway(): StepTextGateway /** * Set the provider's text gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useTextGateway(StepTextGateway $gateway): self { $this->textGateway = $gateway; diff --git a/src/ai/src/Providers/Concerns/HasTranscriptionGateway.php b/src/ai/src/Providers/Concerns/HasTranscriptionGateway.php index 881041e0fa..1efeb295de 100644 --- a/src/ai/src/Providers/Concerns/HasTranscriptionGateway.php +++ b/src/ai/src/Providers/Concerns/HasTranscriptionGateway.php @@ -5,6 +5,7 @@ namespace Hypervel\Ai\Providers\Concerns; use Hypervel\Ai\Contracts\Gateway\TranscriptionGateway; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait HasTranscriptionGateway { @@ -21,8 +22,9 @@ public function transcriptionGateway(): TranscriptionGateway /** * Set the provider's transcription gateway. * - * Boot or tests only for configured providers, which are shared across requests. + * Changing a shared provider's gateway affects subsequent operations. */ + #[BootOrTestsOnly(whenShared: true)] public function useTranscriptionGateway(TranscriptionGateway $gateway): self { $this->transcriptionGateway = $gateway; diff --git a/src/ai/src/Reranking.php b/src/ai/src/Reranking.php index f2d3611f6c..ab4d9e1505 100644 --- a/src/ai/src/Reranking.php +++ b/src/ai/src/Reranking.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Ai\Gateway\FakeRerankingGateway; use Hypervel\Ai\PendingResponses\PendingReranking; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use InvalidArgumentException; @@ -31,8 +32,9 @@ public static function of(Collection|array $documents): PendingReranking /** * Fake reranking operations. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeRerankingGateway { return Ai::fakeReranking($responses); diff --git a/src/ai/src/Stores.php b/src/ai/src/Stores.php index b0d1291640..b64a2944ce 100644 --- a/src/ai/src/Stores.php +++ b/src/ai/src/Stores.php @@ -9,6 +9,7 @@ use Hypervel\Ai\Enums\Lab; use Hypervel\Ai\Gateway\FakeStoreGateway; use Hypervel\Ai\Providers\Provider; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; class Stores @@ -50,8 +51,9 @@ public static function delete(string $storeId, Provider|Lab|string|null $provide /** * Fake store operations. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = [], bool $files = true): FakeStoreGateway { if ($files) { diff --git a/src/ai/src/Transcription.php b/src/ai/src/Transcription.php index d180ee8528..ecfd1d91f8 100644 --- a/src/ai/src/Transcription.php +++ b/src/ai/src/Transcription.php @@ -12,6 +12,7 @@ use Hypervel\Ai\Gateway\FakeTranscriptionGateway; use Hypervel\Ai\PendingResponses\PendingTranscriptionGeneration; use Hypervel\Http\UploadedFile; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; class Transcription @@ -69,8 +70,9 @@ public static function fromUpload(UploadedFile $file): PendingTranscriptionGener /** * Fake transcription generation. * - * Tests only. The fake gateway is shared across requests in the worker. + * The fake gateway is shared across requests in the worker. */ + #[TestsOnly] public static function fake(Closure|array $responses = []): FakeTranscriptionGateway { return Ai::fakeTranscriptions($responses); diff --git a/src/api-client/composer.json b/src/api-client/composer.json index 8ab36b3461..6e03550bad 100644 --- a/src/api-client/composer.json +++ b/src/api-client/composer.json @@ -26,7 +26,7 @@ "require": { "php": "^8.4", "guzzlehttp/psr7": "^3.1", - "psr/http-message": "^2.0", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/container": "^0.4", @@ -35,7 +35,8 @@ "hypervel/guzzle-promises": "^3.1", "hypervel/http": "^0.4", "hypervel/pipeline": "^0.4", - "hypervel/support": "^0.4" + "hypervel/support": "^0.4", + "psr/http-message": "^2.0" }, "autoload": { "psr-4": { diff --git a/src/api-client/src/PendingRequest.php b/src/api-client/src/PendingRequest.php index 34fee56b9c..b53070ad60 100644 --- a/src/api-client/src/PendingRequest.php +++ b/src/api-client/src/PendingRequest.php @@ -18,6 +18,7 @@ use Hypervel\Http\Client\Request as HttpRequest; use Hypervel\Http\Client\Response as HttpResponse; use Hypervel\Pipeline\Pipeline; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Http; use Hypervel\Support\Stringable; @@ -330,6 +331,7 @@ public function async(bool $async = true): static /** * Reject a custom Guzzle client that would bypass the API request pipeline. */ + #[Unsupported('Use setHandler() to configure a request-specific transport handler.')] public function setClient(ClientInterface $client): never { throw new BadMethodCallException( diff --git a/src/attributes/LICENSE.md b/src/attributes/LICENSE.md new file mode 100644 index 0000000000..b16dec17f3 --- /dev/null +++ b/src/attributes/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Hypervel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/src/attributes/README.md b/src/attributes/README.md new file mode 100644 index 0000000000..f47eb7da14 --- /dev/null +++ b/src/attributes/README.md @@ -0,0 +1,6 @@ +Attributes for Hypervel +=== + +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/attributes) + +Documentation: https://hypervel.org/docs/static-analysis#method-attributes diff --git a/src/attributes/composer.json b/src/attributes/composer.json new file mode 100644 index 0000000000..193d8fd9ba --- /dev/null +++ b/src/attributes/composer.json @@ -0,0 +1,41 @@ +{ + "name": "hypervel/attributes", + "description": "The Hypervel Attributes package.", + "license": "MIT", + "type": "library", + "keywords": [ + "php", + "attributes", + "hypervel" + ], + "authors": [ + { + "name": "Albert Chen", + "email": "albert@hypervel.org" + }, + { + "name": "Raj Siva-Rajah", + "homepage": "https://github.com/binaryfire" + } + ], + "support": { + "issues": "https://github.com/hypervel/components/issues", + "source": "https://github.com/hypervel/components" + }, + "require": { + "php": "^8.4" + }, + "autoload": { + "psr-4": { + "Hypervel\\Support\\": "src/" + } + }, + "config": { + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-main": "0.4-dev" + } + } +} diff --git a/src/attributes/src/Attributes/BootOnly.php b/src/attributes/src/Attributes/BootOnly.php new file mode 100644 index 0000000000..30579379e7 --- /dev/null +++ b/src/attributes/src/Attributes/BootOnly.php @@ -0,0 +1,25 @@ +policies[$class] = $policy; @@ -286,11 +291,12 @@ public function policy(string $class, string $policy): static /** * Register a callback to run before all Gate checks. * - * Boot-only. The callback persists on the singleton Gate for the worker - * lifetime and runs during authorization in every subsequent request. + * The callback persists on the singleton Gate for the worker lifetime + * and runs during authorization in every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function before(callable $callback): static { $this->beforeCallbacks[] = $callback; @@ -301,11 +307,12 @@ public function before(callable $callback): static /** * Register a callback to run after all Gate checks. * - * Boot-only. The callback persists on the singleton Gate for the worker - * lifetime and runs during authorization in every subsequent request. + * The callback persists on the singleton Gate for the worker lifetime + * and runs during authorization in every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function after(callable $callback): static { $this->afterCallbacks[] = $callback; @@ -714,11 +721,12 @@ protected function guessPolicyName(string $class): array /** * Specify a callback to be used to guess policy names. * - * Boot-only. The callback persists on the singleton Gate for the worker - * lifetime and affects policy discovery in every subsequent request. + * The callback persists on the singleton Gate for the worker lifetime + * and affects policy discovery in every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function guessPolicyNamesUsing(callable $callback): static { $this->guessPolicyNamesUsingCallback = $callback; @@ -870,11 +878,12 @@ public function policies(): array /** * Set the default denial response for gates and policies. * - * Boot-only. The response persists on the singleton Gate for the worker - * lifetime and affects authorization failures in every subsequent request. + * The response persists on the singleton Gate for the worker lifetime and + * affects authorization failures in every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function defaultDenialResponse(Response $response): static { $this->defaultDenialResponse = $response; @@ -885,12 +894,13 @@ public function defaultDenialResponse(Response $response): static /** * Set the container instance used by the gate. * - * Tests only. Swaps the container on the singleton Gate used by every - * coroutine; per-request use races across coroutines and breaks every - * concurrent authorization check. + * Swaps the container on the singleton Gate used by every coroutine; + * per-request use races across coroutines and breaks every concurrent + * authorization check. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/auth/src/AuthManager.php b/src/auth/src/AuthManager.php index b70379dd64..782be2ab2d 100755 --- a/src/auth/src/AuthManager.php +++ b/src/auth/src/AuthManager.php @@ -10,6 +10,9 @@ use Hypervel\Contracts\Auth\Guard; use Hypervel\Contracts\Auth\StatefulGuard; use Hypervel\Contracts\Container\Container; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\RebindsCallbacksToSelf; use InvalidArgumentException; use ReflectionException; @@ -220,8 +223,12 @@ public function setDefaultDriver(UnitEnum|string $name): void /** * Register a new callback based request guard. * + * The guard creator persists in the singleton's customCreators array for + * the worker lifetime and applies to every subsequent guard resolution. + * * @return $this */ + #[BootOnly(whenShared: true)] public function viaRequest(string $driver, callable $callback): static { return $this->extend($driver, function (Container $app, string $name, array $config) use ($callback): RequestGuard { @@ -293,12 +300,13 @@ public function clearUserCache(mixed $identifier, UnitEnum|string|null $guard = /** * Configure where guests are redirected by the "auth" middleware. * - * Boot-only. The callback persists in authentication middleware and - * exception static properties for the worker lifetime and affects every + * The callback persists in authentication middleware and exception + * static properties for the worker lifetime and affects every * subsequent unauthenticated or session-mismatch request. * * @return $this */ + #[BootOnly] public function redirectGuestsTo(callable|string|null $redirect): static { AuthenticationRedirects::redirectGuestsTo($redirect); @@ -309,12 +317,13 @@ public function redirectGuestsTo(callable|string|null $redirect): static /** * Configure where users are redirected by the "guest" middleware. * - * Boot-only. The callback persists in the guest middleware static property - * for the worker lifetime and affects every subsequent already-authenticated + * The callback persists in the guest middleware static property for the + * worker lifetime and affects every subsequent already-authenticated * guest-route request. * * @return $this */ + #[BootOnly] public function redirectUsersTo(callable|string $redirect): static { AuthenticationRedirects::redirectUsersTo($redirect); @@ -325,12 +334,13 @@ public function redirectUsersTo(callable|string $redirect): static /** * Configure where users are redirected by the authentication and guest middleware. * - * Boot-only. The callbacks persist in authentication middleware and - * exception static properties for the worker lifetime and affect every + * The callbacks persist in authentication middleware and exception + * static properties for the worker lifetime and affect every * subsequent matching request. * * @return $this */ + #[BootOnly] public function redirectTo(callable|string|null $guests = null, callable|string|null $users = null): static { AuthenticationRedirects::redirectTo(guests: $guests, users: $users); @@ -344,11 +354,12 @@ public function redirectTo(callable|string|null $guests = null, callable|string| * Anonymous closures run in this manager's class scope; non-static closures * also receive the manager as $this. * - * Boot-only. The callback persists in the singleton's customCreators array - * for the worker lifetime and applies to every subsequent guard resolution. + * The callback persists in the singleton's customCreators array for the + * worker lifetime and applies to every subsequent guard resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback): static { try { @@ -366,12 +377,13 @@ public function extend(string $driver, Closure $callback): static /** * Register a custom provider creator Closure. * - * Boot-only. The callback persists in the singleton's customProviderCreators - * array for the worker lifetime and applies to every subsequent user - * provider resolution. + * The callback persists in the singleton's customProviderCreators array for + * the worker lifetime and applies to every subsequent user provider + * resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function provider(string $name, Closure $callback): static { $this->customProviderCreators[$name] = $callback; @@ -390,13 +402,14 @@ public function hasResolvedGuards(): bool /** * Forget all of the resolved guard instances. * - * Boot or tests only. Clears the singleton's guard cache; concurrent - * coroutines may already hold references to the prior guard instances - * (each holding its own coroutine-scoped state) that next resolution - * will not share. + * Clears the singleton's guard cache; concurrent coroutines may + * already hold references to the prior guard instances (each holding + * its own coroutine-scoped state) that next resolution will not + * share. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetGuards(): static { $this->guards = []; @@ -438,12 +451,13 @@ public function getAuthContextKeys(): array /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's container reference; per-request use - * races across coroutines and breaks every concurrent request's auth + * Swaps the singleton's container reference; per-request use races + * across coroutines and breaks every concurrent request's auth * resolution. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Container $app): static { $this->app = $app; diff --git a/src/auth/src/AuthenticationException.php b/src/auth/src/AuthenticationException.php index 4bf2dbd1da..98fa53034d 100644 --- a/src/auth/src/AuthenticationException.php +++ b/src/auth/src/AuthenticationException.php @@ -6,6 +6,7 @@ use Exception; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; class AuthenticationException extends Exception { @@ -64,9 +65,10 @@ public function redirectTo(Request $request): ?string /** * Specify the callback that should be used to generate the redirect path. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every unauthenticated exception. + * The callback persists in a static property for the worker lifetime + * and runs on every unauthenticated exception. */ + #[BootOnly] public static function redirectUsing(callable $redirectToCallback): void { static::$redirectToCallback = $redirectToCallback; diff --git a/src/auth/src/AuthenticationRedirects.php b/src/auth/src/AuthenticationRedirects.php index 5df80319ae..289ae66201 100644 --- a/src/auth/src/AuthenticationRedirects.php +++ b/src/auth/src/AuthenticationRedirects.php @@ -7,6 +7,7 @@ use Hypervel\Auth\Middleware\Authenticate; use Hypervel\Auth\Middleware\RedirectIfAuthenticated; use Hypervel\Session\Middleware\AuthenticateSession; +use Hypervel\Support\Attributes\BootOnly; /** * Single source of truth for boot-time auth redirect wiring. @@ -20,10 +21,11 @@ class AuthenticationRedirects /** * Configure where guests are redirected by the "auth" middleware. * - * Boot-only. The callback persists in authentication middleware and - * exception static properties for the worker lifetime and affects every + * The callback persists in authentication middleware and exception + * static properties for the worker lifetime and affects every * subsequent unauthenticated or session-mismatch request. */ + #[BootOnly] public static function redirectGuestsTo(callable|string|null $redirect): void { $redirect = $redirect === null @@ -36,10 +38,11 @@ public static function redirectGuestsTo(callable|string|null $redirect): void /** * Configure where users are redirected by the "guest" middleware. * - * Boot-only. The callback persists in the guest middleware static property - * for the worker lifetime and affects every subsequent already-authenticated + * The callback persists in the guest middleware static property for the + * worker lifetime and affects every subsequent already-authenticated * guest-route request. */ + #[BootOnly] public static function redirectUsersTo(callable|string $redirect): void { self::redirectTo(users: $redirect); @@ -48,10 +51,11 @@ public static function redirectUsersTo(callable|string $redirect): void /** * Configure where users are redirected by the authentication and guest middleware. * - * Boot-only. The callbacks persist in authentication middleware and - * exception static properties for the worker lifetime and affect every + * The callbacks persist in authentication middleware and exception + * static properties for the worker lifetime and affect every * subsequent matching request. */ + #[BootOnly] public static function redirectTo(callable|string|null $guests = null, callable|string|null $users = null): void { $guests = is_string($guests) ? fn () => $guests : $guests; diff --git a/src/auth/src/EloquentUserProvider.php b/src/auth/src/EloquentUserProvider.php index dedab97e9e..fce322a43c 100755 --- a/src/auth/src/EloquentUserProvider.php +++ b/src/auth/src/EloquentUserProvider.php @@ -16,6 +16,8 @@ use Hypervel\Coroutine\Coroutine; use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Model; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use SensitiveParameter; use Throwable; @@ -294,14 +296,17 @@ public function rehashPasswordIfRequired(UserContract $user, #[SensitiveParamete * rejected store leaves the provider's existing cache configuration unchanged and * does not register a descriptor or model event listeners. * - * Boot-only. User providers are held by cached guards; runtime use mutates - * the provider used by every subsequent authentication lookup. + * User providers are held by cached guards; runtime use on a provider a + * guard already holds changes every subsequent authentication lookup. The + * model's worker-wide cache invalidation is registered once, so enabling + * the cache on a newly created provider is safe. * * @param null|array $tags optional tag names enabling tag-based bulk flush; requires any-mode tag support * @return $this * * @throws InvalidArgumentException when the TTL, tags, or resolved store are not supported */ + #[BootOnly(whenShared: true)] public function enableCache( ?string $storeName, int $ttl = self::DEFAULT_CACHE_TTL, @@ -388,11 +393,12 @@ public function clearUserCache(mixed $identifier): void * The fully qualified model class name is always included in the key * automatically. The resolver only controls the identifier segment. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every cached user lookup and invalidation. + * The resolver persists in a static property for the worker lifetime + * and runs on every cached user lookup and invalidation. * * @param Closure(mixed, class-string, null|(Model&UserContract)): string $callback */ + #[BootOnly] public static function resolveUserCacheKeyUsing(Closure $callback): void { static::$cacheKeyResolver = $callback; @@ -409,11 +415,12 @@ public static function resolveUserCacheKeyUsing(Closure $callback): void * (per-provider, from auth.providers.*.cache.tags) unioned with the * resolver's return value. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every cached user write. + * The resolver persists in a static property for the worker lifetime + * and runs on every cached user write. * * @param Closure(): list $callback */ + #[BootOnly] public static function resolveUserCacheTagsUsing(Closure $callback): void { static::$cacheTagsResolver = $callback; @@ -638,11 +645,12 @@ public function getHasher(): HasherContract /** * Set the hasher implementation. * - * Boot or tests only. User providers are held by cached guards; runtime use - * mutates password verification for every subsequent authentication lookup. + * User providers are held by cached guards; runtime use mutates password + * verification for every subsequent authentication lookup. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setHasher(HasherContract $hasher): static { $this->hasher = $hasher; @@ -663,12 +671,13 @@ public function getModel(): string /** * Set the name of the Eloquent user model. * - * Boot or tests only. User providers are held by cached guards; runtime use - * changes the model used by every subsequent authentication lookup. + * User providers are held by cached guards; runtime use changes the model + * used by every subsequent authentication lookup. * * @param class-string $model * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setModel(string $model): static { $this->model = $model; @@ -693,12 +702,13 @@ public function getQueryCallback(): ?Closure /** * Set the callback to modify the query before retrieving users. * - * Boot or tests only. User providers are held by cached guards; runtime use - * mutates the query applied to every subsequent authentication lookup. + * User providers are held by cached guards; runtime use mutates the query + * applied to every subsequent authentication lookup. * * @param null|(Closure(Builder<*>):mixed) $queryCallback * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function withQuery(?Closure $queryCallback = null): static { $this->queryCallback = $queryCallback; diff --git a/src/auth/src/GuardHelpers.php b/src/auth/src/GuardHelpers.php index bb25cb2809..ae8f1bd435 100644 --- a/src/auth/src/GuardHelpers.php +++ b/src/auth/src/GuardHelpers.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Auth\Authenticatable as AuthenticatableContract; use Hypervel\Contracts\Auth\UserProvider; +use Hypervel\Support\Attributes\BootOrTestsOnly; /** * These methods are typically the same across all guards. @@ -28,6 +29,24 @@ trait GuardHelpers */ protected ?UserProvider $provider = null; + /** + * The sequence used to build unique per-guard context key prefixes. + * + * Each class using this trait has its own sequence, so the prefix also + * includes that class. + */ + // Do not reset this; live context entries may still use earlier prefixes. + private static int $contextKeySequence = 0; + + /** + * The prefix of this guard's coroutine context keys. + * + * Users and credentials cached in context belong to the guard that + * resolved them, so a guard created later with the same name, such as + * after AuthManager::forgetGuards(), starts without them. + */ + private ?string $contextKeyPrefix = null; + /** * Determine if the current user is authenticated. If not, throw an exception. * @@ -75,13 +94,22 @@ public function getProvider(): ?UserProvider /** * Set the user provider used by the guard. * - * Boot or tests only. The provider is stored on the worker-lifetime guard - * and affects every subsequent request. + * The provider is stored on the worker-lifetime guard and affects every + * subsequent request. * * @param TProvider $provider */ + #[BootOrTestsOnly(whenShared: true)] public function setProvider(UserProvider $provider): void { $this->provider = $provider; } + + /** + * Get this guard's coroutine context key for the given entry. + */ + protected function contextKey(string $key): string + { + return ($this->contextKeyPrefix ??= '__auth.guards.' . self::class . '.' . ++self::$contextKeySequence . '.') . $key; + } } diff --git a/src/auth/src/Middleware/Authenticate.php b/src/auth/src/Middleware/Authenticate.php index c254d96efd..81d54f4b94 100644 --- a/src/auth/src/Middleware/Authenticate.php +++ b/src/auth/src/Middleware/Authenticate.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Auth\Factory as Auth; use Hypervel\Contracts\Auth\Middleware\AuthenticatesRequests; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\HttpFoundation\Response; class Authenticate implements AuthenticatesRequests @@ -99,10 +100,11 @@ protected function redirectTo(Request $request): ?string /** * Specify the callback that should be used to generate the redirect path. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every unauthenticated request handled by this + * The callback persists in a static property for the worker lifetime + * and runs on every unauthenticated request handled by this * middleware. */ + #[BootOnly] public static function redirectUsing(callable $redirectToCallback): void { static::$redirectToCallback = $redirectToCallback; diff --git a/src/auth/src/Middleware/RedirectIfAuthenticated.php b/src/auth/src/Middleware/RedirectIfAuthenticated.php index 4175213896..0057e214ad 100644 --- a/src/auth/src/Middleware/RedirectIfAuthenticated.php +++ b/src/auth/src/Middleware/RedirectIfAuthenticated.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Auth; use Hypervel\Support\Facades\Route; use Symfony\Component\HttpFoundation\Response; @@ -84,10 +85,10 @@ protected function defaultRedirectUri(): string /** * Specify the callback that should be used to generate the redirect path. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every already-authenticated request handled by this - * middleware. + * The callback persists in a static property for the worker lifetime and + * runs on every already-authenticated request handled by this middleware. */ + #[BootOnly] public static function redirectUsing(callable $redirectToCallback): void { static::$redirectToCallback = $redirectToCallback; diff --git a/src/auth/src/Notifications/ResetPassword.php b/src/auth/src/Notifications/ResetPassword.php index 8293574820..5183d68931 100644 --- a/src/auth/src/Notifications/ResetPassword.php +++ b/src/auth/src/Notifications/ResetPassword.php @@ -13,6 +13,7 @@ use Hypervel\Contracts\Mail\Mailable; use Hypervel\Notifications\Messages\MailMessage; use Hypervel\Notifications\Notification; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Lang; use Hypervel\Support\Facades\Password; use InvalidArgumentException; @@ -117,11 +118,12 @@ protected function resetUrl(mixed $notifiable): string /** * Set a callback that should be used when creating the reset password button URL. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every reset-password notification. + * The callback persists in a static property for the worker lifetime + * and runs on every reset-password notification. * * @param Closure(mixed, string): string $callback */ + #[BootOnly] public static function createUrlUsing(Closure $callback): void { static::$createUrlCallback = $callback; @@ -130,11 +132,12 @@ public static function createUrlUsing(Closure $callback): void /** * Set a callback that should be used when building the notification mail message. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every reset-password notification. + * The callback persists in a static property for the worker lifetime + * and runs on every reset-password notification. * * @param Closure(mixed, string): (Mailable|MailMessage) $callback */ + #[BootOnly] public static function toMailUsing(Closure $callback): void { static::$toMailCallback = $callback; diff --git a/src/auth/src/Notifications/VerifyEmail.php b/src/auth/src/Notifications/VerifyEmail.php index 5e72f35e14..41b490c2d3 100644 --- a/src/auth/src/Notifications/VerifyEmail.php +++ b/src/auth/src/Notifications/VerifyEmail.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Mail\Mailable; use Hypervel\Notifications\Messages\MailMessage; use Hypervel\Notifications\Notification; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Config; use Hypervel\Support\Facades\Lang; @@ -85,11 +86,12 @@ protected function verificationUrl(mixed $notifiable): string /** * Set a callback that should be used when creating the email verification URL. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every email-verification notification. + * The callback persists in a static property for the worker lifetime + * and runs on every email-verification notification. * * @param Closure(mixed): string $callback */ + #[BootOnly] public static function createUrlUsing(Closure $callback): void { static::$createUrlCallback = $callback; @@ -98,11 +100,12 @@ public static function createUrlUsing(Closure $callback): void /** * Set a callback that should be used when building the notification mail message. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every email-verification notification. + * The callback persists in a static property for the worker lifetime + * and runs on every email-verification notification. * * @param Closure(mixed, string): (Mailable|MailMessage) $callback */ + #[BootOnly] public static function toMailUsing(Closure $callback): void { static::$toMailCallback = $callback; diff --git a/src/auth/src/Passwords/PasswordBroker.php b/src/auth/src/Passwords/PasswordBroker.php index d4bae35166..62d9c2973a 100755 --- a/src/auth/src/Passwords/PasswordBroker.php +++ b/src/auth/src/Passwords/PasswordBroker.php @@ -12,6 +12,7 @@ use Hypervel\Contracts\Auth\UserProvider; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Timebox; use SensitiveParameter; use UnexpectedValueException; @@ -200,9 +201,10 @@ public function getTimebox(): Timebox /** * Set the event dispatcher instance. * - * Boot or tests only. The dispatcher is stored on the worker-lifetime broker - * and affects every subsequent password reset-link event. + * The dispatcher is stored on the worker-lifetime broker and affects every + * subsequent password reset-link event. */ + #[BootOrTestsOnly(whenShared: true)] public function setDispatcher(Dispatcher $events): void { $this->events = $events; diff --git a/src/auth/src/Passwords/PasswordBrokerManager.php b/src/auth/src/Passwords/PasswordBrokerManager.php index 1e34ab5443..27a1355a42 100644 --- a/src/auth/src/Passwords/PasswordBrokerManager.php +++ b/src/auth/src/Passwords/PasswordBrokerManager.php @@ -10,6 +10,7 @@ use Hypervel\Contracts\Auth\PasswordBrokerFactory as FactoryContract; use Hypervel\Contracts\Container\Container; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use UnitEnum; @@ -196,11 +197,12 @@ public function setDefaultDriver(UnitEnum|string $name): void /** * Refresh the event dispatcher on resolved brokers. * - * Boot or tests only. Replaces the dispatcher on every resolved concrete - * broker for the worker lifetime; per-request use races across coroutines. - * Reached by Event::fake() / Event::fakeFor() so cached brokers follow the - * active dispatcher and its later restoration. + * Replaces the dispatcher on every resolved concrete broker for the worker + * lifetime; per-request use races across coroutines. Reached by + * Event::fake() / Event::fakeFor() so cached brokers follow the active + * dispatcher and its later restoration. */ + #[BootOrTestsOnly(whenShared: true)] public function refreshEventDispatcher(Dispatcher $events): void { foreach ($this->brokers as $broker) { diff --git a/src/auth/src/RequestGuard.php b/src/auth/src/RequestGuard.php index 25e793966d..c59b81daf5 100644 --- a/src/auth/src/RequestGuard.php +++ b/src/auth/src/RequestGuard.php @@ -33,10 +33,6 @@ class RequestGuard implements Guard /** * Create a new authentication guard. - * - * The $name parameter is a Hypervel addition for coroutine-safe Context - * keying. Each named guard needs a unique Context key so multiple - * RequestGuard instances don't collide. */ public function __construct( protected string $name, @@ -156,7 +152,7 @@ public function getAuthContextKeys(): array */ protected function getExplicitUserContextKey(): string { - return "__auth.guards.{$this->name}.user.explicit"; + return $this->contextKey('user.explicit'); } /** @@ -164,7 +160,7 @@ protected function getExplicitUserContextKey(): string */ protected function getResolvedUserContextKey(): string { - return "__auth.guards.{$this->name}.user.resolved"; + return $this->contextKey('user.resolved'); } /** diff --git a/src/auth/src/SessionGuard.php b/src/auth/src/SessionGuard.php index 7c6319e90d..adb30dc6df 100644 --- a/src/auth/src/SessionGuard.php +++ b/src/auth/src/SessionGuard.php @@ -23,6 +23,8 @@ use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Session\Session; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Facades\Hash; use Hypervel\Support\Str; use Hypervel\Support\Timebox; @@ -111,8 +113,8 @@ public function __construct( $classHash = hash('xxh128', static::class); $this->hashedName = 'login_' . $this->name . '_' . $classHash; $this->hashedRecallerName = 'remember_' . $this->name . '_' . $classHash; - $this->userContextKey = "__auth.guards.{$this->name}.user"; - $this->missingUserContextKey = "__auth.guards.{$this->name}.missing_user"; + $this->userContextKey = $this->contextKey('user'); + $this->missingUserContextKey = $this->contextKey('missing_user'); } /** @@ -705,8 +707,8 @@ protected function rehashUserPasswordForDeviceLogout(#[SensitiveParameter] strin /** * Register an authentication attempt event listener. * - * 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. */ public function attempting(callable $callback): void { @@ -814,11 +816,12 @@ protected function getRememberDuration(): int /** * Set the number of minutes the remember me cookie should be valid for. * - * Boot-only. The duration is stored on the worker-lifetime guard and affects - * every subsequent request. + * The duration is stored on the worker-lifetime guard and affects every + * subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function setRememberDuration(int $minutes): static { $this->rememberDuration = $minutes; @@ -843,9 +846,10 @@ public function getCookieJar(): CookieJar /** * Set the cookie creator instance used by the guard. * - * Boot or tests only. The cookie jar is stored on the worker-lifetime guard - * and affects every subsequent request. + * The cookie jar is stored on the worker-lifetime guard and affects every + * subsequent request. */ + #[BootOrTestsOnly(whenShared: true)] public function setCookieJar(CookieJar $cookie): void { $this->cookie = $cookie; @@ -862,9 +866,10 @@ public function getDispatcher(): ?Dispatcher /** * Set the event dispatcher instance. * - * Boot or tests only. The dispatcher is stored on the worker-lifetime guard - * and affects every subsequent request. + * The dispatcher is stored on the worker-lifetime guard and affects every + * subsequent request. */ + #[BootOrTestsOnly(whenShared: true)] public function setDispatcher(Dispatcher $events): void { $this->events = $events; @@ -935,7 +940,7 @@ public function getAuthContextKeys(): array { return [ $this->userContextKey, - $this->getContextStateKey('loggedOut'), + $this->contextKey('loggedOut'), ]; } @@ -963,7 +968,7 @@ public function getTimebox(): Timebox */ protected function getContextState(string $key, mixed $default = null): mixed { - return CoroutineContext::get($this->getContextStateKey($key), $default); + return CoroutineContext::get($this->contextKey($key), $default); } /** @@ -971,15 +976,7 @@ protected function getContextState(string $key, mixed $default = null): mixed */ protected function setContextState(string $key, mixed $value): void { - CoroutineContext::set($this->getContextStateKey($key), $value); - } - - /** - * Get a per-request state Context key. - */ - protected function getContextStateKey(string $key): string - { - return "__auth.guards.{$this->name}.{$key}"; + CoroutineContext::set($this->contextKey($key), $value); } /** diff --git a/src/auth/src/TokenGuard.php b/src/auth/src/TokenGuard.php index cb6018b895..04cafc623a 100644 --- a/src/auth/src/TokenGuard.php +++ b/src/auth/src/TokenGuard.php @@ -32,7 +32,6 @@ class TokenGuard implements Guard /** * Create a new authentication guard. * - * @param string $name the name of the guard for Context keying * @param string $inputKey the name of the query string item from the request containing the API token * @param string $storageKey the name of the token "column" in persistent storage * @param bool $hash indicates if the API token is hashed in storage @@ -196,7 +195,7 @@ public function getAuthContextKeys(): array */ protected function getExplicitUserContextKey(): string { - return "__auth.guards.{$this->name}.user.explicit"; + return $this->contextKey('user.explicit'); } /** @@ -205,10 +204,10 @@ protected function getExplicitUserContextKey(): string protected function getContextKeyForToken(?string $token): string { if ($token === null || $token === '') { - return "__auth.guards.{$this->name}.user.default"; + return $this->contextKey('user.default'); } - return "__auth.guards.{$this->name}.user." . hash('xxh128', $token, options: ['seed' => $this->hashSeed]); + return $this->contextKey('user.' . hash('xxh128', $token, options: ['seed' => $this->hashSeed])); } /** diff --git a/src/broadcasting/composer.json b/src/broadcasting/composer.json index 9e0c4a70cc..27fe1fa9ae 100644 --- a/src/broadcasting/composer.json +++ b/src/broadcasting/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/bus": "^0.4", "hypervel/collections": "^0.4", "hypervel/connection-pool": "^0.4", diff --git a/src/broadcasting/src/BroadcastManager.php b/src/broadcasting/src/BroadcastManager.php index 2e58f8f5ed..1f6cfa9c20 100644 --- a/src/broadcasting/src/BroadcastManager.php +++ b/src/broadcasting/src/BroadcastManager.php @@ -46,6 +46,9 @@ use Hypervel\Redis\RedisConfig; use Hypervel\Routing\Router; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Queue\Concerns\ResolvesQueueRoutes; use Hypervel\Support\RebindsCallbacksToSelf; use InvalidArgumentException; @@ -576,8 +579,9 @@ public function getDefaultDriver(): string /** * Set the default broadcast driver name. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultDriver(UnitEnum|string $name): void { $name = $name instanceof UnitEnum ? (string) enum_value($name) : $name; @@ -588,9 +592,9 @@ public function setDefaultDriver(UnitEnum|string $name): void /** * Disconnect the given driver and remove it from the local cache. * - * Boot or tests only, plus operational recovery for pooled drivers. - * Mercure and explicitly pooled drivers also close their shared pools. - * Other connections sharing a pool acquire a fresh one on their next operation. + * Use this to recover broken pooled resources. Mercure and explicitly + * pooled drivers also close their shared pools. Other connections sharing + * a pool acquire a fresh one on their next operation. */ public function purge(UnitEnum|string|null $name = null): void { @@ -640,12 +644,12 @@ public function purge(UnitEnum|string|null $name = null): void * Anonymous closures run in this manager's class scope; non-static closures * also receive the manager as $this. * - * Boot-only. The callback persists in the singleton's customCreators array - * for the worker lifetime and applies to every subsequent broadcaster - * resolution. + * The callback persists in the singleton's customCreators array for the + * worker lifetime and applies to every subsequent broadcaster resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback): static { try { @@ -679,12 +683,13 @@ public function getApplication(): Container /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's container reference; per-request use - * races across coroutines and breaks every concurrent request resolving + * Swaps the singleton's container reference; per-request use races + * across coroutines and breaks every concurrent request resolving * broadcasters through this manager. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Container $app): static { $this->app = $app; @@ -695,11 +700,12 @@ public function setApplication(Container $app): static /** * Forget all of the resolved driver instances. * - * Boot or tests only. This is cache-only: pooled broadcasters remain - * shared resources until purged or reclaimed by their idle TTL. + * This is cache-only: pooled broadcasters remain shared resources + * until purged or reclaimed by their idle TTL. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetDrivers(): static { $this->drivers = []; diff --git a/src/broadcasting/src/BroadcastPoolProxy.php b/src/broadcasting/src/BroadcastPoolProxy.php index 6783a0ebc4..bd95dfbf68 100644 --- a/src/broadcasting/src/BroadcastPoolProxy.php +++ b/src/broadcasting/src/BroadcastPoolProxy.php @@ -10,6 +10,7 @@ use Hypervel\Contracts\Broadcasting\HasBroadcastChannel; use Hypervel\Http\Request; use Hypervel\ObjectPool\PoolProxy; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use RuntimeException; @@ -31,9 +32,10 @@ public function resolveAuthenticatedUser(Request $request): ?array /** * Register the user retrieval callback used to authenticate connections. * - * Boot-only. The callback persists in instance state on the cached pool - * proxy for the worker lifetime; per-request use races across coroutines. + * The callback persists in instance state on the cached pool proxy for + * the worker lifetime; per-request use races across coroutines. */ + #[BootOnly(whenShared: true)] public function resolveAuthenticatedUserUsing(?Closure $callback): void { $this->authenticatedUserCallback = $callback; @@ -42,10 +44,11 @@ public function resolveAuthenticatedUserUsing(?Closure $callback): void /** * Register a channel authenticator. * - * Boot-only. Delegates to the underlying broadcaster; the channel - * authorizer and options persist in shared static state on the - * Broadcaster class for the worker lifetime. + * Delegates to the underlying broadcaster; the channel authorizer + * and options persist in shared static state on the Broadcaster + * class for the worker lifetime. */ + #[BootOnly] public function channel(HasBroadcastChannel|string $channel, callable|string $callback, array $options = []): static { $this->invoke(__FUNCTION__, func_get_args()); diff --git a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php index c24f384d55..035af821c2 100644 --- a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php @@ -11,6 +11,7 @@ use Hypervel\Contracts\Container\Container; use Hypervel\Database\ConnectionResolver; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Str; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -222,9 +223,10 @@ public function getAbly(): AblyRest /** * Set the underlying Ably SDK instance. * - * Boot or tests only. Replaces the SDK client on this worker-cached broadcaster; - * per-request mutation races across coroutines. + * Replaces the SDK client on this worker-cached broadcaster; per-request + * mutation races across coroutines. */ + #[BootOrTestsOnly(whenShared: true)] public function setAbly(AblyRest $ably): void { $this->ably = $ably; diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index 5072dca7e3..44bea46d4b 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -13,6 +13,8 @@ use Hypervel\Contracts\Routing\UrlRoutable; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Reflector; use ReflectionClass; @@ -76,12 +78,12 @@ public function resolveAuthenticatedUser(Request $request): ?array /** * Register the user retrieval callback used to authenticate connections. * - * Boot-only. The callback persists in instance state on the cached - * broadcaster for the worker lifetime; per-request use races across - * coroutines. + * The callback persists in instance state on the cached broadcaster + * for the worker lifetime; per-request use races across coroutines. * * See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication. */ + #[BootOnly(whenShared: true)] public function resolveAuthenticatedUserUsing(?Closure $callback): void { $this->authenticatedUserCallback = $callback; @@ -90,12 +92,13 @@ public function resolveAuthenticatedUserUsing(?Closure $callback): void /** * Register a channel authenticator. * - * Boot-only. The channel authorizer and options persist in shared static - * state on the Broadcaster class for the worker lifetime and apply to - * every subsequent connection authentication. + * The channel authorizer and options persist in shared static state on + * the Broadcaster class for the worker lifetime and apply to every + * subsequent connection authentication. * * @return $this */ + #[BootOnly] public function channel(HasBroadcastChannel|string $channel, callable|string $callback, array $options = []): static { if ($channel instanceof HasBroadcastChannel) { @@ -114,9 +117,10 @@ public function channel(HasBroadcastChannel|string $channel, callable|string $ca /** * Register the outgoing channel formatter. * - * Boot-only. The callback persists in shared static state for the worker - * lifetime and applies to every broadcaster across all coroutines. + * The callback persists in shared static state for the worker lifetime + * and applies to every broadcaster across all coroutines. */ + #[BootOrTestsOnly] public static function formatChannelsUsing(?Closure $callback): void { static::$channelFormatter = $callback; @@ -133,9 +137,10 @@ public static function channelFormatter(): ?Closure /** * Register the incoming channel authorizer. * - * Boot-only. The callback persists in shared static state for the worker - * lifetime and applies to every broadcaster across all coroutines. + * The callback persists in shared static state for the worker lifetime + * and applies to every broadcaster across all coroutines. */ + #[BootOrTestsOnly] public static function authorizeChannelsUsing(?Closure $callback): void { static::$channelAuthorizer = $callback; diff --git a/src/broadcasting/src/Broadcasters/MercureBroadcaster.php b/src/broadcasting/src/Broadcasters/MercureBroadcaster.php index 80d4931cd8..647b058297 100644 --- a/src/broadcasting/src/Broadcasters/MercureBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/MercureBroadcaster.php @@ -12,6 +12,7 @@ use Hypervel\Http\JsonResponse; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use JsonException; use RuntimeException; @@ -278,9 +279,10 @@ public function getHub(): HubInterface /** * Set the Mercure hub instance. * - * Boot or tests only. Replaces the hub on this worker-cached broadcaster - * and updates the worker-wide cookie encryption exemptions. + * Replaces the hub on this worker-cached broadcaster and updates the + * worker-wide cookie encryption exemptions. */ + #[BootOrTestsOnly] public function setHub(HubInterface $hub): void { $this->hub = $hub; diff --git a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php index 202e056627..dc8f004e1f 100644 --- a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php @@ -9,6 +9,7 @@ use Hypervel\Database\ConnectionResolver; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Pusher\ApiErrorException; use Pusher\Pusher; @@ -164,9 +165,10 @@ public function getPusher(): Pusher /** * Set the Pusher SDK instance. * - * Boot or tests only. Replaces the SDK client on this worker-cached broadcaster; - * per-request mutation races across coroutines. + * Replaces the SDK client on this worker-cached broadcaster; per-request + * mutation races across coroutines. */ + #[BootOrTestsOnly(whenShared: true)] public function setPusher(Pusher $pusher): void { $this->pusher = $pusher; diff --git a/src/bus/composer.json b/src/bus/composer.json index 89347e872b..842bf60594 100644 --- a/src/bus/composer.json +++ b/src/bus/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", diff --git a/src/bus/src/DispatchLockContext.php b/src/bus/src/DispatchLockContext.php index 8361964c24..9da0f981a3 100644 --- a/src/bus/src/DispatchLockContext.php +++ b/src/bus/src/DispatchLockContext.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Contracts\Cache\Repository as Cache; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; use Throwable; use WeakMap; @@ -51,13 +52,14 @@ class DispatchLockContext /** * Register a callback that captures the context needed for deferred lock release. * - * Boot-only. Callbacks persist for the worker lifetime and affect subsequent dispatches. - * Return a wrapper that invokes the release operation once and restores the previous - * context in finally, or null when no context is needed. The first wrapper is outermost. - * Passing null clears registrations; capture runs before acquiring a lock. + * Callbacks persist for the worker lifetime and affect subsequent dispatches. Return a + * wrapper that invokes the release operation once and restores the previous context in + * finally, or null when no context is needed. The first wrapper is outermost. Passing + * null clears registrations; capture runs before acquiring a lock. * * @param null|callable(): (null|Closure(Closure(): void): void) $callback */ + #[BootOnly] public static function captureReleaseContextUsing(?callable $callback): void { if ($callback === null) { diff --git a/src/bus/src/Dispatcher.php b/src/bus/src/Dispatcher.php index 2ef692789f..13f332ae15 100644 --- a/src/bus/src/Dispatcher.php +++ b/src/bus/src/Dispatcher.php @@ -17,6 +17,7 @@ use Hypervel\Queue\Attributes\Queue as QueueAttribute; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\Jobs\SyncJob; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Queue\Concerns\ResolvesQueueRoutes; use Hypervel\Support\Traits\ReadsClassAttributes; @@ -296,12 +297,13 @@ protected function queueRoutesContainer(): Container /** * Set the pipes through which commands should be piped before dispatching. * - * Boot-only. The pipes persist on the singleton dispatcher for the worker - * lifetime and apply to every subsequent dispatch; per-request use races - * across coroutines. + * The pipes persist on the singleton dispatcher for the worker lifetime + * and apply to every subsequent dispatch; per-request use races across + * coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function pipeThrough(array $pipes): static { $this->pipes = $pipes; @@ -312,11 +314,12 @@ public function pipeThrough(array $pipes): static /** * Map a command to a handler. * - * Boot-only. The handler map persists on the singleton dispatcher for the - * worker lifetime; per-request use races across coroutines. + * The handler map persists on the singleton dispatcher for the worker + * lifetime; per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function map(array $map): static { $this->handlers = array_merge($this->handlers, $map); @@ -327,11 +330,12 @@ public function map(array $map): static /** * Allow dispatching after responses. * - * Boot-only. Toggles a flag on the singleton dispatcher for the worker - * lifetime; per-request use races across coroutines. + * Toggles a flag on the singleton dispatcher for the worker lifetime; + * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function withDispatchingAfterResponses(): static { $this->allowsDispatchingAfterResponses = true; @@ -342,11 +346,12 @@ public function withDispatchingAfterResponses(): static /** * Disable dispatching after responses. * - * Boot-only. Toggles a flag on the singleton dispatcher for the worker - * lifetime; per-request use races across coroutines. + * Toggles a flag on the singleton dispatcher for the worker lifetime; + * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function withoutDispatchingAfterResponses(): static { $this->allowsDispatchingAfterResponses = false; diff --git a/src/cache/composer.json b/src/cache/composer.json index a21febac1f..7b9128bf65 100644 --- a/src/cache/composer.json +++ b/src/cache/composer.json @@ -27,6 +27,7 @@ "php": "^8.4", "ext-filter": "*", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/config": "^0.4", "hypervel/console": "^0.4", diff --git a/src/cache/src/AnyModeTaggedCache.php b/src/cache/src/AnyModeTaggedCache.php index 91df4c8c65..4d43348021 100644 --- a/src/cache/src/AnyModeTaggedCache.php +++ b/src/cache/src/AnyModeTaggedCache.php @@ -15,6 +15,7 @@ use Hypervel\Cache\Events\KeyRetrievalFailed; use Hypervel\Cache\Events\RetrievingKey; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\Unsupported; use Throwable; use UnitEnum; @@ -35,6 +36,7 @@ abstract class AnyModeTaggedCache extends TaggedCache * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::get() with the full key.')] public function get(array|UnitEnum|string $key, mixed $default = null): mixed { throw new BadMethodCallException( @@ -48,6 +50,7 @@ public function get(array|UnitEnum|string $key, mixed $default = null): mixed * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::get() with the full key.')] public function getRaw(UnitEnum|string $key): mixed { throw new BadMethodCallException( @@ -61,6 +64,7 @@ public function getRaw(UnitEnum|string $key): mixed * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::get() with the full key.')] public function getAuthoritativeRaw(UnitEnum|string $key): mixed { throw new BadMethodCallException( @@ -74,6 +78,7 @@ public function getAuthoritativeRaw(UnitEnum|string $key): mixed * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::many() with the full keys.')] public function many(array $keys): array { throw new BadMethodCallException( @@ -87,6 +92,7 @@ public function many(array $keys): array * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::many() with the full keys.')] public function manyRaw(array $keys): array { throw new BadMethodCallException( @@ -100,6 +106,7 @@ public function manyRaw(array $keys): array * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::has() with the full key.')] public function has(array|UnitEnum|string $key): bool { throw new BadMethodCallException( @@ -113,6 +120,7 @@ public function has(array|UnitEnum|string $key): bool * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::pull() with the full key.')] public function pull(UnitEnum|string $key, mixed $default = null): mixed { throw new BadMethodCallException( @@ -126,6 +134,7 @@ public function pull(UnitEnum|string $key, mixed $default = null): mixed * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Use Cache::forget() with the full key, or flush() to remove all tagged items.')] public function forget(array|UnitEnum|string $key): bool { throw new BadMethodCallException( @@ -139,6 +148,7 @@ public function forget(array|UnitEnum|string $key): bool * * @throws BadMethodCallException always - tags are for writing and flushing only */ + #[Unsupported('Put the item again through tags() to change its TTL.')] public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int $ttl): bool { throw new BadMethodCallException( diff --git a/src/cache/src/ArrayStore.php b/src/cache/src/ArrayStore.php index 4d4f88306d..aea8161022 100644 --- a/src/cache/src/ArrayStore.php +++ b/src/cache/src/ArrayStore.php @@ -51,6 +51,26 @@ public function __construct( $this->locksContextKey = self::LOCKS_CONTEXT_KEY_PREFIX . $suffix; } + /** + * Give the cloned store its own coroutine context entries. + * + * The clone starts with the values and locks stored in the current + * coroutine, but later changes through either store leave the other's + * entries untouched. + */ + public function __clone(): void + { + $items = $this->getCacheItems(); + $records = $this->getLockRecords(); + $suffix = (string) ++self::$contextKeySequence; + + $this->storageContextKey = self::STORAGE_CONTEXT_KEY_PREFIX . $suffix; + $this->locksContextKey = self::LOCKS_CONTEXT_KEY_PREFIX . $suffix; + + CoroutineContext::set($this->storageContextKey, $items); + CoroutineContext::set($this->locksContextKey, $records); + } + /** * Get the cached item for the given key. * diff --git a/src/cache/src/CacheManager.php b/src/cache/src/CacheManager.php index b88cf6d6b3..e9823702ca 100644 --- a/src/cache/src/CacheManager.php +++ b/src/cache/src/CacheManager.php @@ -15,6 +15,9 @@ use Hypervel\Contracts\Redis\Factory as RedisFactory; use Hypervel\Contracts\Session\Session; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\RebindsCallbacksToSelf; use Hypervel\Support\Str; use InvalidArgumentException; @@ -41,6 +44,20 @@ class CacheManager implements FactoryContract */ protected const string MEMOIZED_CONTEXT_KEY_PREFIX = '__cache.memoized.'; + /** + * The sequence used to build unique per-manager memo context keys. + */ + // Do not reset this; live context entries may still use earlier prefixes. + private static int $memoizedContextKeySequence = 0; + + /** + * The prefix of this manager's memoized repository context keys. + * + * Memoized repositories wrap this manager's stores, so each manager keys + * them with its own prefix. + */ + private ?string $memoizedContextKeyPrefix = null; + /** * The array of resolved cache stores. */ @@ -107,11 +124,19 @@ public function memo(UnitEnum|string|null $driver = null): CacheRepository // directly in coroutine context because coroutine teardown is the // request reset boundary. return CoroutineContext::getOrSet( - self::MEMOIZED_CONTEXT_KEY_PREFIX . $driver, + $this->memoizedContextKey($driver), fn (): CacheRepository => $this->createMemoizedRepository($driver), ); } + /** + * Get this manager's coroutine context key for the given memoized driver. + */ + protected function memoizedContextKey(string $driver): string + { + return ($this->memoizedContextKeyPrefix ??= self::MEMOIZED_CONTEXT_KEY_PREFIX . ++self::$memoizedContextKeySequence . '.') . $driver; + } + /** * Create a memoized repository for the given driver. */ @@ -449,11 +474,12 @@ protected function setEventDispatcher(Repository $repository): void /** * Re-set the event dispatcher on resolved event-emitting cache repositories. * - * Boot or tests only. Replaces the dispatcher on cached concrete repositories - * that already emit events, preserving disabled stores and custom repository - * implementations. Per-request use races across coroutines. Reached by - * Event::fake() / Event::fakeFor() to install and restore fake dispatchers. + * Replaces the dispatcher on cached concrete repositories that already emit + * events, preserving disabled stores and custom repository implementations. + * Per-request use races across coroutines. Reached by Event::fake() / + * Event::fakeFor() to install and restore fake dispatchers. */ + #[BootOrTestsOnly(whenShared: true)] public function refreshEventDispatcher(): void { foreach ($this->stores as $repository) { @@ -498,8 +524,9 @@ public function getDefaultDriver(): string /** * Set the default cache driver name. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultDriver(UnitEnum|string $name): void { if ($name instanceof UnitEnum) { @@ -512,26 +539,27 @@ public function setDefaultDriver(UnitEnum|string $name): void /** * Unset the given driver instances. * - * Boot or tests only. Mutates the singleton's store cache; concurrent - * coroutines may already hold a reference to the store and next resolution - * will rebuild with fresh connections. + * Mutates the singleton's store cache; concurrent coroutines may already + * hold a reference to the store and next resolution will rebuild with + * fresh connections. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetDriver(array|UnitEnum|string|null $name = null): static { $name ??= $this->getDefaultDriver(); foreach (is_array($name) ? $name : [$name] as $cacheName) { - if ($cacheName instanceof UnitEnum) { - $cacheName = (string) enum_value($cacheName); - } + // Numeric store names reach here as integers when taken from the + // configured store keys. + $cacheName = $cacheName instanceof UnitEnum ? (string) enum_value($cacheName) : (string) $cacheName; if (isset($this->stores[$cacheName])) { unset($this->stores[$cacheName]); } - CoroutineContext::forget(self::MEMOIZED_CONTEXT_KEY_PREFIX . $cacheName); + CoroutineContext::forget($this->memoizedContextKey($cacheName)); } return $this; @@ -540,10 +568,11 @@ public function forgetDriver(array|UnitEnum|string|null $name = null): static /** * Disconnect the given driver and remove from local cache. * - * Boot or tests only. Mutates the singleton's store cache; concurrent - * coroutines may already hold a reference to the store and next resolution - * will rebuild with fresh connections. + * Mutates the singleton's store cache; concurrent coroutines may already + * hold a reference to the store and next resolution will rebuild with + * fresh connections. */ + #[BootOrTestsOnly(whenShared: true)] public function purge(UnitEnum|string|null $name = null): void { if ($name instanceof UnitEnum) { @@ -561,11 +590,12 @@ public function purge(UnitEnum|string|null $name = null): void * Anonymous closures run in this manager's class scope; non-static closures * also receive the manager as $this. * - * Boot-only. The callback persists in the singleton's customCreators array - * for the worker lifetime and applies to every subsequent store resolution. + * The callback persists in the singleton's customCreators array for the + * worker lifetime and applies to every subsequent store resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback): static { try { @@ -583,12 +613,13 @@ public function extend(string $driver, Closure $callback): static /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's container reference; per-request use - * races across coroutines and breaks every concurrent request resolving - * stores through this manager. + * Swaps the singleton's container reference; per-request use races + * across coroutines and breaks every concurrent request resolving stores + * through this manager. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Container $app): static { $this->app = $app; @@ -599,10 +630,10 @@ public function setApplication(Container $app): static /** * Register classes that cache stores may unserialize. * - * Boot-only. The resolver contributes to the worker-lifetime cache policy - * and is evaluated when the application finishes booting in console processes, - * after configuration reload in Swoole workers, or before a custom server - * process starts handling work. An earlier cache read evaluates the current + * The resolver contributes to the worker-lifetime cache policy and is + * evaluated when the application finishes booting in console processes, after + * configuration reload in Swoole workers, or before a custom server process + * starts handling work. An earlier cache read evaluates the current * contributions without memoizing them. * * @param Closure(): array $resolver @@ -610,6 +641,7 @@ public function setApplication(Container $app): static * * @throws LogicException */ + #[BootOnly(whenShared: true)] public function allowSerializableClassesUsing(Closure $resolver): static { $this->serializableClassPolicy->allowUsing($resolver); @@ -620,11 +652,12 @@ public function allowSerializableClassesUsing(Closure $resolver): static /** * Finalize the worker-lifetime serializable class policy. * - * Boot-only. The policy is frozen for the worker lifetime; later - * contributions throw and every subsequent unserialize uses the frozen list. + * The policy is frozen for the worker lifetime; later contributions throw + * and every subsequent unserialize uses the frozen list. * * @internal */ + #[BootOnly(whenShared: true)] public function finalizeSerializableClasses(): void { $this->serializableClassPolicy->finalize(); @@ -633,9 +666,10 @@ public function finalizeSerializableClasses(): void /** * Register a callback to be invoked when an unserializable class is encountered. * - * Boot or tests only. The callback persists for the worker lifetime and - * affects every subsequent cache read. + * The callback persists for the worker lifetime and affects every + * subsequent cache read. */ + #[BootOrTestsOnly] public function handleUnserializableClassUsing(?callable $callback): void { Repository::handleUnserializableClassUsing($callback); diff --git a/src/cache/src/DatabaseStore.php b/src/cache/src/DatabaseStore.php index 154bf70114..afcdd67964 100644 --- a/src/cache/src/DatabaseStore.php +++ b/src/cache/src/DatabaseStore.php @@ -14,6 +14,7 @@ use Hypervel\Database\Query\Builder; use Hypervel\Database\SQLiteConnection; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Str; @@ -445,11 +446,12 @@ public function getConnection(): ConnectionInterface /** * Set the connection name for the cache store. * - * Boot-only. Persists on the cached store for the worker lifetime; + * Persists on the cached store for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setConnection(string $connectionName): static { $this->connectionName = $connectionName; @@ -468,11 +470,12 @@ public function getLockConnection(): ConnectionInterface /** * Specify the connection that should be used to manage locks. * - * Boot-only. Persists on the cached store for the worker lifetime; + * Persists on the cached store for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setLockConnection(string $connectionName): static { $this->lockConnectionName = $connectionName; @@ -491,9 +494,10 @@ public function getPrefix(): string /** * Set the cache key prefix. * - * Boot-only. Persists on the cached store for the worker lifetime; + * Persists on the cached store for the worker lifetime; * per-request use races across coroutines. */ + #[BootOnly(whenShared: true)] public function setPrefix(string $prefix): void { $this->prefix = $prefix; diff --git a/src/cache/src/FileStore.php b/src/cache/src/FileStore.php index a9d8a42d94..e241488cbe 100644 --- a/src/cache/src/FileStore.php +++ b/src/cache/src/FileStore.php @@ -12,6 +12,7 @@ use Hypervel\Coroutine\Coroutine; use Hypervel\Filesystem\Filesystem; use Hypervel\Filesystem\LockableFile; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\InteractsWithTime; use RuntimeException; @@ -425,11 +426,12 @@ public function getDirectory(): string /** * Set the working directory of the cache. * - * Boot-only. Persists on the cached store for the worker lifetime; + * Persists on the cached store for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setDirectory(string $directory): static { $this->directory = $directory; @@ -440,11 +442,12 @@ public function setDirectory(string $directory): static /** * Set the cache directory where locks should be stored. * - * Boot-only. Persists on the cached store for the worker lifetime; + * Persists on the cached store for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setLockDirectory(?string $lockDirectory): static { $this->lockDirectory = $lockDirectory; diff --git a/src/cache/src/MemoizedTaggedCache.php b/src/cache/src/MemoizedTaggedCache.php index 7404cc8923..882ca3da16 100644 --- a/src/cache/src/MemoizedTaggedCache.php +++ b/src/cache/src/MemoizedTaggedCache.php @@ -223,6 +223,10 @@ public function forget(array|UnitEnum|string $key): bool */ public function flush(): bool { + // A clone isn't registered with the memoized store, so the store-level + // flushes below don't reach its own memoized values. + $this->flushMemoized(); + if ($this->taggedCache instanceof AnyModeTaggedCache) { $this->memoizedStore->flushMemoized(); } else { @@ -289,6 +293,18 @@ protected function prefix(string $key): string return $this->store->getPrefix() . $key; } + /** + * Give the cloned wrapper its own copy of the wrapped tagged cache. + */ + public function __clone(): void + { + // The cloned delegate owns the cloned store and its tag set; the wrapper + // reuses them rather than cloning the store a second time. + $this->taggedCache = clone $this->taggedCache; + $this->store = $this->taggedCache->getStore(); + $this->tags = $this->taggedCache->getTags(); + } + /** * Handle dynamic calls into macros or pass missing methods to the tagged cache. */ diff --git a/src/cache/src/RedisStore.php b/src/cache/src/RedisStore.php index 297c0485e1..f1de46cae1 100644 --- a/src/cache/src/RedisStore.php +++ b/src/cache/src/RedisStore.php @@ -27,6 +27,7 @@ use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Redis\Factory as Redis; use Hypervel\Redis\RedisProxy; +use Hypervel\Support\Attributes\BootOnly; use RuntimeException; class RedisStore extends TaggableStore implements CanFlushLocks, LockProvider @@ -329,11 +330,12 @@ public function flushStaleTags(): ?array * PhpRedis 6.3.0 or later. These additional requirements do not apply * to all mode. * - * Boot-only. Mutates state on a per-worker singleton; runtime mutation - * races across coroutines. + * Mutates state on a per-worker singleton; runtime mutation races + * across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setTagMode(TagMode|string $mode): static { $this->tagMode = $mode instanceof TagMode @@ -391,10 +393,11 @@ public function hasSeparateLockStore(): bool /** * Specify the name of the connection that should be used to store data. * - * Boot-only. Persists on the cached store for the worker lifetime and - * triggers cache invalidation of resolved operations; per-request use - * races across coroutines. + * Persists on the cached store for the worker lifetime and triggers + * cache invalidation of resolved operations; per-request use races + * across coroutines. */ + #[BootOnly(whenShared: true)] public function setConnection(?string $connection): void { $this->connection = $connection ?? 'default'; @@ -404,11 +407,12 @@ public function setConnection(?string $connection): void /** * Specify the name of the connection that should be used to manage locks. * - * Boot-only. Persists on the cached store for the worker lifetime; + * Persists on the cached store for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setLockConnection(string $connection): static { $this->lockConnection = $connection; @@ -435,10 +439,11 @@ public function getRedis(): Redis /** * Set the cache key prefix. * - * Boot-only. Persists on the cached store for the worker lifetime and - * triggers cache invalidation of resolved operations; per-request use - * races across coroutines. + * Persists on the cached store for the worker lifetime and triggers + * cache invalidation of resolved operations; per-request use races + * across coroutines. */ + #[BootOnly(whenShared: true)] public function setPrefix(?string $prefix): void { $this->prefix = $prefix ?? ''; diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index e150eb2ac6..d283b0b580 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -40,6 +40,8 @@ use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\InteractsWithTime; @@ -1004,12 +1006,13 @@ public function getDefaultCacheTime(): ?int /** * Set the default cache time in seconds. * - * Boot-only. Persists on the cached repository for the worker lifetime - * and applies to every subsequent write that falls back to the default + * Persists on the cached repository for the worker lifetime and + * applies to every subsequent write that falls back to the default * TTL; per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setDefaultCacheTime(?int $seconds): static { $this->default = $seconds; @@ -1028,12 +1031,13 @@ public function getStore(): Store /** * Set the cache store implementation. * - * Boot-only. Replaces the backing store on the cached repository for the - * worker lifetime; per-request use races and every concurrent - * read/write through this repository would hit the wrong store. + * Replaces the backing store on the cached repository for the worker + * lifetime; per-request use races and every concurrent read/write + * through this repository would hit the wrong store. * * @return $this */ + #[BootOnly(whenShared: true)] public function setStore(Store $store): static { $this->store = $store; @@ -1052,10 +1056,11 @@ public function getEventDispatcher(): ?Dispatcher /** * Set the event dispatcher instance. * - * Boot or tests only. Persists on the cached repository for the worker - * lifetime; per-request use races across coroutines. Reached by + * Persists on the cached repository for the worker lifetime; per-request + * use races across coroutines. Reached by * CacheManager::refreshEventDispatcher() from Event::fake() / fakeFor(). */ + #[BootOrTestsOnly(whenShared: true)] public function setEventDispatcher(Dispatcher $events): void { $this->events = $events; @@ -1064,9 +1069,10 @@ public function setEventDispatcher(Dispatcher $events): void /** * Register a callback to be invoked when an unserializable class is encountered. * - * Boot or tests only. The callback persists for the worker lifetime and - * affects every subsequent cache read. + * The callback persists for the worker lifetime and affects every + * subsequent cache read. */ + #[BootOrTestsOnly] public static function handleUnserializableClassUsing(?callable $callback): void { static::$unserializableClassHandler = $callback === null diff --git a/src/cache/src/SerializableClassPolicy.php b/src/cache/src/SerializableClassPolicy.php index 00dc6ac90d..0aaa995570 100644 --- a/src/cache/src/SerializableClassPolicy.php +++ b/src/cache/src/SerializableClassPolicy.php @@ -5,6 +5,7 @@ namespace Hypervel\Cache; use Closure; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use LogicException; @@ -42,13 +43,14 @@ public function __construct( /** * Register classes that PHP may unserialize. * - * Boot-only. Contributions are held for the process-wide policy and cannot - * change after finalization. + * Contributions are held for the process-wide policy and cannot change + * after finalization. * * @param Closure(): array $resolver * * @throws LogicException */ + #[BootOnly(whenShared: true)] public function allowUsing(Closure $resolver): void { if ($this->finalized) { @@ -63,11 +65,11 @@ public function allowUsing(Closure $resolver): void /** * Finalize the serializable class policy. * - * Boot-only. The result is frozen for every subsequent cache read in the - * process. + * The result is frozen for every subsequent cache read in the process. * * @internal */ + #[BootOnly(whenShared: true)] public function finalize(): void { if ($this->finalized) { diff --git a/src/cache/src/StorageStore.php b/src/cache/src/StorageStore.php index a87efd780c..ee08acf40a 100644 --- a/src/cache/src/StorageStore.php +++ b/src/cache/src/StorageStore.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Filesystem\Filesystem; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\InteractsWithTime; class StorageStore implements Store @@ -308,9 +309,10 @@ public function getPrefix(): string /** * Set the cache key prefix. * - * Boot-only. Persists on the cached store for the worker lifetime; + * Persists on the cached store for the worker lifetime; * per-request use races across coroutines. */ + #[BootOnly(whenShared: true)] public function setPrefix(string $prefix): void { $this->prefix = $prefix; diff --git a/src/cache/src/SwooleTableManager.php b/src/cache/src/SwooleTableManager.php index 20be587919..25b8881ae5 100644 --- a/src/cache/src/SwooleTableManager.php +++ b/src/cache/src/SwooleTableManager.php @@ -5,6 +5,7 @@ namespace Hypervel\Cache; use Hypervel\Contracts\Container\Container; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use LogicException; use Swoole\Table; @@ -74,9 +75,10 @@ public function get(string $name): SwooleTableState /** * Prevent tables from being created after the server initialization phase. * - * Boot-only. Creating a table after the server forks would give each worker - * private state instead of one shared cache table. + * Creating a table after the server forks would give each worker private + * state instead of one shared cache table. */ + #[BootOnly(whenShared: true)] public function seal(): void { $this->sealed = true; diff --git a/src/cache/src/TagSet.php b/src/cache/src/TagSet.php index fb627f139a..9299dd5dfa 100644 --- a/src/cache/src/TagSet.php +++ b/src/cache/src/TagSet.php @@ -30,6 +30,17 @@ public function __construct(Store $store, array $names = []) $this->names = $names; } + /** + * Get a copy of the tag set that uses the given store. + */ + public function withStore(Store $store): static + { + $tagSet = clone $this; + $tagSet->store = $store; + + return $tagSet; + } + /** * Reset all tags in the set. */ diff --git a/src/cache/src/TaggedCache.php b/src/cache/src/TaggedCache.php index 26846277a4..be4ad62b24 100644 --- a/src/cache/src/TaggedCache.php +++ b/src/cache/src/TaggedCache.php @@ -130,4 +130,14 @@ protected function event(object $event): void parent::event($event); } + + /** + * Give the cloned tagged cache a tag set on its cloned store. + */ + public function __clone(): void + { + parent::__clone(); + + $this->tags = $this->tags->withStore($this->store); + } } diff --git a/src/collections/composer.json b/src/collections/composer.json index 0e04dfd53c..55493e983e 100644 --- a/src/collections/composer.json +++ b/src/collections/composer.json @@ -26,6 +26,7 @@ "require": { "php": "^8.4", "ext-filter": "*", + "hypervel/attributes": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/contracts": "^0.4", "hypervel/macroable": "^0.4", diff --git a/src/collections/src/Enumerable.php b/src/collections/src/Enumerable.php index a00d9563d0..94589f1fa3 100644 --- a/src/collections/src/Enumerable.php +++ b/src/collections/src/Enumerable.php @@ -10,6 +10,7 @@ use Exception; use Hypervel\Contracts\Support\Arrayable; use Hypervel\Contracts\Support\Jsonable; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use IteratorAggregate; use JsonException; @@ -1181,10 +1182,10 @@ public function escapeWhenCastingToString(bool $escape = true): static; /** * Add a method to the list of proxied methods. * - * Boot or tests only. The method is registered on a worker-wide static and - * applies to every subsequent collection access; per-request use races - * across coroutines. + * The method is registered on a worker-wide static and applies to every + * subsequent collection access; per-request use races across coroutines. */ + #[BootOrTestsOnly] public static function proxy(string $method): void; /** diff --git a/src/collections/src/Traits/EnumeratesValues.php b/src/collections/src/Traits/EnumeratesValues.php index 3ac00baed3..12e716b2f3 100644 --- a/src/collections/src/Traits/EnumeratesValues.php +++ b/src/collections/src/Traits/EnumeratesValues.php @@ -11,6 +11,7 @@ use Hypervel\Contracts\Support\Arrayable; use Hypervel\Contracts\Support\Jsonable; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Enumerable; use Hypervel\Support\HigherOrderCollectionProxy; @@ -1016,10 +1017,10 @@ public function escapeWhenCastingToString(bool $escape = true): static /** * Add a method to the list of proxied methods. * - * Boot or tests only. The method is registered on a worker-wide static and - * applies to every subsequent collection access; per-request use races - * across coroutines. + * The method is registered on a worker-wide static and applies to every + * subsequent collection access; per-request use races across coroutines. */ + #[BootOrTestsOnly] public static function proxy(string $method): void { static::$proxies[] = $method; @@ -1028,9 +1029,10 @@ public static function proxy(string $method): void /** * Flush the registered proxies, restoring the default list. * - * Boot or tests only. Resets the worker-wide proxy registry; concurrent - * coroutines may resolve different proxies depending on timing. + * Resets the worker-wide proxy registry; concurrent coroutines may + * resolve different proxies depending on timing. */ + #[BootOrTestsOnly] public static function flushProxies(): void { static::$proxies = self::DEFAULT_PROXIES; diff --git a/src/concurrency/composer.json b/src/concurrency/composer.json index 31294aee82..235f28bb8e 100644 --- a/src/concurrency/composer.json +++ b/src/concurrency/composer.json @@ -26,6 +26,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", "hypervel/container": "^0.4", diff --git a/src/concurrency/src/ConcurrencyManager.php b/src/concurrency/src/ConcurrencyManager.php index 77a952dc3f..640f57bf17 100644 --- a/src/concurrency/src/ConcurrencyManager.php +++ b/src/concurrency/src/ConcurrencyManager.php @@ -8,6 +8,7 @@ use Closure; use Hypervel\Contracts\Concurrency\Driver; use Hypervel\Process\Factory as ProcessFactory; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\MultipleInstanceManager; use UnitEnum; @@ -72,8 +73,9 @@ public function getDefaultInstance(): string /** * Set the default concurrency driver name. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultInstance(string $name): void { $this->config->set('concurrency.default', $name); diff --git a/src/config/composer.json b/src/config/composer.json index f1fe42d567..168e41887c 100644 --- a/src/config/composer.json +++ b/src/config/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/contracts": "^0.4", "hypervel/macroable": "^0.4", diff --git a/src/config/src/Repository.php b/src/config/src/Repository.php index 8a0cb0dd86..d21b104a2f 100644 --- a/src/config/src/Repository.php +++ b/src/config/src/Repository.php @@ -8,6 +8,7 @@ use Closure; use Hypervel\Contracts\Config\Repository as ConfigContract; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Traits\Macroable; use InvalidArgumentException; @@ -177,10 +178,10 @@ public function collection(string $key, mixed $default = null): Collection /** * Set a given configuration value. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. */ + #[BootOrTestsOnly(whenShared: true)] public function set(array|string $key, mixed $value = null): void { $keys = is_array($key) ? $key : [$key => $value]; @@ -197,10 +198,10 @@ public function set(array|string $key, mixed $value = null): void /** * Prepend a value onto an array configuration value. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. */ + #[BootOrTestsOnly(whenShared: true)] public function prepend(string $key, mixed $value): void { $array = $this->get($key, []); @@ -213,10 +214,10 @@ public function prepend(string $key, mixed $value): void /** * Push a value onto an array configuration value. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. */ + #[BootOrTestsOnly(whenShared: true)] public function push(string $key, mixed $value): void { $array = $this->get($key, []); @@ -279,13 +280,13 @@ public function offsetGet($offset): mixed /** * Set a configuration option. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. * * @param int|string $offset * @param mixed $value */ + #[BootOrTestsOnly(whenShared: true)] public function offsetSet($offset, $value): void { $this->set((string) $offset, $value); @@ -294,12 +295,12 @@ public function offsetSet($offset, $value): void /** * Unset a configuration option. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. * * @param int|string $offset */ + #[BootOrTestsOnly(whenShared: true)] public function offsetUnset($offset): void { $this->set((string) $offset, null); diff --git a/src/connection-pool/composer.json b/src/connection-pool/composer.json index fe43955829..e6d5b7e872 100644 --- a/src/connection-pool/composer.json +++ b/src/connection-pool/composer.json @@ -31,6 +31,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/contracts": "^0.4", "hypervel/coordinator": "^0.4", "hypervel/coroutine": "^0.4", diff --git a/src/connection-pool/src/KeepaliveConnection.php b/src/connection-pool/src/KeepaliveConnection.php index 67b490e9e4..1751826c3b 100644 --- a/src/connection-pool/src/KeepaliveConnection.php +++ b/src/connection-pool/src/KeepaliveConnection.php @@ -15,6 +15,7 @@ use Hypervel\Coordinator\Timer; use Hypervel\Coroutine\Coroutine; use Hypervel\Engine\Channel; +use Hypervel\Support\Attributes\Unsupported; use Psr\Log\LoggerInterface; use Swoole\Coroutine\CanceledException; use Throwable; @@ -67,8 +68,11 @@ public function discard(): void } /** + * Get the underlying connection. + * * @throws InvalidArgumentException */ + #[Unsupported('Use call() instead.')] public function getConnection(): mixed { throw new InvalidArgumentException('Please use call instead of getConnection.'); diff --git a/src/console/composer.json b/src/console/composer.json index ccbbc91add..35e72422e8 100644 --- a/src/console/composer.json +++ b/src/console/composer.json @@ -31,6 +31,7 @@ "ext-posix": "*", "ext-swoole": "^6.3.0RC1", "dragonmantank/cron-expression": "^3.4", + "hypervel/attributes": "^0.4", "hypervel/bus": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", diff --git a/src/console/src/Application.php b/src/console/src/Application.php index d93ff3ef50..ecde585bd3 100644 --- a/src/console/src/Application.php +++ b/src/console/src/Application.php @@ -14,6 +14,8 @@ use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Foundation\Application as ApplicationContract; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Defer\DeferredCallback; use Hypervel\Support\Defer\DeferredCallbackCollection; use Hypervel\Support\ProcessUtils; @@ -190,11 +192,12 @@ public static function formatCommandString(string $string): string /** * Register a console "starting" bootstrapper. * - * Boot-only. The bootstrapper persists in a static property for the worker - * lifetime and runs for every subsequent console application instance. + * The bootstrapper persists in a static property for the worker lifetime + * and runs for every subsequent console application instance. * * @param Closure(static): mixed $callback */ + #[BootOnly] public static function starting(Closure $callback): void { static::$bootstrappers[] = $callback; @@ -213,9 +216,10 @@ protected function bootstrap(): void /** * Clear the console application bootstrappers. * - * Boot or tests only. Clears worker-wide console bootstrappers; concurrent - * console application creation may observe different startup hooks. + * Clears worker-wide console bootstrappers; concurrent console application + * creation may observe different startup hooks. */ + #[BootOrTestsOnly] public static function forgetBootstrappers(): void { static::$bootstrappers = []; diff --git a/src/console/src/CacheCommandMutex.php b/src/console/src/CacheCommandMutex.php index 796a6d9954..ab26e224f2 100644 --- a/src/console/src/CacheCommandMutex.php +++ b/src/console/src/CacheCommandMutex.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Cache\Factory as Cache; use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Cache\Store; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\InteractsWithTime; class CacheCommandMutex implements CommandMutex @@ -105,11 +106,12 @@ protected function shouldUseLocks(Store $store): bool /** * Specify the cache store that should be used. * - * Boot-only. Mutates the shared command mutex instance for the worker - * lifetime; per-request use races across coroutines. + * Mutates the shared command mutex instance for the worker lifetime; + * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function useStore(?string $store): static { $this->store = $store; diff --git a/src/console/src/Commands/ScheduleListCommand.php b/src/console/src/Commands/ScheduleListCommand.php index 7e48234acc..3db15cc529 100644 --- a/src/console/src/Commands/ScheduleListCommand.php +++ b/src/console/src/Commands/ScheduleListCommand.php @@ -13,6 +13,7 @@ use Hypervel\Console\Scheduling\Event; use Hypervel\Console\Scheduling\Schedule; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Str; @@ -365,9 +366,10 @@ public static function getTerminalWidth(): int /** * Set a callback that should be used when resolving the terminal width. * - * Tests only. The resolver persists in a static property for the worker - * lifetime and affects every subsequent schedule:list render. + * The resolver persists in a static property for the worker lifetime + * and affects every subsequent schedule:list render. */ + #[TestsOnly] public static function resolveTerminalWidthUsing(?Closure $resolver): void { static::$terminalWidthResolver = $resolver; diff --git a/src/console/src/Prohibitable.php b/src/console/src/Prohibitable.php index 8b276b012d..e801e90466 100644 --- a/src/console/src/Prohibitable.php +++ b/src/console/src/Prohibitable.php @@ -4,6 +4,8 @@ namespace Hypervel\Console; +use Hypervel\Support\Attributes\BootOnly; + trait Prohibitable { /** @@ -14,9 +16,10 @@ trait Prohibitable /** * Indicate whether the command should be prohibited from running. * - * Boot-only. The prohibition flag persists in a static property for the - * worker lifetime and affects every subsequent execution of this command. + * The prohibition flag persists in a static property for the worker + * lifetime and affects every subsequent execution of this command. */ + #[BootOnly] public static function prohibit(bool $prohibit = true): void { static::$prohibitedFromRunning = $prohibit; diff --git a/src/console/src/Scheduling/CacheAware.php b/src/console/src/Scheduling/CacheAware.php index 986c21f81b..c52be7409d 100644 --- a/src/console/src/Scheduling/CacheAware.php +++ b/src/console/src/Scheduling/CacheAware.php @@ -4,15 +4,18 @@ namespace Hypervel\Console\Scheduling; +use Hypervel\Support\Attributes\BootOnly; + interface CacheAware { /** * Specify the cache store that should be used. * - * Boot-only. Mutates the shared mutex instance for the worker lifetime; + * Mutates the shared mutex instance for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function useStore(string $store): static; } diff --git a/src/console/src/Scheduling/CacheEventMutex.php b/src/console/src/Scheduling/CacheEventMutex.php index a1f9a4e1a2..9b9c91df18 100644 --- a/src/console/src/Scheduling/CacheEventMutex.php +++ b/src/console/src/Scheduling/CacheEventMutex.php @@ -7,6 +7,7 @@ use Hypervel\Contracts\Cache\Factory as CacheFactory; use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Cache\Store; +use Hypervel\Support\Attributes\BootOnly; class CacheEventMutex implements EventMutex, CacheAware { @@ -95,11 +96,12 @@ protected function shouldUseLocks(Store $store): bool /** * Specify the cache store that should be used. * - * Boot-only. Mutates the shared mutex instance for the worker lifetime; + * Mutates the shared mutex instance for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function useStore(?string $store): static { $this->store = $store; diff --git a/src/console/src/Scheduling/CacheSchedulingMutex.php b/src/console/src/Scheduling/CacheSchedulingMutex.php index 68e9abf5cf..287e415009 100644 --- a/src/console/src/Scheduling/CacheSchedulingMutex.php +++ b/src/console/src/Scheduling/CacheSchedulingMutex.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Cache\Factory as CacheFactory; use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Cache\Store; +use Hypervel\Support\Attributes\BootOnly; class CacheSchedulingMutex implements SchedulingMutex, CacheAware { @@ -81,11 +82,12 @@ protected function shouldUseLocks(Store $store): bool /** * Specify the cache store that should be used. * - * Boot-only. Mutates the shared mutex instance for the worker lifetime; + * Mutates the shared mutex instance for the worker lifetime; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function useStore(?string $store): static { $this->store = $store; diff --git a/src/console/src/Scheduling/CallbackEvent.php b/src/console/src/Scheduling/CallbackEvent.php index 2ed6a5e13b..a8b26983eb 100644 --- a/src/console/src/Scheduling/CallbackEvent.php +++ b/src/console/src/Scheduling/CallbackEvent.php @@ -8,6 +8,7 @@ use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Container\Container; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Reflector; use InvalidArgumentException; use LogicException; @@ -110,6 +111,7 @@ public function shouldSkipDueToOverlapping(): bool * * @throws RuntimeException */ + #[Unsupported('Only command and exec tasks can run in the background.')] public function runInBackground(): static { throw new RuntimeException('Scheduled closures can not be run in the background.'); diff --git a/src/console/src/Scheduling/Schedule.php b/src/console/src/Scheduling/Schedule.php index ab3985307c..c66bc4284e 100644 --- a/src/console/src/Scheduling/Schedule.php +++ b/src/console/src/Scheduling/Schedule.php @@ -23,6 +23,7 @@ use Hypervel\Contracts\Queue\ShouldBeUnique; use Hypervel\Contracts\Queue\ShouldQueue; use Hypervel\Queue\CallQueuedClosure; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Date; use Hypervel\Support\InteractsWithTime; @@ -453,11 +454,12 @@ public function eventsForEnvironments(array $environments): array /** * Specify the cache store that should be used to store mutexes. * - * Boot-only. Mutates the shared mutex instances used by this worker's - * schedule; per-request use races across coroutines. + * Mutates the shared mutex instances used by this worker's schedule; + * per-request use races across coroutines. * * @return $this */ + #[BootOnly] public function useCache(UnitEnum|string|null $store): static { if (is_null($store)) { @@ -504,9 +506,10 @@ protected function getDispatcher(): Dispatcher * * This prevents the scheduler from hitting the application cache to determine if it needs to pause or interrupt. * - * Boot-only. Mutates process-global scheduler flags; runtime use changes - * pause and interrupt behavior for every concurrent scheduler run. + * Mutates process-global scheduler flags; runtime use changes pause and + * interrupt behavior for every concurrent scheduler run. */ + #[BootOnly] public static function withoutInterruptionPolling(): void { static::$pausable = false; @@ -530,9 +533,10 @@ public function hasBeenInterruptedSince(DateTimeInterface $time): bool /** * Indicate that every event on the schedule should only run on one server for each cron expression. * - * Boot-only. The flag persists for the worker lifetime and affects every - * schedule evaluated in the process. + * The flag persists for the worker lifetime and affects every schedule + * evaluated in the process. */ + #[BootOnly] public static function alwaysOnOneServer(bool $value = true): void { static::$alwaysOnOneServer = $value; @@ -541,11 +545,12 @@ public static function alwaysOnOneServer(bool $value = true): void /** * Register a callback to be invoked when a scheduled task runs for longer than a given amount of time. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching scheduler event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching scheduler event. * * @param callable(Event, float): mixed $handler */ + #[BootOnly] public function whenTaskRunsLongerThan(DateTimeInterface|CarbonInterval|float|int $threshold, callable $handler): void { $threshold = $threshold instanceof DateTimeInterface diff --git a/src/container/composer.json b/src/container/composer.json index dad1dc6339..3979e60730 100644 --- a/src/container/composer.json +++ b/src/container/composer.json @@ -25,6 +25,7 @@ "require": { "php": "^8.4", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", diff --git a/src/container/src/Container.php b/src/container/src/Container.php index ac9f4955dc..c53cea13a0 100755 --- a/src/container/src/Container.php +++ b/src/container/src/Container.php @@ -20,6 +20,9 @@ use Hypervel\Contracts\Container\SelfBuilding; use Hypervel\Contracts\Container\Transient; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\ClassMetadataCache; use Hypervel\Support\Traits\ReflectsClosures; use InvalidArgumentException; @@ -37,11 +40,6 @@ class Container implements ContainerContract { use ReflectsClosures; - /** - * Context key for coroutine-local resolution state. - */ - protected const string RESOLUTION_STATE_CONTEXT_KEY = '__container.resolution_state'; - /** * Maximum resolution depth before assuming a circular dependency. * @@ -52,14 +50,16 @@ class Container implements ContainerContract protected const int MAX_RESOLUTION_DEPTH = 500; /** - * Context key prefix for coroutine-local scoped instances. + * The current globally available container (if any). */ - protected const string SCOPED_CONTEXT_PREFIX = '__container.scoped.'; + protected static ?self $instance = null; /** - * The current globally available container (if any). + * The number of context key prefixes given to containers in this worker. + * + * It is never reset, so no container reuses another container's prefix. */ - protected static ?self $instance = null; + protected static int $contextKeyPrefixCount = 0; /** * An array of the types that have been resolved. @@ -128,6 +128,14 @@ class Container implements ContainerContract */ protected array $scopedInstances = []; + /** + * The prefix of this container's coroutine context keys. + * + * Scoped instances and resolution state live in coroutine context, which + * every container shares, so each container keys them with its own prefix. + */ + protected ?string $contextKeyPrefix = null; + /** * The registered type aliases. * @@ -399,7 +407,7 @@ public function isScopedInstance(mixed $instance): bool } foreach ($this->scopedInstances as $abstract => $_) { - if (CoroutineContext::get(self::SCOPED_CONTEXT_PREFIX . $abstract) === $instance) { + if (CoroutineContext::get($this->contextKey('scoped.' . $abstract)) === $instance) { return true; } } @@ -427,7 +435,7 @@ public function resolvedScoped(string $abstract): bool { $abstract = $this->getAlias($abstract); - return CoroutineContext::has(self::SCOPED_CONTEXT_PREFIX . $abstract) + return CoroutineContext::has($this->contextKey('scoped.' . $abstract)) || ((isset($this->instances[$abstract]) || array_key_exists($abstract, $this->instances)) && $this->isScoped($abstract)); } @@ -492,13 +500,13 @@ public function isAlias(string $name): bool /** * Register a binding with the container. * - * Boot-only. Bindings persist on the worker-lifetime container; runtime - * use races across coroutines and changes resolution for every subsequent - * request. + * Bindings persist on the worker-lifetime container; runtime use races + * across coroutines and changes resolution for every subsequent request. * * @throws TypeError * @throws ReflectionException */ + #[BootOnly(whenShared: true)] public function bind(Closure|string $abstract, Closure|string|null $concrete = null, bool $shared = false): void { if ($abstract instanceof Closure) { @@ -583,9 +591,10 @@ public function hasMethodBinding(string $method): bool /** * Bind a callback to resolve with Container::call. * - * Boot-only. Method bindings persist on the worker-lifetime container and - * affect every subsequent Container::call() for the bound method. + * Method bindings persist on the worker-lifetime container and affect + * every subsequent Container::call() for the bound method. */ + #[BootOnly(whenShared: true)] public function bindMethod(array|string $method, Closure $callback): void { $this->methodBindings[$this->parseBindMethod($method)] = $callback; @@ -614,10 +623,11 @@ public function callMethodBinding(string $method, mixed $instance): mixed /** * Add a contextual binding to the container. * - * Boot-only. Contextual bindings persist on the worker-lifetime container; - * runtime use races across coroutines and changes dependency resolution for - * every subsequent build of the concrete. + * Contextual bindings persist on the worker-lifetime container; runtime use + * races across coroutines and changes dependency resolution for every + * subsequent build of the concrete. */ + #[BootOnly(whenShared: true)] public function addContextualBinding(string $concrete, string $abstract, mixed $implementation): void { $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation; @@ -626,9 +636,10 @@ public function addContextualBinding(string $concrete, string $abstract, mixed $ /** * Register a binding if it hasn't already been registered. * - * Boot-only. Conditional bindings mutate the worker-lifetime container for - * every subsequent request. + * Conditional bindings mutate the worker-lifetime container for every + * subsequent request. */ + #[BootOnly(whenShared: true)] public function bindIf(Closure|string $abstract, Closure|string|null $concrete = null, bool $shared = false): void { if ($abstract instanceof Closure) { @@ -650,10 +661,11 @@ public function bindIf(Closure|string $abstract, Closure|string|null $concrete = /** * Register a shared binding in the container. * - * Boot-only. Singleton bindings persist on the worker-lifetime container; - * runtime use races across coroutines and changes shared resolution for - * every subsequent request. + * Singleton bindings persist on the worker-lifetime container; runtime + * use races across coroutines and changes shared resolution for every + * subsequent request. */ + #[BootOnly(whenShared: true)] public function singleton(Closure|string $abstract, Closure|string|null $concrete = null): void { $this->bind($abstract, $concrete, true); @@ -662,9 +674,10 @@ public function singleton(Closure|string $abstract, Closure|string|null $concret /** * Register a shared binding if it hasn't already been registered. * - * Boot-only. Conditional singleton bindings mutate the worker-lifetime - * container for every subsequent request. + * Conditional singleton bindings mutate the worker-lifetime container + * for every subsequent request. */ + #[BootOnly(whenShared: true)] public function singletonIf(Closure|string $abstract, Closure|string|null $concrete = null): void { $this->bindIf($abstract, $concrete, true); @@ -673,9 +686,10 @@ public function singletonIf(Closure|string $abstract, Closure|string|null $concr /** * Register a scoped binding in the container. * - * Boot-only. Scoped binding definitions persist on the worker-lifetime - * container; only their resolved instances are coroutine-scoped. + * Scoped binding definitions persist on the worker-lifetime container; + * only their resolved instances are coroutine-scoped. */ + #[BootOnly(whenShared: true)] public function scoped(Closure|string $abstract, Closure|string|null $concrete = null): void { if ($abstract instanceof Closure) { @@ -690,9 +704,10 @@ public function scoped(Closure|string $abstract, Closure|string|null $concrete = /** * Register a scoped binding if it hasn't already been registered. * - * Boot-only. Conditional scoped bindings mutate the worker-lifetime - * container for every subsequent request. + * Conditional scoped bindings mutate the worker-lifetime container + * for every subsequent request. */ + #[BootOnly(whenShared: true)] public function scopedIf(Closure|string $abstract, Closure|string|null $concrete = null): void { if ($abstract instanceof Closure) { @@ -738,11 +753,12 @@ protected function bindBasedOnClosureReturnTypes( /** * "Extend" an abstract type in the container. * - * Boot-only. The extender persists in the container's $extenders array for - * the worker lifetime and runs on every subsequent resolution of $abstract. + * The extender persists in the container's $extenders array for the worker + * lifetime and runs on every subsequent resolution of $abstract. * * @throws InvalidArgumentException */ + #[BootOnly(whenShared: true)] public function extend(string $abstract, Closure $closure): void { $abstract = $this->getAlias($abstract); @@ -756,14 +772,13 @@ public function extend(string $abstract, Closure $closure): void $this->rebound($abstract); } elseif ($this->isScoped($abstract) - && ($instance = CoroutineContext::get(self::SCOPED_CONTEXT_PREFIX . $abstract)) !== null + && ($instance = CoroutineContext::get($this->contextKey('scoped.' . $abstract))) !== null ) { // Apply the extender to the current cached scoped instance immediately. // Also queue it in $this->extenders for future resolutions — unlike singletons // (which persist for the worker lifetime), scoped instances are destroyed each // request by forgetScopedInstances() and must re-apply extenders on rebuild. - $contextKey = self::SCOPED_CONTEXT_PREFIX . $abstract; - CoroutineContext::set($contextKey, $closure($instance, $this)); + CoroutineContext::set($this->contextKey('scoped.' . $abstract), $closure($instance, $this)); $this->extenders[$abstract][] = $closure; @@ -780,15 +795,16 @@ public function extend(string $abstract, Closure $closure): void /** * Register an existing instance as shared in the container. * - * Boot or tests only. Replaces a worker-lifetime shared instance; runtime - * use races across coroutines and changes the object returned to every - * subsequent resolver. + * Replaces a worker-lifetime shared instance; runtime use races across + * coroutines and changes the object returned to every subsequent + * resolver. * * @template TInstance of mixed * * @param TInstance $instance * @return TInstance */ + #[BootOrTestsOnly(whenShared: true)] public function instance(string $abstract, mixed $instance): mixed { $this->removeAbstractAlias($abstract); @@ -832,9 +848,10 @@ protected function removeAbstractAlias(string $searched): void /** * Assign a set of tags to a given binding. * - * Boot-only. Tags persist on the worker-lifetime container and affect every - * subsequent tagged() resolution. + * Tags persist on the worker-lifetime container and affect every subsequent + * tagged() resolution. */ + #[BootOnly(whenShared: true)] public function tag(array|string $abstracts, array|string $tags): void { $tags = is_array($tags) ? $tags : array_slice(func_get_args(), 1); @@ -869,12 +886,12 @@ public function tagged(string $tag): iterable /** * Alias a type to a different name. * - * Boot-only. Aliases persist on the worker-lifetime container; runtime use - * races across coroutines and changes resolution for every subsequent - * request. + * Aliases persist on the worker-lifetime container; runtime use races + * across coroutines and changes resolution for every subsequent request. * * @throws LogicException */ + #[BootOnly(whenShared: true)] public function alias(string $abstract, string $alias): void { if ($alias === $abstract) { @@ -901,9 +918,10 @@ public function alias(string $abstract, string $alias): void /** * Bind a new callback to an abstract's rebind event. * - * Boot-only. Rebinding callbacks persist on the worker-lifetime container - * and run whenever the abstract is rebound later in the worker. + * Rebinding callbacks persist on the worker-lifetime container and run + * whenever the abstract is rebound later in the worker. */ + #[BootOnly(whenShared: true)] public function rebinding(string $abstract, Closure $callback): mixed { $this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback; @@ -918,9 +936,10 @@ public function rebinding(string $abstract, Closure $callback): mixed /** * Refresh an instance on the given target and method. * - * Boot-only. Registers a rebinding callback on the worker-lifetime - * container; runtime use leaks the callback across later rebindings. + * Registers a rebinding callback on the worker-lifetime container; + * runtime use leaks the callback across later rebindings. */ + #[BootOnly(whenShared: true)] public function refresh(string $abstract, mixed $target, string $method): mixed { return $this->rebinding($abstract, function ($app, $instance) use ($target, $method) { @@ -1214,8 +1233,7 @@ protected function resolve( && $this->isScoped($abstract) && ! $needsContextualBuild ) { - $contextKey = self::SCOPED_CONTEXT_PREFIX . $abstract; - $instance = CoroutineContext::get($contextKey); + $instance = CoroutineContext::get($this->contextKey('scoped.' . $abstract)); if ($instance !== null) { return $instance; @@ -1309,7 +1327,7 @@ protected function resolve( if (! $needsContextualBuild) { if ($applyImplicitLifetime && ! $transientResolution && $this->isShared($abstract)) { if ($this->isScoped($abstract)) { - CoroutineContext::set(self::SCOPED_CONTEXT_PREFIX . $abstract, $object); + CoroutineContext::set($this->contextKey('scoped.' . $abstract), $object); $publishedCache = 'scoped'; $publishedValue = $object; } else { @@ -1362,7 +1380,7 @@ protected function resolve( return $object; } catch (Throwable $e) { if ($publishedCache === 'scoped') { - $contextKey = self::SCOPED_CONTEXT_PREFIX . $abstract; + $contextKey = $this->contextKey('scoped.' . $abstract); if ($publishedValue !== null && CoroutineContext::get($contextKey) === $publishedValue @@ -1625,7 +1643,7 @@ protected function getContextualConcrete(string $abstract): mixed */ protected function getResolutionState(): ?ContainerResolutionState { - $resolutionState = CoroutineContext::get(self::RESOLUTION_STATE_CONTEXT_KEY); + $resolutionState = CoroutineContext::get($this->contextKey('resolution_state')); return $resolutionState instanceof ContainerResolutionState ? $resolutionState : null; } @@ -1640,11 +1658,19 @@ protected function getOrCreateResolutionState(): ContainerResolutionState } $resolutionState = new ContainerResolutionState; - CoroutineContext::set(self::RESOLUTION_STATE_CONTEXT_KEY, $resolutionState); + CoroutineContext::set($this->contextKey('resolution_state'), $resolutionState); return $resolutionState; } + /** + * Get this container's coroutine context key for the given entry. + */ + protected function contextKey(string $name): string + { + return ($this->contextKeyPrefix ??= '__container.' . ++self::$contextKeyPrefixCount . '.') . $name; + } + /** * Get the current build stack without creating resolution state. * @@ -2161,9 +2187,10 @@ protected function unresolvablePrimitive(ParameterRecipe $param): never /** * Register a new before resolving callback for all types. * - * Boot-only. The callback persists on the worker-lifetime container and - * runs before every matching future resolution. + * The callback persists on the worker-lifetime container and runs + * before every matching future resolution. */ + #[BootOnly(whenShared: true)] public function beforeResolving(Closure|string $abstract, ?Closure $callback = null): void { if (is_string($abstract)) { @@ -2180,9 +2207,10 @@ public function beforeResolving(Closure|string $abstract, ?Closure $callback = n /** * Register a new resolving callback. * - * Boot-only. The callback persists on the worker-lifetime container and - * runs during every matching future resolution. + * The callback persists on the worker-lifetime container and runs + * during every matching future resolution. */ + #[BootOnly(whenShared: true)] public function resolving(Closure|string $abstract, ?Closure $callback = null): void { if (is_string($abstract)) { @@ -2199,9 +2227,10 @@ public function resolving(Closure|string $abstract, ?Closure $callback = null): /** * Register a new after resolving callback for all types. * - * Boot-only. The callback persists on the worker-lifetime container and - * runs after every matching future resolution. + * The callback persists on the worker-lifetime container and runs after + * every matching future resolution. */ + #[BootOnly(whenShared: true)] public function afterResolving(Closure|string $abstract, ?Closure $callback = null): void { if (is_string($abstract)) { @@ -2218,9 +2247,10 @@ public function afterResolving(Closure|string $abstract, ?Closure $callback = nu /** * Register a new after resolving attribute callback for all types. * - * Boot-only. The callback persists on the worker-lifetime container and - * runs after every future resolution with the matching attribute. + * The callback persists on the worker-lifetime container and runs after + * every future resolution with the matching attribute. */ + #[BootOnly(whenShared: true)] public function afterResolvingAttribute(string $attribute, Closure $callback): void { $this->afterResolvingAttributeCallbacks[$attribute][] = $callback; @@ -2379,10 +2409,10 @@ protected function getExtenders(string $abstract): array /** * Remove all of the extender callbacks for a given type. * - * Boot or tests only. Clears worker-lifetime extender callbacks; concurrent - * resolutions may diverge depending on whether they resolved before or - * after the removal. + * Clears worker-lifetime extender callbacks; concurrent resolutions may + * diverge depending on whether they resolved before or after the removal. */ + #[BootOrTestsOnly(whenShared: true)] public function forgetExtenders(string $abstract): void { unset($this->extenders[$this->getAlias($abstract)]); @@ -2406,16 +2436,17 @@ protected function forgetCachedInstances(string $abstract): void { unset($this->instances[$abstract], $this->autoSingletons[$abstract]); - CoroutineContext::forget(self::SCOPED_CONTEXT_PREFIX . $abstract); + CoroutineContext::forget($this->contextKey('scoped.' . $abstract)); } /** * Remove a resolved instance from the instance cache. * - * Boot or tests only. Clears a worker-lifetime shared instance and current - * coroutine scoped instance; concurrent coroutines may still hold references - * that next resolution will not share. + * Clears a worker-lifetime shared instance and current coroutine scoped + * instance; concurrent coroutines may still hold references that next + * resolution will not share. */ + #[BootOrTestsOnly(whenShared: true)] public function forgetInstance(string $abstract): void { $this->forgetCachedInstances($this->getAlias($abstract)); @@ -2424,10 +2455,11 @@ public function forgetInstance(string $abstract): void /** * Clear all of the instances from the container. * - * Boot or tests only. Clears worker-lifetime shared instances and all scoped - * context entries known to this container; concurrent coroutines may still - * hold references that next resolution will not share. + * Clears worker-lifetime shared instances and all scoped context entries + * known to this container; concurrent coroutines may still hold references + * that next resolution will not share. */ + #[BootOrTestsOnly(whenShared: true)] public function forgetInstances(): void { $this->instances = []; @@ -2438,25 +2470,26 @@ public function forgetInstances(): void /** * Clear all of the scoped instances from the container. * - * Boot or tests only. Clears scoped context entries known to this container; - * runtime use can invalidate scoped services while a coroutine is still - * handling a request. + * Clears this container's scoped instances in the current coroutine; runtime + * use can invalidate scoped services while the request is still using them. */ + #[BootOrTestsOnly(whenShared: true)] public function forgetScopedInstances(): void { foreach ($this->scopedInstances as $scoped => $_) { - CoroutineContext::forget(self::SCOPED_CONTEXT_PREFIX . $scoped); + CoroutineContext::forget($this->contextKey('scoped.' . $scoped)); } } /** * Set the callback which determines the current container environment. * - * Boot-only. The resolver persists on the worker-lifetime container and is - * used by every currentEnvironmentIs() call. + * The resolver persists on the worker-lifetime container and is used by + * every currentEnvironmentIs() call. * * @param null|(callable(array|string): (bool|string)) $callback */ + #[BootOnly(whenShared: true)] public function resolveEnvironmentUsing(?callable $callback): void { $this->environmentResolver = $callback; @@ -2477,9 +2510,10 @@ public function currentEnvironmentIs(array|string $environments): bool /** * Flush the container of all bindings and resolved instances. * - * Tests only. Clears the worker-lifetime container registry; runtime use - * breaks every concurrent resolution in the worker. + * Clears the worker-lifetime container registry; runtime use breaks + * every concurrent resolution in the worker. */ + #[TestsOnly] public function flush(): void { // Must clear scoped Context entries before clearing $scopedInstances, @@ -2502,6 +2536,29 @@ public function flush(): void BoundMethod::flushState(); } + /** + * Give the cloned container its own coroutine context entries. + * + * Like its shared instances, the clone starts with the scoped instances + * resolved in the current coroutine, but forgetting or replacing them on + * one container leaves the other's untouched. + */ + public function __clone(): void + { + $sourcePrefix = $this->contextKeyPrefix; + $this->contextKeyPrefix = null; + + if ($sourcePrefix === null) { + return; + } + + foreach ($this->scopedInstances as $abstract => $_) { + if (($instance = CoroutineContext::get($sourcePrefix . 'scoped.' . $abstract)) !== null) { + CoroutineContext::set($this->contextKey('scoped.' . $abstract), $instance); + } + } + } + /** * Get the globally available instance of the container. */ @@ -2513,9 +2570,10 @@ public static function getInstance(): self /** * Set the shared instance of the container. * - * Tests only. Replaces the process-wide container singleton; runtime use - * races across coroutines and breaks every facade/global container lookup. + * Replaces the process-wide container singleton; runtime use races across + * coroutines and breaks every facade/global container lookup. */ + #[TestsOnly] public static function setInstance(?self $container = null): ?self { return static::$instance = $container; diff --git a/src/context/composer.json b/src/context/composer.json index 95ad08ce12..a3accea266 100644 --- a/src/context/composer.json +++ b/src/context/composer.json @@ -25,6 +25,7 @@ ], "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/coroutine": "^0.4", "hypervel/engine": "^0.4", "hypervel/http": "^0.4", diff --git a/src/context/src/CoroutineContext.php b/src/context/src/CoroutineContext.php index 6fcdf339b1..ef11e5bde9 100644 --- a/src/context/src/CoroutineContext.php +++ b/src/context/src/CoroutineContext.php @@ -8,6 +8,7 @@ use Closure; use Hypervel\Engine\Coroutine; use Hypervel\Engine\Exceptions\CoroutineDestroyedException; +use Hypervel\Support\Attributes\TestsOnly; use UnitEnum; use function Hypervel\Support\enum_value; @@ -230,9 +231,10 @@ public static function copyFromNonCoroutine(array $keys = [], ?int $coroutineId /** * Copy context data from the specified coroutine context to non-coroutine context. * - * Tests only. This copies coroutine-local values into worker-global storage, - * so request-time use can leak or race across concurrent requests. + * This copies coroutine-local values into worker-global storage, so + * request-time use can leak or race across concurrent requests. */ + #[TestsOnly] public static function copyToNonCoroutine(array $keys = [], ?int $coroutineId = null): void { if (is_null($context = Coroutine::getContextFor($coroutineId))) { @@ -273,13 +275,14 @@ private static function prepareForCopy(array $values): array /** * Store a value in non-coroutine context only. * - * Tests only. This writes worker-global storage even from a coroutine, - * so request-time use can leak state between concurrent requests. + * This writes worker-global storage even from a coroutine, so + * request-time use can leak state between concurrent requests. * * @param TKey $id * @param TValue $value * @return TValue */ + #[TestsOnly] public static function setNonCoroutine(UnitEnum|string $id, mixed $value): mixed { return static::$nonCoroutineContext[is_string($id) ? $id : enum_value($id)] = $value; @@ -305,9 +308,10 @@ public static function getFromNonCoroutine(UnitEnum|string $id, mixed $default = /** * Clear specific keys from non-coroutine context only. * - * Tests only. This mutates worker-global storage, so request-time use can - * race with concurrent requests using the same keys. + * This mutates worker-global storage, so request-time use can race with + * concurrent requests using the same keys. */ + #[TestsOnly] public static function clearFromNonCoroutine(array $keys): void { foreach ($keys as $key) { diff --git a/src/contracts/composer.json b/src/contracts/composer.json index 626c0b5230..82a132fc4a 100644 --- a/src/contracts/composer.json +++ b/src/contracts/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "monolog/monolog": "^3.10", "nesbot/carbon": "^3.13.1", "psr/container": "^2.0.1", diff --git a/src/contracts/src/Auth/Access/Gate.php b/src/contracts/src/Auth/Access/Gate.php index 10e3261eba..151eaa5b12 100644 --- a/src/contracts/src/Auth/Access/Gate.php +++ b/src/contracts/src/Auth/Access/Gate.php @@ -10,6 +10,7 @@ use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Query\Builder as QueryBuilder; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use LogicException; use RuntimeException; @@ -29,6 +30,7 @@ public function has(array|UnitEnum|string $ability): bool; * * @throws InvalidArgumentException */ + #[BootOnly(whenShared: true)] public function define(UnitEnum|string $ability, array|callable|string $callback): static; /** @@ -36,6 +38,7 @@ public function define(UnitEnum|string $ability, array|callable|string $callback * * @return $this */ + #[BootOnly(whenShared: true)] public function resource(string $name, string $class, ?array $abilities = null): static; /** @@ -43,6 +46,7 @@ public function resource(string $name, string $class, ?array $abilities = null): * * @return $this */ + #[BootOnly(whenShared: true)] public function policy(string $class, string $policy): static; /** @@ -50,6 +54,7 @@ public function policy(string $class, string $policy): static; * * @return $this */ + #[BootOnly(whenShared: true)] public function before(callable $callback): static; /** @@ -57,6 +62,7 @@ public function before(callable $callback): static; * * @return $this */ + #[BootOnly(whenShared: true)] public function after(callable $callback): static; /** diff --git a/src/contracts/src/Bus/Dispatcher.php b/src/contracts/src/Bus/Dispatcher.php index ddbc9769fd..959d4a8856 100644 --- a/src/contracts/src/Bus/Dispatcher.php +++ b/src/contracts/src/Bus/Dispatcher.php @@ -4,6 +4,8 @@ namespace Hypervel\Contracts\Bus; +use Hypervel\Support\Attributes\BootOnly; + interface Dispatcher { /** @@ -48,6 +50,7 @@ public function getCommandHandler(mixed $command): mixed; * * @return $this */ + #[BootOnly(whenShared: true)] public function pipeThrough(array $pipes): static; /** @@ -55,5 +58,6 @@ public function pipeThrough(array $pipes): static; * * @return $this */ + #[BootOnly(whenShared: true)] public function map(array $map): static; } diff --git a/src/contracts/src/Config/Repository.php b/src/contracts/src/Config/Repository.php index e84a5b1c30..b0e57b6095 100644 --- a/src/contracts/src/Config/Repository.php +++ b/src/contracts/src/Config/Repository.php @@ -5,6 +5,7 @@ namespace Hypervel\Contracts\Config; use Closure; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; interface Repository @@ -70,27 +71,27 @@ public function all(): array; /** * Set a given configuration value. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. */ + #[BootOrTestsOnly(whenShared: true)] public function set(array|string $key, mixed $value = null): void; /** * Prepend a value onto an array configuration value. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. */ + #[BootOrTestsOnly(whenShared: true)] public function prepend(string $key, mixed $value): void; /** * Push a value onto an array configuration value. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. */ + #[BootOrTestsOnly(whenShared: true)] public function push(string $key, mixed $value): void; } diff --git a/src/contracts/src/Console/Kernel.php b/src/contracts/src/Console/Kernel.php index 113d5b0128..c1d757fe60 100644 --- a/src/contracts/src/Console/Kernel.php +++ b/src/contracts/src/Console/Kernel.php @@ -8,6 +8,7 @@ use Hypervel\Console\Scheduling\Schedule; use Hypervel\Foundation\Bus\PendingDispatch; use Hypervel\Foundation\Console\ClosureCommand; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Input\InputInterface; @@ -36,6 +37,7 @@ public function resolveConsoleSchedule(): Schedule; * * @param-closure-this ClosureCommand $callback */ + #[BootOrTestsOnly(whenShared: true)] public function command(string $signature, Closure $callback): ClosureCommand; /** diff --git a/src/contracts/src/Container/Container.php b/src/contracts/src/Container/Container.php index feba3993ba..e8dbb54b8f 100644 --- a/src/contracts/src/Container/Container.php +++ b/src/contracts/src/Container/Container.php @@ -5,6 +5,9 @@ namespace Hypervel\Contracts\Container; use Closure; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; use LogicException; use Psr\Container\ContainerInterface; @@ -29,11 +32,13 @@ public function bound(string $abstract): bool; * * @throws LogicException */ + #[BootOnly(whenShared: true)] public function alias(string $abstract, string $alias): void; /** * Assign a set of tags to a given binding. */ + #[BootOnly(whenShared: true)] public function tag(array|string $abstracts, array|string $tags): void; /** @@ -44,36 +49,43 @@ public function tagged(string $tag): iterable; /** * Register a binding with the container. */ + #[BootOnly(whenShared: true)] public function bind(Closure|string $abstract, Closure|string|null $concrete = null, bool $shared = false): void; /** * Bind a callback to resolve with Container::call. */ + #[BootOnly(whenShared: true)] public function bindMethod(array|string $method, Closure $callback): void; /** * Register a binding if it hasn't already been registered. */ + #[BootOnly(whenShared: true)] public function bindIf(Closure|string $abstract, Closure|string|null $concrete = null, bool $shared = false): void; /** * Register a shared binding in the container. */ + #[BootOnly(whenShared: true)] public function singleton(Closure|string $abstract, Closure|string|null $concrete = null): void; /** * Register a shared binding if it hasn't already been registered. */ + #[BootOnly(whenShared: true)] public function singletonIf(Closure|string $abstract, Closure|string|null $concrete = null): void; /** * Register a scoped binding in the container. */ + #[BootOnly(whenShared: true)] public function scoped(Closure|string $abstract, Closure|string|null $concrete = null): void; /** * Register a scoped binding if it hasn't already been registered. */ + #[BootOnly(whenShared: true)] public function scopedIf(Closure|string $abstract, Closure|string|null $concrete = null): void; /** @@ -91,6 +103,7 @@ public function isScopedInstance(mixed $instance): bool; * * @throws InvalidArgumentException */ + #[BootOnly(whenShared: true)] public function extend(string $abstract, Closure $closure): void; /** @@ -101,16 +114,19 @@ public function extend(string $abstract, Closure $closure): void; * @param TInstance $instance * @return TInstance */ + #[BootOrTestsOnly(whenShared: true)] public function instance(string $abstract, mixed $instance): mixed; /** * Remove a resolved instance from the instance cache. */ + #[BootOrTestsOnly(whenShared: true)] public function forgetInstance(string $abstract): void; /** * Add a contextual binding to the container. */ + #[BootOnly(whenShared: true)] public function addContextualBinding(string $concrete, string $abstract, mixed $implementation): void; /** @@ -131,6 +147,7 @@ public function factory(string $abstract): Closure; /** * Flush the container of all bindings and resolved instances. */ + #[TestsOnly] public function flush(): void; /** @@ -207,20 +224,24 @@ public function resolved(string $abstract): bool; /** * Register a new rebinding callback for an abstract type. */ + #[BootOnly(whenShared: true)] public function rebinding(string $abstract, Closure $callback): mixed; /** * Register a new before resolving callback. */ + #[BootOnly(whenShared: true)] public function beforeResolving(Closure|string $abstract, ?Closure $callback = null): void; /** * Register a new resolving callback. */ + #[BootOnly(whenShared: true)] public function resolving(Closure|string $abstract, ?Closure $callback = null): void; /** * Register a new after resolving callback. */ + #[BootOnly(whenShared: true)] public function afterResolving(Closure|string $abstract, ?Closure $callback = null): void; } diff --git a/src/contracts/src/Database/ModelIdentifier.php b/src/contracts/src/Database/ModelIdentifier.php index 7c6385349f..ab23aadc15 100644 --- a/src/contracts/src/Database/ModelIdentifier.php +++ b/src/contracts/src/Database/ModelIdentifier.php @@ -6,6 +6,7 @@ use Hypervel\Database\Eloquent\Collection; use Hypervel\Database\Eloquent\Relations\Relation; +use Hypervel\Support\Attributes\BootOnly; /** * Do not use constructor property promotion here. @@ -104,9 +105,10 @@ public function getClass(): ?string /** * Indicate whether to use the relational morph-map when serializing Models. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every model identifier serialization across all coroutines. + * The flag persists in a static property for the worker lifetime and applies + * to every model identifier serialization across all coroutines. */ + #[BootOnly] public static function useMorphMap(bool $useMorphMap = true): void { static::$useMorphMap = $useMorphMap; diff --git a/src/contracts/src/Engine/CoroutineInterface.php b/src/contracts/src/Engine/CoroutineInterface.php index 9afc2869b7..0ef0f45fdf 100644 --- a/src/contracts/src/Engine/CoroutineInterface.php +++ b/src/contracts/src/Engine/CoroutineInterface.php @@ -7,6 +7,7 @@ use ArrayObject; use Hypervel\Engine\Exceptions\CoroutineDestroyedException; use Hypervel\Engine\Exceptions\RunningInNonCoroutineException; +use Hypervel\Support\Attributes\BootOnly; interface CoroutineInterface { @@ -43,9 +44,10 @@ public static function pid(?int $id = null): int; /** * Set the process-wide coroutine configuration. * - * Boot-only. The configuration affects every coroutine subsequently - * created in the worker process. + * The configuration affects every coroutine subsequently created in + * the worker process. */ + #[BootOnly] public static function set(array $config): void; /** diff --git a/src/contracts/src/Events/Dispatcher.php b/src/contracts/src/Events/Dispatcher.php index 5106076fe4..7e3a7fbc0c 100644 --- a/src/contracts/src/Events/Dispatcher.php +++ b/src/contracts/src/Events/Dispatcher.php @@ -6,12 +6,15 @@ use Closure; use Hypervel\Events\QueuedClosure; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface Dispatcher { /** * Register an event listener with the dispatcher. */ + #[BootOnly(whenShared: true)] public function listen( array|Closure|QueuedClosure|string $events, array|object|string|null $listener = null @@ -22,6 +25,7 @@ public function listen( * * Observers receive dispatched events but are not counted by hasListeners(). */ + #[BootOnly(whenShared: true)] public function observe(array|string $events, array|object|string $observer): void; /** @@ -32,6 +36,7 @@ public function hasListeners(string $eventName): bool; /** * Register an event subscriber with the dispatcher. */ + #[BootOnly(whenShared: true)] public function subscribe(object|string $subscriber): void; /** @@ -59,6 +64,7 @@ public function flush(string $event): void; /** * Remove a set of listeners from the dispatcher. */ + #[BootOrTestsOnly(whenShared: true)] public function forget(string $event): void; /** diff --git a/src/contracts/src/Foundation/Application.php b/src/contracts/src/Foundation/Application.php index 83a494f7c4..d056ffb79d 100644 --- a/src/contracts/src/Foundation/Application.php +++ b/src/contracts/src/Foundation/Application.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Contracts\Container\Container; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\ServiceProvider; use RuntimeException; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -34,6 +35,7 @@ public function hasBeenBootstrapped(): bool; * * @return $this */ + #[BootOnly(whenShared: true)] public function setBasePath(string $basePath): static; /** @@ -121,11 +123,13 @@ public function isProduction(): bool; /** * Detect the application's current environment. */ + #[BootOnly(whenShared: true)] public function detectEnvironment(Closure $callback): string; /** * Set the callback to resolve the application's environment. */ + #[BootOnly(whenShared: true)] public function resolveEnvironmentUsing(?callable $callback): void; /** @@ -138,6 +142,7 @@ public function environmentPath(): ?string; * * @return $this */ + #[BootOnly(whenShared: true)] public function useEnvironmentPath(string $path): static; /** @@ -145,6 +150,7 @@ public function useEnvironmentPath(string $path): static; * * @return $this */ + #[BootOnly(whenShared: true)] public function loadEnvironmentFrom(string $file): static; /** @@ -193,11 +199,13 @@ public function isDownForMaintenance(): bool; /** * Register all of the configured providers. */ + #[BootOnly(whenShared: true)] public function registerConfiguredProviders(): void; /** * Register a service provider with the application. */ + #[BootOnly(whenShared: true)] public function register(ServiceProvider|string $provider, bool $force = false): ServiceProvider; /** @@ -223,11 +231,13 @@ public function boot(): void; /** * Register a new boot listener. */ + #[BootOnly(whenShared: true)] public function booting(callable $callback): void; /** * Register a new "booted" listener. */ + #[BootOnly(whenShared: true)] public function booted(callable $callback): void; /** @@ -315,6 +325,7 @@ public function shouldSkipMiddleware(): bool; * * @return $this */ + #[BootOnly(whenShared: true)] public function terminating(callable|string $callback): static; /** diff --git a/src/contracts/src/Http/Kernel.php b/src/contracts/src/Http/Kernel.php index 1f61e552ed..42514fbd50 100644 --- a/src/contracts/src/Http/Kernel.php +++ b/src/contracts/src/Http/Kernel.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Foundation\Application; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\HttpFoundation\Response; interface Kernel @@ -33,83 +34,91 @@ public function hasMiddleware(string $middleware): bool; /** * Add a new middleware to the beginning of the stack if it does not already exist. * - * Boot-only. Middleware persists in the kernel's global stack for the worker - * lifetime and runs on every subsequent request across all coroutines. + * Middleware persists in the kernel's global stack for the worker lifetime + * and runs on every subsequent request across all coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function prependMiddleware(string $middleware): static; /** * Add a new middleware to the end of the stack if it does not already exist. * - * Boot-only. Middleware persists in the kernel's global stack for the worker - * lifetime and runs on every subsequent request across all coroutines. + * Middleware persists in the kernel's global stack for the worker lifetime + * and runs on every subsequent request across all coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function pushMiddleware(string $middleware): static; /** * Prepend the given middleware to the given middleware group. * - * Boot-only. The middleware group persists in the kernel for the worker - * lifetime and runs on every subsequent request matching the group. + * The middleware group persists in the kernel for the worker lifetime + * and runs on every subsequent request matching the group. * * @return $this */ + #[BootOnly] public function prependMiddlewareToGroup(string $group, string $middleware): static; /** * Append the given middleware to the given middleware group. * - * Boot-only. The middleware group persists in the kernel for the worker - * lifetime and runs on every subsequent request matching the group. + * The middleware group persists in the kernel for the worker lifetime + * and runs on every subsequent request matching the group. * * @return $this */ + #[BootOnly] public function appendMiddlewareToGroup(string $group, string $middleware): static; /** * Prepend the given middleware to the middleware priority list. * - * Boot-only. The middleware priority list persists in the kernel for the - * worker lifetime and affects middleware ordering for every subsequent request. + * The middleware priority list persists in the kernel for the worker lifetime + * and affects middleware ordering for every subsequent request. * * @return $this */ + #[BootOnly] public function prependToMiddlewarePriority(string $middleware): static; /** * Append the given middleware to the middleware priority list. * - * Boot-only. The middleware priority list persists in the kernel for the - * worker lifetime and affects middleware ordering for every subsequent request. + * The middleware priority list persists in the kernel for the worker lifetime + * and affects middleware ordering for every subsequent request. * * @return $this */ + #[BootOnly] public function appendToMiddlewarePriority(string $middleware): static; /** * Add the given middleware to the middleware priority list before other middleware. * - * Boot-only. The middleware priority list persists in the kernel for the - * worker lifetime and affects middleware ordering for every subsequent request. + * The middleware priority list persists in the kernel for the worker lifetime + * and affects middleware ordering for every subsequent request. * * @param array|string $before * @return $this */ + #[BootOnly] public function addToMiddlewarePriorityBefore(string|array $before, string $middleware): static; /** * Add the given middleware to the middleware priority list after other middleware. * - * Boot-only. The middleware priority list persists in the kernel for the - * worker lifetime and affects middleware ordering for every subsequent request. + * The middleware priority list persists in the kernel for the worker lifetime + * and affects middleware ordering for every subsequent request. * * @param array|string $after * @return $this */ + #[BootOnly] public function addToMiddlewarePriorityAfter(string|array $after, string $middleware): static; /** @@ -129,12 +138,13 @@ public function getGlobalMiddleware(): array; /** * Set the application's global middleware. * - * Boot-only. Middleware persists in the kernel's global stack for the worker - * lifetime and runs on every subsequent request across all coroutines. + * Middleware persists in the kernel's global stack for the worker lifetime + * and runs on every subsequent request across all coroutines. * * @param array $middleware * @return $this */ + #[BootOnly] public function setGlobalMiddleware(array $middleware): static; /** @@ -147,12 +157,13 @@ public function getMiddlewareGroups(): array; /** * Set the application's middleware groups. * - * Boot-only. Middleware groups persist in the kernel for the worker lifetime - * and affect every subsequent request matching those groups. + * Middleware groups persist in the kernel for the worker lifetime and affect + * every subsequent request matching those groups. * * @param array> $groups * @return $this */ + #[BootOnly] public function setMiddlewareGroups(array $groups): static; /** @@ -165,23 +176,25 @@ public function getMiddlewareAliases(): array; /** * Set the application's route middleware aliases. * - * Boot-only. Middleware aliases persist in the kernel for the worker lifetime - * and affect every subsequent request that uses them. + * Middleware aliases persist in the kernel for the worker lifetime and affect + * every subsequent request that uses them. * * @param array $aliases * @return $this */ + #[BootOnly] public function setMiddlewareAliases(array $aliases): static; /** * Set the application's middleware priority. * - * Boot-only. The middleware priority list persists in the kernel for the - * worker lifetime and affects middleware ordering for every subsequent request. + * The middleware priority list persists in the kernel for the worker lifetime + * and affects middleware ordering for every subsequent request. * * @param string[] $priority * @return $this */ + #[BootOnly] public function setMiddlewarePriority(array $priority): static; /** diff --git a/src/contracts/src/Image/Driver.php b/src/contracts/src/Image/Driver.php index 3007b0f984..5ae063f962 100644 --- a/src/contracts/src/Image/Driver.php +++ b/src/contracts/src/Image/Driver.php @@ -5,6 +5,7 @@ namespace Hypervel\Contracts\Image; use Hypervel\Image\ImagePipeline; +use Hypervel\Support\Attributes\BootOnly; interface Driver { @@ -30,9 +31,10 @@ public function dominantColor(string $contents): string; /** * Register a transformation handler. * - * Boot-only. The handler persists on a cached driver for the worker lifetime and affects every subsequent image processed by that driver. + * The handler persists on a cached driver for the worker lifetime and affects every subsequent image processed by that driver. * * @param class-string $transformation */ + #[BootOnly(whenShared: true)] public function transformUsing(string $transformation, callable $callback): static; } diff --git a/src/contracts/src/ObjectPool/Factory.php b/src/contracts/src/ObjectPool/Factory.php index bee7e128cf..5006d1fea9 100644 --- a/src/contracts/src/ObjectPool/Factory.php +++ b/src/contracts/src/ObjectPool/Factory.php @@ -5,6 +5,7 @@ namespace Hypervel\Contracts\ObjectPool; use Hypervel\ObjectPool\PoolDefinition; +use Hypervel\Support\Attributes\BootOrTestsOnly; interface Factory { @@ -61,8 +62,9 @@ public function purge(string $identity, ?ObjectPool $expected = null): bool; /** * Remove and close every registered pool. * - * Boot or tests only. This clears worker-lifetime pools shared by every - * coroutine; use targeted removal for runtime resource recovery. + * This clears worker-lifetime pools shared by every coroutine; use + * targeted removal for runtime resource recovery. */ + #[BootOrTestsOnly] public function purgeAll(): void; } diff --git a/src/contracts/src/ObjectPool/Recycler.php b/src/contracts/src/ObjectPool/Recycler.php index 0d3aad897c..fe863cdbc8 100644 --- a/src/contracts/src/ObjectPool/Recycler.php +++ b/src/contracts/src/ObjectPool/Recycler.php @@ -4,21 +4,25 @@ namespace Hypervel\Contracts\ObjectPool; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; + interface Recycler { /** * Start periodic pool maintenance. * - * Boot-only. Starting during a request schedules worker-wide maintenance - * that affects every subsequently registered pool. + * Starting during a request schedules worker-wide maintenance that + * affects every subsequently registered pool. */ + #[BootOnly] public function start(): void; /** * Stop automatic maintenance of objects in managed pools. * - * Boot or tests only. Stopping disables automatic maintenance for every - * pool in the worker. + * Stopping disables automatic maintenance for every pool in the worker. */ + #[BootOrTestsOnly] public function stop(): void; } diff --git a/src/contracts/src/Queue/Monitor.php b/src/contracts/src/Queue/Monitor.php index de95d08dcd..d88adbd88a 100644 --- a/src/contracts/src/Queue/Monitor.php +++ b/src/contracts/src/Queue/Monitor.php @@ -4,25 +4,31 @@ namespace Hypervel\Contracts\Queue; +use Hypervel\Support\Attributes\BootOnly; + interface Monitor { /** * Register a callback to be executed on every iteration through the queue loop. */ + #[BootOnly] public function looping(mixed $callback): void; /** * Register a callback to be executed when a job fails after the maximum number of retries. */ + #[BootOnly] public function failing(mixed $callback): void; /** * Register a callback to be executed when a daemon queue is starting. */ + #[BootOnly] public function starting(mixed $callback): void; /** * Register a callback to be executed when a daemon queue is stopping. */ + #[BootOnly] public function stopping(mixed $callback): void; } diff --git a/src/contracts/src/Queue/Queue.php b/src/contracts/src/Queue/Queue.php index 8c602bacff..89f3ddc4a4 100644 --- a/src/contracts/src/Queue/Queue.php +++ b/src/contracts/src/Queue/Queue.php @@ -6,6 +6,7 @@ use DateInterval; use DateTimeInterface; +use Hypervel\Support\Attributes\BootOrTestsOnly; use UnitEnum; interface Queue @@ -80,5 +81,6 @@ public function getConnectionName(): string; * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setConnectionName(string $name): static; } diff --git a/src/contracts/src/Redis/Connection.php b/src/contracts/src/Redis/Connection.php index 261238687e..98dc0d17f5 100644 --- a/src/contracts/src/Redis/Connection.php +++ b/src/contracts/src/Redis/Connection.php @@ -5,17 +5,20 @@ namespace Hypervel\Contracts\Redis; use Closure; +use Hypervel\Support\Attributes\BootOnly; interface Connection { /** * Register a Redis command listener with the connection. */ + #[BootOnly] public function listen(Closure $callback): void; /** * Register a Redis command failure listener with the connection. */ + #[BootOnly] public function listenForFailures(Closure $callback): void; /** diff --git a/src/contracts/src/Routing/BindingRegistrar.php b/src/contracts/src/Routing/BindingRegistrar.php index d3cdf85f4a..fce8633e4c 100644 --- a/src/contracts/src/Routing/BindingRegistrar.php +++ b/src/contracts/src/Routing/BindingRegistrar.php @@ -5,12 +5,14 @@ namespace Hypervel\Contracts\Routing; use Closure; +use Hypervel\Support\Attributes\BootOnly; interface BindingRegistrar { /** * Add a new route parameter binder. */ + #[BootOnly(whenShared: true)] public function bind(string $key, string|callable $binder): void; /** diff --git a/src/contracts/src/Routing/UrlGenerator.php b/src/contracts/src/Routing/UrlGenerator.php index bb7f01859f..fafa010c82 100644 --- a/src/contracts/src/Routing/UrlGenerator.php +++ b/src/contracts/src/Routing/UrlGenerator.php @@ -7,6 +7,7 @@ use BackedEnum; use DateInterval; use DateTimeInterface; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; interface UrlGenerator @@ -75,5 +76,6 @@ public function getRootControllerNamespace(): ?string; * * @return $this */ + #[BootOnly(whenShared: true)] public function setRootControllerNamespace(string $rootNamespace): static; } diff --git a/src/contracts/src/ServerProcess/ProcessInterface.php b/src/contracts/src/ServerProcess/ProcessInterface.php index b1e574c976..d58a8f315e 100644 --- a/src/contracts/src/ServerProcess/ProcessInterface.php +++ b/src/contracts/src/ServerProcess/ProcessInterface.php @@ -4,6 +4,7 @@ namespace Hypervel\Contracts\ServerProcess; +use Hypervel\Support\Attributes\BootOnly; use Swoole\Server; interface ProcessInterface @@ -11,6 +12,7 @@ interface ProcessInterface /** * Create process objects and bind them to the server. */ + #[BootOnly] public function bind(Server $server): void; /** diff --git a/src/contracts/src/Session/Session.php b/src/contracts/src/Session/Session.php index d8c7e058d6..a0f7873cb1 100644 --- a/src/contracts/src/Session/Session.php +++ b/src/contracts/src/Session/Session.php @@ -5,6 +5,7 @@ namespace Hypervel\Contracts\Session; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use SessionHandlerInterface; use UnitEnum; @@ -18,6 +19,7 @@ public function getName(): string; /** * Set the name of the session. */ + #[BootOnly(whenShared: true)] public function setName(string $name): void; /** diff --git a/src/contracts/src/Translation/Loader.php b/src/contracts/src/Translation/Loader.php index 13e5040785..75d8880015 100644 --- a/src/contracts/src/Translation/Loader.php +++ b/src/contracts/src/Translation/Loader.php @@ -4,6 +4,8 @@ namespace Hypervel\Contracts\Translation; +use Hypervel\Support\Attributes\BootOnly; + interface Loader { /** @@ -14,16 +16,19 @@ public function load(string $locale, string $group, ?string $namespace = null): /** * Add a new namespace to the loader. */ + #[BootOnly(whenShared: true)] public function addNamespace(string $namespace, string $hint): void; /** * Add a new JSON path to the loader. */ + #[BootOnly(whenShared: true)] public function addJsonPath(string $path): void; /** * Add a new path to the loader. */ + #[BootOnly(whenShared: true)] public function addPath(string $path): void; /** diff --git a/src/contracts/src/Translation/Translator.php b/src/contracts/src/Translation/Translator.php index 022ec7a60e..d3b1ab8433 100644 --- a/src/contracts/src/Translation/Translator.php +++ b/src/contracts/src/Translation/Translator.php @@ -5,6 +5,7 @@ namespace Hypervel\Contracts\Translation; use Countable; +use Hypervel\Support\Attributes\BootOnly; interface Translator { @@ -48,9 +49,10 @@ public function getFallback(): string; /** * Set the fallback locale being used. * - * Boot-only. The fallback is shared by the worker's Translator instance and - * affects every subsequent translation lookup in that worker. Use - * setLocale() for a current-request locale override. + * The fallback is shared by the worker's Translator instance and affects + * every subsequent translation lookup in that worker. Use setLocale() for a + * current-request locale override. */ + #[BootOnly(whenShared: true)] public function setFallback(string $fallback): void; } diff --git a/src/contracts/src/Validation/Factory.php b/src/contracts/src/Validation/Factory.php index 49d552f41f..22ce2c4454 100644 --- a/src/contracts/src/Validation/Factory.php +++ b/src/contracts/src/Validation/Factory.php @@ -5,6 +5,7 @@ namespace Hypervel\Contracts\Validation; use Closure; +use Hypervel\Support\Attributes\BootOnly; interface Factory { @@ -16,15 +17,18 @@ public function make(array $data, array $rules, array $messages = [], array $att /** * Register a custom validator extension. */ + #[BootOnly(whenShared: true)] public function extend(string $rule, Closure|string $extension, ?string $message = null): void; /** * Register a custom implicit validator extension. */ + #[BootOnly(whenShared: true)] public function extendImplicit(string $rule, Closure|string $extension, ?string $message = null): void; /** * Register a custom implicit validator message replacer. */ + #[BootOnly(whenShared: true)] public function replacer(string $rule, Closure|string $replacer): void; } diff --git a/src/contracts/src/View/Factory.php b/src/contracts/src/View/Factory.php index d1404d8159..11843d28d4 100644 --- a/src/contracts/src/View/Factory.php +++ b/src/contracts/src/View/Factory.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Contracts\Support\Arrayable; +use Hypervel\Support\Attributes\BootOnly; interface Factory { @@ -32,6 +33,7 @@ public function first(array $views, Arrayable|array $data = [], array $mergeData /** * Add a piece of shared data to the environment. */ + #[BootOnly(whenShared: true)] public function share(array|string $key, mixed $value = null): mixed; /** @@ -49,6 +51,7 @@ public function creator(array|string $views, Closure|string $callback): array; * * @return $this */ + #[BootOnly(whenShared: true)] public function addNamespace(string $namespace, string|array $hints): static; /** @@ -56,6 +59,7 @@ public function addNamespace(string $namespace, string|array $hints): static; * * @return $this */ + #[BootOnly(whenShared: true)] public function replaceNamespace(string $namespace, string|array $hints): static; /** diff --git a/src/cookie/composer.json b/src/cookie/composer.json index b6469f2bfe..b1ad2d37fd 100644 --- a/src/cookie/composer.json +++ b/src/cookie/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", diff --git a/src/cookie/src/CookieJar.php b/src/cookie/src/CookieJar.php index 967affd385..e807c9e766 100644 --- a/src/cookie/src/CookieJar.php +++ b/src/cookie/src/CookieJar.php @@ -9,6 +9,7 @@ use Hypervel\Context\RequestContext; use Hypervel\Contracts\Cookie\QueueingFactory as JarContract; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Traits\Macroable; use Symfony\Component\HttpFoundation\Cookie; @@ -203,12 +204,13 @@ protected function getPathAndDomain(?string $path, ?string $domain, ?bool $secur /** * Set the default path and domain for the jar. * - * Boot-only. The defaults persist on the worker-lifetime singleton; + * The defaults persist on the worker-lifetime singleton; * per-request mutation races across coroutines and affects every * subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function setDefaultPathAndDomain(string $path, ?string $domain, ?bool $secure = false, ?string $sameSite = null): static { [$this->path, $this->domain, $this->secure, $this->sameSite] = [$path, $domain, $secure, $sameSite]; diff --git a/src/cookie/src/Middleware/EncryptCookies.php b/src/cookie/src/Middleware/EncryptCookies.php index 4c28e3d13b..e054043ebc 100644 --- a/src/cookie/src/Middleware/EncryptCookies.php +++ b/src/cookie/src/Middleware/EncryptCookies.php @@ -10,6 +10,7 @@ use Hypervel\Cookie\CookieValuePrefix; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; @@ -55,9 +56,10 @@ public function __construct( /** * Disable encryption for the given cookie name(s). * - * Boot-only. The list persists for the worker's lifetime and applies to - * every subsequent request. + * On the shared middleware, the list persists for the worker's lifetime and + * applies to every subsequent request. */ + #[BootOnly(whenShared: true)] public function disableFor(array|string $name): void { $this->except = array_merge($this->except, (array) $name); @@ -194,9 +196,10 @@ public function isDisabled(string $name): bool /** * Indicate that the given cookies should never be encrypted. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every subsequent request. + * 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 { static::$neverEncrypt = array_values(array_unique( @@ -210,9 +213,10 @@ public static function except(array|string $cookies): void * When set, all other cookies pass through unencrypted. * Takes precedence over except() and the $except property. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The list persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function only(array|string $cookies): void { static::$onlyEncrypt = array_values(array_unique( diff --git a/src/core/composer.json b/src/core/composer.json index 7c2e37c35c..75f026ff63 100644 --- a/src/core/composer.json +++ b/src/core/composer.json @@ -31,6 +31,7 @@ "require": { "php": "^8.4", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", "hypervel/coordinator": "^0.4", diff --git a/src/core/src/Logger/StdoutLogger.php b/src/core/src/Logger/StdoutLogger.php index 391f039539..9e1dcd45bc 100644 --- a/src/core/src/Logger/StdoutLogger.php +++ b/src/core/src/Logger/StdoutLogger.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Config\Repository; use Hypervel\Contracts\Log\StdoutLoggerInterface; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use Psr\Log\InvalidArgumentException as PsrInvalidArgumentException; use Psr\Log\LoggerTrait; @@ -64,9 +65,10 @@ public function __construct(private Repository $config, ?OutputInterface $output /** * Reload the cached stdout logger configuration. * - * Boot-only. The cached format and enabled levels affect every subsequent - * log entry in the worker. + * The cached format and enabled levels affect every subsequent log entry + * in the worker. */ + #[BootOnly(whenShared: true)] public function reloadConfiguration(): void { $format = $this->config->string('app.stdout_log.format'); diff --git a/src/coroutine/composer.json b/src/coroutine/composer.json index ea3b37e020..7d185df7dd 100644 --- a/src/coroutine/composer.json +++ b/src/coroutine/composer.json @@ -33,6 +33,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/container": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", diff --git a/src/coroutine/src/Coroutine.php b/src/coroutine/src/Coroutine.php index 289e788a1a..8d259b5b10 100644 --- a/src/coroutine/src/Coroutine.php +++ b/src/coroutine/src/Coroutine.php @@ -13,6 +13,7 @@ use Hypervel\Engine\Coroutine as Co; use Hypervel\Engine\Exceptions\CoroutineDestroyedException; use Hypervel\Engine\Exceptions\RunningInNonCoroutineException; +use Hypervel\Support\Attributes\BootOnly; use Swoole\Coroutine\CanceledException; use Throwable; @@ -40,12 +41,13 @@ public static function id(): int /** * Register a callback to be called after a coroutine is created. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs for every subsequently created coroutine. Callbacks - * run synchronously during child startup and must not suspend. - * Hooks that inherit parent context must honor DETACHED_CONTEXT_KEY while - * preserving values explicitly installed in the child. + * The callback persists in a static property for the worker lifetime and + * runs for every subsequently created coroutine. Callbacks run + * synchronously during child startup and must not suspend. Hooks that + * inherit parent context must honor DETACHED_CONTEXT_KEY while preserving + * values explicitly installed in the child. */ + #[BootOnly] public static function afterCreated(callable $callback): void { static::$afterCreatedCallbacks[] = $callback; @@ -283,9 +285,10 @@ public static function preferCancellation(?Throwable $kept, Throwable $next): Th /** * Enable or disable exception reporting in coroutines. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every coroutine spawned across the process. + * The flag persists in a static property for the worker lifetime and + * applies to every coroutine spawned across the process. */ + #[BootOnly] public static function reportExceptions(bool $report = true): void { static::$reportExceptions = $report; diff --git a/src/data/composer.json b/src/data/composer.json index 249dae8055..328c6bc046 100644 --- a/src/data/composer.json +++ b/src/data/composer.json @@ -29,8 +29,9 @@ } }, "require": { - "ext-tokenizer": "*", "php": "^8.4", + "ext-tokenizer": "*", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", diff --git a/src/data/src/Attributes/Validation/AnyOf.php b/src/data/src/Attributes/Validation/AnyOf.php index eee4a1cbcd..37f60bba48 100644 --- a/src/data/src/Attributes/Validation/AnyOf.php +++ b/src/data/src/Attributes/Validation/AnyOf.php @@ -7,6 +7,7 @@ use Attribute; use Hypervel\Data\Exceptions\CannotBuildValidationRule; use Hypervel\Data\Support\Validation\ValidationPath; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Validation\Rules\AnyOf as BaseAnyOf; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] @@ -38,6 +39,7 @@ public static function keyword(): string /** * Reject string-based any-of rule construction. */ + #[Unsupported('Construct the validation attribute directly with typed arguments.')] public static function create(string ...$parameters): static { throw CannotBuildValidationRule::create('Cannot create an any-of rule from string parameters.'); diff --git a/src/data/src/Attributes/Validation/Can.php b/src/data/src/Attributes/Validation/Can.php index b36bdf06a5..3c52e1da62 100644 --- a/src/data/src/Attributes/Validation/Can.php +++ b/src/data/src/Attributes/Validation/Can.php @@ -7,6 +7,7 @@ use Attribute; use Hypervel\Data\Exceptions\CannotBuildValidationRule; use Hypervel\Data\Support\Validation\ValidationPath; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Validation\Rules\Can as BaseCan; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] @@ -41,6 +42,7 @@ public static function keyword(): string /** * Reject string-based can rule construction. */ + #[Unsupported('Construct the validation attribute directly with typed arguments.')] public static function create(string ...$parameters): static { throw CannotBuildValidationRule::create('Cannot create a can rule from string parameters.'); diff --git a/src/data/src/Attributes/Validation/Password.php b/src/data/src/Attributes/Validation/Password.php index ced77075c7..58f1826db6 100644 --- a/src/data/src/Attributes/Validation/Password.php +++ b/src/data/src/Attributes/Validation/Password.php @@ -8,6 +8,7 @@ use Hypervel\Data\Exceptions\CannotBuildValidationRule; use Hypervel\Data\Support\Validation\References\ExternalReference; use Hypervel\Data\Support\Validation\ValidationPath; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Validation\Rules\Password as BasePassword; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] @@ -87,6 +88,7 @@ public static function keyword(): string /** * Reject string-based password rule construction. */ + #[Unsupported('Construct the validation attribute directly with typed arguments.')] public static function create(string ...$parameters): static { throw CannotBuildValidationRule::create('Cannot create a password rule from string parameters.'); diff --git a/src/data/src/Concerns/PaginatedDataCollectable.php b/src/data/src/Concerns/PaginatedDataCollectable.php index 83b7f0ccd5..ddb2c2148c 100644 --- a/src/data/src/Concerns/PaginatedDataCollectable.php +++ b/src/data/src/Concerns/PaginatedDataCollectable.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Data\Exceptions\PaginatedCollectionIsAlwaysWrapped; +use Hypervel\Support\Attributes\Unsupported; /** * @template TKey of array-key @@ -40,6 +41,7 @@ public function count(): int /** * Disable wrapping for the collection. */ + #[Unsupported] public function withoutWrapping(): static { throw PaginatedCollectionIsAlwaysWrapped::create(); diff --git a/src/data/src/Support/DataConfig.php b/src/data/src/Support/DataConfig.php index e1b0e4c6b3..e56a74ef2c 100644 --- a/src/data/src/Support/DataConfig.php +++ b/src/data/src/Support/DataConfig.php @@ -12,6 +12,7 @@ use Hypervel\Data\RuleInferrers\RuleInferrer; use Hypervel\Data\Support\Creation\ValidationStrategy; use Hypervel\Data\Transformers\Transformer; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; class DataConfig @@ -149,11 +150,12 @@ public function __construct(Repository $config) /** * Register aliases that abstract data casts store instead of class names. * - * Boot-only. The aliases persist on the worker-lifetime configuration and - * affect every subsequent data cast in the worker. + * The aliases persist on the worker-lifetime configuration and affect + * every subsequent data cast in the worker. * * @param array> $map */ + #[BootOnly(whenShared: true)] public function enforceMorphMap(array $map): void { $morphMap = $this->morphMap; diff --git a/src/database/composer.json b/src/database/composer.json index c008e91121..037988f56b 100644 --- a/src/database/composer.json +++ b/src/database/composer.json @@ -30,6 +30,7 @@ "ext-pdo": "*", "ext-swoole": "^6.3.0RC1", "brick/math": "^1.0", + "hypervel/attributes": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", diff --git a/src/database/src/Capsule/Manager.php b/src/database/src/Capsule/Manager.php index 9b219904f5..7eabe10c2a 100644 --- a/src/database/src/Capsule/Manager.php +++ b/src/database/src/Capsule/Manager.php @@ -18,6 +18,7 @@ use Hypervel\Database\Query\Builder; use Hypervel\Database\Schema\Builder as SchemaBuilder; use Hypervel\Database\SimpleConnectionResolver; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Traits\CapsuleManagerTrait; use UnitEnum; @@ -129,7 +130,11 @@ public function addConnection(array $config, string $name = 'default'): void /** * Bootstrap Eloquent so it is ready for usage. + * + * The resolver and dispatcher persist in Eloquent's static properties for + * the worker lifetime and are used by every model across all coroutines. */ + #[BootOnly] public function bootEloquent(): void { Eloquent::setConnectionResolver($this->manager); diff --git a/src/database/src/Connection.php b/src/database/src/Connection.php index 77b0ce9743..81ff86eb0c 100755 --- a/src/database/src/Connection.php +++ b/src/database/src/Connection.php @@ -28,6 +28,8 @@ use Hypervel\Database\Schema\Builder as SchemaBuilder; use Hypervel\Filesystem\Filesystem; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Traits\Macroable; use LogicException; @@ -1269,7 +1271,7 @@ public function getErrorCount(): int /** * Register a database query listener with the connection. * - * Boot-only. Registers a listener on the worker-global event dispatcher; + * When the dispatcher is the application's worker-shared dispatcher, * per-request registration persists and affects subsequent requests. * * @param Closure(QueryExecuted): mixed $callback @@ -1681,8 +1683,13 @@ public function getEventDispatcher(): ?Dispatcher /** * Set the event dispatcher instance on the connection. * + * A reusable physical connection keeps the dispatcher after release, so + * every later coroutine that borrows it dispatches query and transaction + * events to it. + * * @return $this */ + #[TestsOnly(whenShared: true)] public function setEventDispatcher(Dispatcher $events): static { $this->events = $events; @@ -1692,7 +1699,12 @@ public function setEventDispatcher(Dispatcher $events): static /** * Unset the event dispatcher for this connection. + * + * A reusable physical connection keeps the null dispatcher after release, + * so every later coroutine that borrows it fires no query or transaction + * events. */ + #[TestsOnly(whenShared: true)] public function unsetEventDispatcher(): void { $this->events = null; @@ -1763,8 +1775,13 @@ public function hasPhysicalTransaction(): bool /** * Set the transaction manager instance on the connection. * + * A reusable physical connection keeps the manager after release, so + * every later coroutine that borrows it schedules after-commit callbacks + * on it. + * * @return $this */ + #[TestsOnly(whenShared: true)] public function setTransactionManager(DatabaseTransactionsManager $manager): static { $this->transactionsManager = $manager; @@ -1783,9 +1800,11 @@ public function getTransactionManager(): ?DatabaseTransactionsManager /** * Unset the transaction manager for this connection. * - * Tests only. A pooled connection keeps the null manager after release, so every - * later coroutine that borrows it fails when scheduling after-commit callbacks. + * A reusable physical connection keeps the null manager after release, so + * every later coroutine that borrows it fails when scheduling after-commit + * callbacks. */ + #[TestsOnly(whenShared: true)] public function unsetTransactionManager(): void { $this->transactionsManager = null; @@ -1986,10 +2005,11 @@ public function supportsSkipLocked(): bool /** * Register a connection resolver. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every subsequent Connection construction for the - * given driver across all coroutines. + * The resolver persists in a static property for the worker lifetime + * and runs on every subsequent Connection construction for the given + * driver across all coroutines. */ + #[BootOnly] public static function resolverFor(string $driver, Closure $callback): void { static::$resolvers[$driver] = $callback; diff --git a/src/database/src/Connectors/ConnectionFactory.php b/src/database/src/Connectors/ConnectionFactory.php index 15f6a3ada4..adcdada98c 100755 --- a/src/database/src/Connectors/ConnectionFactory.php +++ b/src/database/src/Connectors/ConnectionFactory.php @@ -15,6 +15,8 @@ use Hypervel\Database\PostgresConnection; use Hypervel\Database\SQLiteConnection; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use LogicException; use PDO; @@ -99,9 +101,10 @@ public function getExtension(array $config, ?string $name): ?callable /** * Register an extension connection resolver. * - * Boot-only. The resolver persists in the singleton factory's extensions - * array for the worker lifetime and applies to every subsequent connection. + * The resolver persists in the singleton factory's extensions array for the + * worker lifetime and applies to every subsequent connection. */ + #[BootOnly(whenShared: true)] public function extend(string $name, callable $resolver): void { $this->extensions[$name] = $resolver; @@ -110,10 +113,11 @@ public function extend(string $name, callable $resolver): void /** * Remove an extension connection resolver. * - * Boot or tests only. Mutates the singleton factory's extensions array; - * concurrent coroutines establishing connections may see the resolver - * removed mid-resolution. + * Mutates the singleton factory's extensions array; concurrent + * coroutines establishing connections may see the resolver removed + * mid-resolution. */ + #[BootOrTestsOnly(whenShared: true)] public function forgetExtension(string $name): void { unset($this->extensions[$name]); diff --git a/src/database/src/DatabaseCliManager.php b/src/database/src/DatabaseCliManager.php index 0d2779808c..3f55cd275b 100644 --- a/src/database/src/DatabaseCliManager.php +++ b/src/database/src/DatabaseCliManager.php @@ -4,6 +4,8 @@ namespace Hypervel\Database; +use Hypervel\Support\Attributes\BootOnly; + class DatabaseCliManager { /** @@ -16,11 +18,12 @@ class DatabaseCliManager /** * Register a database client resolver. * - * Boot-only. The resolver persists on the auto-singleton manager for the - * worker lifetime and applies to every subsequent database CLI session. + * The resolver persists on the auto-singleton manager for the worker + * lifetime and applies to every subsequent database CLI session. * * @param callable(array): DatabaseCliConfiguration $resolver */ + #[BootOnly(whenShared: true)] public function extend(string $driver, callable $resolver): void { $this->extensions[$driver] = $resolver; diff --git a/src/database/src/DatabaseManager.php b/src/database/src/DatabaseManager.php index d69271f58b..5d80b93f58 100755 --- a/src/database/src/DatabaseManager.php +++ b/src/database/src/DatabaseManager.php @@ -18,6 +18,10 @@ use Hypervel\Database\Events\QueryExecuted; use Hypervel\Database\Pool\PoolManager; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Fluent; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Traits\Macroable; @@ -213,6 +217,7 @@ public function resolveConnectionDirectly(string $name): ConnectionInterface * * @throws RuntimeException Always - dynamic connections not supported in Hypervel */ + #[Unsupported('Configure every connection in config/database.php.')] public function build(array $config): never { throw new RuntimeException( @@ -226,6 +231,7 @@ public function build(array $config): never * * @throws RuntimeException Always - dynamic connections not supported in Hypervel */ + #[Unsupported('Configure every connection in config/database.php.')] public function connectUsing(string $name, array $config, bool $force = false): never { throw new RuntimeException( @@ -324,11 +330,12 @@ protected function dispatchConnectionEstablishedEvent(Connection $connection): v * pooled connection), and flushes the pool. Use this when connection * configuration has changed and you need to fully reset. * - * Boot or tests only. Flushes the shared pool used by every coroutine; - * concurrent coroutines holding pooled connections will release them back - * into a discarded pool, and the current coroutine may briefly hold two - * pooled connections (the old one releases via defer at coroutine end). + * Flushes the shared pool used by every coroutine; concurrent coroutines + * holding pooled connections will release them back into a discarded + * pool, and the current coroutine may briefly hold two pooled connections + * (the old one releases via defer at coroutine end). */ + #[BootOrTestsOnly] public function purge(UnitEnum|string|null $name = null): void { if ($name instanceof UnitEnum) { @@ -539,8 +546,9 @@ public function getDefaultConnection(): ?string /** * Register a callback to be invoked when the current connection queries for longer than a given amount of time. * - * Boot-only. The callback persists on the database manager for the worker lifetime and affects every subsequent request for the same connection. + * The callback persists on the database manager for the worker lifetime and affects every subsequent request for the same connection. */ + #[BootOnly] public function whenQueryingForLongerThan(DateTimeInterface|CarbonInterval|float|int $threshold, callable $handler): void { $connectionName = $this->getEffectiveConnectionName(); @@ -743,9 +751,10 @@ public function availableDrivers(): array * by both the pooled path (PooledConnection → factory) and the non-pooled * path (DatabaseManager → factory). * - * Boot-only. The resolver persists on the singleton ConnectionFactory for - * the worker lifetime and applies to every subsequent connection. + * The resolver persists on the singleton ConnectionFactory for the worker + * lifetime and applies to every subsequent connection. */ + #[BootOnly] public function extend(string $name, callable $resolver): void { $this->factory->extend($name, $resolver); @@ -754,10 +763,11 @@ public function extend(string $name, callable $resolver): void /** * Remove an extension connection resolver. * - * Boot or tests only. Mutates the singleton ConnectionFactory's extension - * registry; concurrent coroutines establishing connections may see the - * resolver removed mid-resolution. + * Mutates the singleton ConnectionFactory's extension registry; + * concurrent coroutines establishing connections may see the resolver + * removed mid-resolution. */ + #[BootOrTestsOnly] public function forgetExtension(string $name): void { $this->factory->forgetExtension($name); @@ -790,10 +800,11 @@ public function getConnections(): array * PHP class name would falsely report bound. bound() only checks * bindings/instances/aliases. * - * Boot or tests only. Calls purge() on every connection (flushes the - * shared pools); concurrent coroutines lose their cached connection - * references mid-request. + * Calls purge() on every connection (flushes the shared pools); + * concurrent coroutines lose their cached connection references + * mid-request. */ + #[BootOrTestsOnly] public static function purgeConnections(): void { $container = Container::getInstance(); @@ -813,10 +824,11 @@ public static function purgeConnections(): void /** * Set the database reconnector callback. * - * Boot-only. The reconnector persists on the singleton DatabaseManager for - * the worker lifetime and is invoked for every disconnected connection - * across all coroutines. + * The reconnector persists on the singleton DatabaseManager for the worker + * lifetime and is invoked for every disconnected connection across all + * coroutines. */ + #[BootOnly(whenShared: true)] public function setReconnector(callable $reconnector): void { $this->reconnector = $reconnector(...); @@ -825,11 +837,12 @@ public function setReconnector(callable $reconnector): void /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's application reference; per-request use - * races across coroutines and breaks every concurrent database operation. + * Swaps the singleton's application reference; per-request use races + * across coroutines and breaks every concurrent database operation. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Application $app): static { $this->app = $app; diff --git a/src/database/src/DatabaseTransactionsManager.php b/src/database/src/DatabaseTransactionsManager.php index ee0d949e9c..67ab2c49fd 100755 --- a/src/database/src/DatabaseTransactionsManager.php +++ b/src/database/src/DatabaseTransactionsManager.php @@ -6,6 +6,7 @@ use Hypervel\Context\CoroutineContext; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Throwable; @@ -393,9 +394,10 @@ public static function hasNonCoroutinePendingTransactions(): bool /** * Copy transaction state from the current coroutine to non-coroutine storage. * - * Tests only. This copies coroutine-local transaction state into worker-global - * storage, so request-time use can leak or race across concurrent requests. + * This copies coroutine-local transaction state into worker-global storage, so + * request-time use can leak or race across concurrent requests. */ + #[TestsOnly] public static function copyToNonCoroutineState(): void { CoroutineContext::setNonCoroutine( @@ -407,9 +409,10 @@ public static function copyToNonCoroutineState(): void /** * Restore the test lifecycle's transaction state in the current coroutine. * - * Tests only. This deliberately shares transaction ownership across setup, - * test and teardown; normal child coroutines must own separate transactions. + * This deliberately shares transaction ownership across setup, test and + * teardown; normal child coroutines must own separate transactions. */ + #[TestsOnly] public static function copyFromNonCoroutineState(): void { $state = CoroutineContext::getFromNonCoroutine(DatabaseTransactionState::CONTEXT_KEY); @@ -422,9 +425,10 @@ public static function copyFromNonCoroutineState(): void /** * Clear all transaction state from non-coroutine storage. * - * Tests only. This mutates worker-global transaction state, so request-time - * use can race with concurrent requests using the same storage. + * This mutates worker-global transaction state, so request-time use can + * race with concurrent requests using the same storage. */ + #[TestsOnly] public static function clearNonCoroutineState(): void { CoroutineContext::clearFromNonCoroutine([DatabaseTransactionState::CONTEXT_KEY]); diff --git a/src/database/src/Eloquent/Casts/Json.php b/src/database/src/Eloquent/Casts/Json.php index 70de1d0a71..fd5582d820 100644 --- a/src/database/src/Eloquent/Casts/Json.php +++ b/src/database/src/Eloquent/Casts/Json.php @@ -4,6 +4,8 @@ namespace Hypervel\Database\Eloquent\Casts; +use Hypervel\Support\Attributes\BootOnly; + class Json { private const int MAXIMUM_NESTING_DEPTH = 512; @@ -58,9 +60,10 @@ public static function decode(mixed $value, ?bool $associative = true): mixed /** * Encode all values using the given callable. * - * Boot-only. The encoder persists in a static property for the worker - * lifetime and affects every subsequent JSON cast encode. + * The encoder persists in a static property for the worker lifetime + * and affects every subsequent JSON cast encode. */ + #[BootOnly] public static function encodeUsing(?callable $encoder): void { static::$encoder = $encoder; @@ -69,9 +72,10 @@ public static function encodeUsing(?callable $encoder): void /** * Decode all values using the given callable. * - * Boot-only. The decoder persists in a static property for the worker - * lifetime and affects every subsequent JSON cast decode. + * The decoder persists in a static property for the worker lifetime + * and affects every subsequent JSON cast decode. */ + #[BootOnly] public static function decodeUsing(?callable $decoder): void { static::$decoder = $decoder; diff --git a/src/database/src/Eloquent/Concerns/HasAttributes.php b/src/database/src/Eloquent/Concerns/HasAttributes.php index 62f1c0f834..f9db09be04 100644 --- a/src/database/src/Eloquent/Concerns/HasAttributes.php +++ b/src/database/src/Eloquent/Concerns/HasAttributes.php @@ -37,6 +37,7 @@ use Hypervel\Database\Eloquent\Relations\Relation; use Hypervel\Database\LazyLoadingViolationException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Carbon; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; @@ -1434,9 +1435,10 @@ protected function castAttributeAsEncryptedString(string $key, #[SensitiveParame /** * Set the encrypter instance that will be used to encrypt attributes. * - * Boot-only. The encrypter persists in a static property for the worker - * lifetime and is used by primitive encrypted casts across all coroutines. + * The encrypter persists in a static property for the worker lifetime and + * is used by primitive encrypted casts across all coroutines. */ + #[BootOnly] public static function encryptUsing(?EncrypterContract $encrypter): void { static::$encrypter = $encrypter; @@ -1875,8 +1877,8 @@ protected function resolveCasterClass(string $key): mixed /** * Flush the caster cache for the model. * - * 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. */ public static function flushCasterCache(): void { diff --git a/src/database/src/Eloquent/Concerns/HasEvents.php b/src/database/src/Eloquent/Concerns/HasEvents.php index 52f3c2782a..bbca913475 100644 --- a/src/database/src/Eloquent/Concerns/HasEvents.php +++ b/src/database/src/Eloquent/Concerns/HasEvents.php @@ -11,6 +11,9 @@ use Hypervel\Events\NullDispatcher; use Hypervel\Events\QueuedClosure; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use InvalidArgumentException; use ReflectionClass; @@ -75,14 +78,15 @@ public static function resolveObserveAttributes(): array /** * Register observers with the model. * - * Boot-only. Observer listeners are registered on the worker-wide event - * dispatcher and persist for the worker lifetime; runtime use leaks - * listeners across every subsequent request. + * Observer listeners are registered on the worker-wide event dispatcher + * and persist for the worker lifetime; runtime use leaks listeners + * across every subsequent request. * * @param array|class-string|object $classes * * @throws InvalidArgumentException */ + #[BootOnly] public static function observe(object|array|string $classes): void { $instance = new static; @@ -95,10 +99,15 @@ public static function observe(object|array|string $classes): void /** * Register a single observer with the model. * + * Observer listeners persist on the worker-wide event dispatcher for the + * worker lifetime; runtime use leaks listeners across every subsequent + * request. + * * @param class-string|object $class * * @throws InvalidArgumentException */ + #[BootOnly] protected function registerObserver(object|string $class): void { $className = $this->resolveObserverClassName($class); @@ -193,8 +202,12 @@ public function removeObservableEvents(array|string $observables): void /** * Register a model event with the dispatcher. * + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. + * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] protected static function registerModelEvent(string $event, mixed $callback): void { if (isset(static::$dispatcher)) { @@ -267,12 +280,12 @@ protected function filterModelEventResults(mixed $result): mixed /** * Register a retrieved model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function retrieved(mixed $callback): void { static::registerModelEvent('retrieved', $callback); @@ -281,12 +294,12 @@ public static function retrieved(mixed $callback): void /** * Register a saving model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function saving(mixed $callback): void { static::registerModelEvent('saving', $callback); @@ -295,12 +308,12 @@ public static function saving(mixed $callback): void /** * Register a saved model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function saved(mixed $callback): void { static::registerModelEvent('saved', $callback); @@ -309,12 +322,12 @@ public static function saved(mixed $callback): void /** * Register an updating model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function updating(mixed $callback): void { static::registerModelEvent('updating', $callback); @@ -323,12 +336,12 @@ public static function updating(mixed $callback): void /** * Register an updated model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function updated(mixed $callback): void { static::registerModelEvent('updated', $callback); @@ -337,12 +350,12 @@ public static function updated(mixed $callback): void /** * Register a creating model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function creating(mixed $callback): void { static::registerModelEvent('creating', $callback); @@ -351,12 +364,12 @@ public static function creating(mixed $callback): void /** * Register a created model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function created(mixed $callback): void { static::registerModelEvent('created', $callback); @@ -365,12 +378,12 @@ public static function created(mixed $callback): void /** * Register a replicating model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function replicating(mixed $callback): void { static::registerModelEvent('replicating', $callback); @@ -379,12 +392,12 @@ public static function replicating(mixed $callback): void /** * Register a deleting model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function deleting(mixed $callback): void { static::registerModelEvent('deleting', $callback); @@ -393,12 +406,12 @@ public static function deleting(mixed $callback): void /** * Register a deleted model event with the dispatcher. * - * Boot-only. Listener persists on the worker-wide event dispatcher for the - * worker lifetime; runtime use leaks listeners across every subsequent - * request. + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. * * @param array|callable|class-string|QueuedClosure $callback */ + #[BootOnly] public static function deleted(mixed $callback): void { static::registerModelEvent('deleted', $callback); @@ -407,9 +420,10 @@ public static function deleted(mixed $callback): void /** * Remove all the event listeners for the model. * - * Boot or tests only. Removes listeners from the worker-wide event - * dispatcher; concurrent coroutines lose model events mid-request. + * Removes listeners from the worker-wide event dispatcher; + * concurrent coroutines lose model events mid-request. */ + #[BootOrTestsOnly] public static function flushEventListeners(): void { if (! isset(static::$dispatcher)) { @@ -455,9 +469,10 @@ public static function getEventDispatcher(): ?Dispatcher /** * Set the event dispatcher instance. * - * Boot-only. The dispatcher persists in a static property for the worker - * lifetime and is used by every model event firing across all coroutines. + * The dispatcher persists in a static property for the worker lifetime + * and is used by every model event firing across all coroutines. */ + #[BootOnly] public static function setEventDispatcher(Dispatcher $dispatcher): void { static::$dispatcher = $dispatcher; @@ -466,9 +481,10 @@ public static function setEventDispatcher(Dispatcher $dispatcher): void /** * Unset the event dispatcher for models. * - * Tests only. Clears the worker-wide event dispatcher shared by every - * coroutine; concurrent model operations will not fire events. + * Clears the worker-wide event dispatcher shared by every coroutine; + * concurrent model operations will not fire events. */ + #[TestsOnly] public static function unsetEventDispatcher(): void { static::$dispatcher = null; diff --git a/src/database/src/Eloquent/Concerns/HasGlobalScopes.php b/src/database/src/Eloquent/Concerns/HasGlobalScopes.php index 3d351e6a33..78dd9160f8 100644 --- a/src/database/src/Eloquent/Concerns/HasGlobalScopes.php +++ b/src/database/src/Eloquent/Concerns/HasGlobalScopes.php @@ -10,6 +10,8 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Scope; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use InvalidArgumentException; use ReflectionAttribute; @@ -54,15 +56,15 @@ public static function resolveGlobalScopeAttributes(): array /** * Register a new global scope on the model. * - * Boot-only. The scope persists in a static property for the worker - * lifetime and applies to every query for this model class across all - * coroutines. + * The scope persists in a static property for the worker lifetime and + * applies to every query for this model class across all coroutines. * * @param (Closure(Builder): mixed)|Scope|string $scope * @param null|(Closure(Builder): mixed)|Scope $implementation * * @throws InvalidArgumentException */ + #[BootOnly] public static function addGlobalScope(Scope|Closure|string $scope, Scope|Closure|null $implementation = null): mixed { if (is_string($scope) && ($implementation instanceof Closure || $implementation instanceof Scope)) { @@ -84,9 +86,10 @@ public static function addGlobalScope(Scope|Closure|string $scope, Scope|Closure /** * Register multiple global scopes on the model. * - * Boot-only. Delegates to addGlobalScope() per entry; each scope persists - * in a static property for the worker lifetime. + * Delegates to addGlobalScope() per entry; each scope persists in a + * static property for the worker lifetime. */ + #[BootOnly] public static function addGlobalScopes(array $scopes): void { foreach ($scopes as $key => $scope) { @@ -131,10 +134,11 @@ public static function getAllGlobalScopes(): array /** * Set the current global scopes. * - * Boot or tests only. Replaces the worker-wide global scopes registry - * shared by every coroutine; per-request use changes scope behavior for - * every concurrent model query. + * Replaces the worker-wide global scopes registry shared by every + * coroutine; per-request use changes scope behavior for every + * concurrent model query. */ + #[BootOrTestsOnly] public static function setAllGlobalScopes(array $scopes): void { static::$globalScopes = $scopes; diff --git a/src/database/src/Eloquent/Concerns/HasRelationships.php b/src/database/src/Eloquent/Concerns/HasRelationships.php index 8c1d9fdd21..7f42a13fc4 100644 --- a/src/database/src/Eloquent/Concerns/HasRelationships.php +++ b/src/database/src/Eloquent/Concerns/HasRelationships.php @@ -26,6 +26,7 @@ use Hypervel\Database\Eloquent\Relations\Pivot; use Hypervel\Database\Eloquent\Relations\Relation; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\StrCache; trait HasRelationships @@ -104,10 +105,11 @@ public function relationResolver(string $class, string $key): ?Closure /** * Define a dynamic relation resolver. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every relation resolution for this model class - * across all coroutines. + * The resolver persists in a static property for the worker lifetime + * and runs on every relation resolution for this model class across + * all coroutines. */ + #[BootOnly] public static function resolveRelationUsing(string $name, Closure $callback): void { static::$relationResolvers = array_replace_recursive( diff --git a/src/database/src/Eloquent/Factories/Factory.php b/src/database/src/Eloquent/Factories/Factory.php index a78a9d7326..9f90b7dc05 100644 --- a/src/database/src/Eloquent/Factories/Factory.php +++ b/src/database/src/Eloquent/Factories/Factory.php @@ -11,6 +11,7 @@ use Hypervel\Database\Eloquent\Collection as EloquentCollection; use Hypervel\Database\Eloquent\Factories\Attributes\UseModel; use Hypervel\Database\Eloquent\Model; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Enumerable; @@ -940,11 +941,12 @@ public function modelName(): string /** * Specify the callback that should be invoked to guess model names based on factory names. * - * Boot or tests only. The callback persists in a static property for the - * worker lifetime and runs on every model-name guess across all coroutines. + * The callback persists in a static property for the worker lifetime and + * runs on every model-name guess across all coroutines. * * @param callable(self): class-string $callback */ + #[BootOrTestsOnly] public static function guessModelNamesUsing(callable $callback): void { static::$modelNameResolvers[static::class] = $callback; @@ -953,9 +955,10 @@ public static function guessModelNamesUsing(callable $callback): void /** * Specify the default namespace that contains the application's model factories. * - * Boot or tests only. The namespace persists in a static property for the - * worker lifetime and applies to every subsequent factory-name lookup. + * The namespace persists in a static property for the worker lifetime and + * applies to every subsequent factory-name lookup. */ + #[BootOrTestsOnly] public static function useNamespace(string $namespace): void { static::$namespace = $namespace; @@ -979,12 +982,12 @@ public static function factoryForModel(string $modelName): self /** * Specify the callback that should be invoked to guess factory names based on dynamic relationship names. * - * Boot or tests only. The callback persists in a static property for the - * worker lifetime and runs on every factory-name guess across all - * coroutines. + * The callback persists in a static property for the worker lifetime and + * runs on every factory-name guess across all coroutines. * * @param callable(class-string): class-string $callback */ + #[BootOrTestsOnly] public static function guessFactoryNamesUsing(callable $callback): void { static::$factoryNameResolver = $callback; @@ -993,9 +996,10 @@ public static function guessFactoryNamesUsing(callable $callback): void /** * Specify that relationships should create parent relationships by default. * - * Boot or tests only. The flag persists in a static property for the worker - * lifetime and applies to every factory build across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every factory build across all coroutines. */ + #[BootOrTestsOnly] public static function expandRelationshipsByDefault(): void { static::$expandRelationshipsByDefault = true; @@ -1004,9 +1008,10 @@ public static function expandRelationshipsByDefault(): void /** * Specify that relationships should not create parent relationships by default. * - * Boot or tests only. The flag persists in a static property for the worker - * lifetime and applies to every factory build across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every factory build across all coroutines. */ + #[BootOrTestsOnly] public static function dontExpandRelationshipsByDefault(): void { static::$expandRelationshipsByDefault = false; diff --git a/src/database/src/Eloquent/Model.php b/src/database/src/Eloquent/Model.php index 279dd1b16e..04df153107 100644 --- a/src/database/src/Eloquent/Model.php +++ b/src/database/src/Eloquent/Model.php @@ -42,6 +42,9 @@ use Hypervel\Database\Query\Builder as QueryBuilder; use Hypervel\Engine\Coroutine; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\Str; use Hypervel\Support\StrCache; @@ -598,10 +601,10 @@ protected static function whenBooted(Closure $callback): void /** * Clear the list of booted models so they will be re-booted. * - * Boot or tests only. Clears worker-wide boot state, boot callbacks, and - * global scopes; concurrent coroutines mid-query may observe inconsistent - * model state. + * Clears worker-wide boot state, boot callbacks, and global scopes; + * concurrent coroutines mid-query may observe inconsistent model state. */ + #[BootOrTestsOnly] public static function clearBootedModels(): void { static::$booting = []; @@ -680,9 +683,10 @@ public static function isIgnoringTouch(?string $class = null): bool /** * Indicate that models should prevent lazy loading, silently discarding attributes, and accessing missing attributes. * - * Boot-only. Toggles worker-wide strict-mode flags that apply to every - * model operation across all coroutines. + * Toggles worker-wide strict-mode flags that apply to every model + * operation across all coroutines. */ + #[BootOnly] public static function shouldBeStrict(bool $shouldBeStrict = true): void { static::preventLazyLoading($shouldBeStrict); @@ -693,9 +697,10 @@ public static function shouldBeStrict(bool $shouldBeStrict = true): void /** * Prevent model relationships from being lazy loaded. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every relationship access across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every relationship access across all coroutines. */ + #[BootOnly] public static function preventLazyLoading(bool $value = true): void { static::$modelsShouldPreventLazyLoading = $value; @@ -704,9 +709,10 @@ public static function preventLazyLoading(bool $value = true): void /** * Determine if model relationships should be automatically eager loaded when accessed. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every relationship access across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every relationship access across all coroutines. */ + #[BootOnly] public static function automaticallyEagerLoadRelationships(bool $value = true): void { static::$modelsShouldAutomaticallyEagerLoadRelationships = $value; @@ -738,11 +744,12 @@ public static function withoutAutomaticallyEagerLoadingRelationships(callable $c /** * Register a callback that is responsible for handling lazy loading violations. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every lazy-loading violation across all coroutines. + * The callback persists in a static property for the worker lifetime and + * runs on every lazy-loading violation across all coroutines. * * @param null|(callable(self, string, LazyLoadingViolationException): mixed) $callback */ + #[BootOnly] public static function handleLazyLoadingViolationUsing(?callable $callback): void { static::$lazyLoadingViolationCallback = $callback; @@ -751,9 +758,10 @@ public static function handleLazyLoadingViolationUsing(?callable $callback): voi /** * Prevent non-fillable attributes from being silently discarded. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every mass assignment across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every mass assignment across all coroutines. */ + #[BootOnly] public static function preventSilentlyDiscardingAttributes(bool $value = true): void { static::$modelsShouldPreventSilentlyDiscardingAttributes = $value; @@ -762,12 +770,13 @@ public static function preventSilentlyDiscardingAttributes(bool $value = true): /** * Register a callback that is responsible for handling discarded attribute violations. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every discarded-attribute violation across all + * The callback persists in a static property for the worker lifetime + * and runs on every discarded-attribute violation across all * coroutines. * * @param null|(callable(self, array, MassAssignmentException): mixed) $callback */ + #[BootOnly] public static function handleDiscardedAttributeViolationUsing(?callable $callback): void { static::$discardedAttributeViolationCallback = $callback; @@ -776,9 +785,10 @@ public static function handleDiscardedAttributeViolationUsing(?callable $callbac /** * Prevent accessing missing attributes on retrieved models. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every attribute access across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every attribute access across all coroutines. */ + #[BootOnly] public static function preventAccessingMissingAttributes(bool $value = true): void { static::$modelsShouldPreventAccessingMissingAttributes = $value; @@ -787,12 +797,12 @@ public static function preventAccessingMissingAttributes(bool $value = true): vo /** * Register a callback that is responsible for handling missing attribute violations. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every missing-attribute violation across all - * coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every missing-attribute violation across all coroutines. * * @param null|(callable(self, string, MissingAttributeException): mixed) $callback */ + #[BootOnly] public static function handleMissingAttributeViolationUsing(?callable $callback): void { static::$missingAttributeViolationCallback = $callback; @@ -801,9 +811,10 @@ public static function handleMissingAttributeViolationUsing(?callable $callback) /** * Report invalid value exceptions during implicit route model binding. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every implicit route binding across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every implicit route binding across all coroutines. */ + #[BootOnly] public static function reportRouteModelBindingExceptions(bool $value = true): void { static::$reportRouteModelBindingExceptions = $value; @@ -2573,10 +2584,10 @@ public static function getConnectionResolver(): ?Resolver /** * Set the connection resolver instance. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and is used by every model's connection resolution across all - * coroutines. + * The resolver persists in a static property for the worker lifetime and + * is used by every model's connection resolution across all coroutines. */ + #[BootOnly] public static function setConnectionResolver(Resolver $resolver): void { static::$resolver = $resolver; @@ -2585,9 +2596,10 @@ public static function setConnectionResolver(Resolver $resolver): void /** * Unset the connection resolver for models. * - * Tests only. Clears the worker-wide connection resolver shared by every - * coroutine; concurrent model queries will fail or rebuild. + * Clears the worker-wide connection resolver shared by every coroutine; + * concurrent model queries will fail or rebuild. */ + #[TestsOnly] public static function unsetConnectionResolver(): void { static::$resolver = null; diff --git a/src/database/src/Eloquent/Relations/Relation.php b/src/database/src/Eloquent/Relations/Relation.php index 6d2328b879..06f61bc7f4 100644 --- a/src/database/src/Eloquent/Relations/Relation.php +++ b/src/database/src/Eloquent/Relations/Relation.php @@ -15,6 +15,7 @@ use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder as QueryBuilder; use Hypervel\Database\Query\Expression; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\Traits\ForwardsCalls; use Hypervel\Support\Traits\Macroable; @@ -524,9 +525,10 @@ protected function whereInMethod(Model $model, string $key): string /** * Prevent polymorphic relationships from being used without model mappings. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every polymorphic resolution across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every polymorphic resolution across all coroutines. */ + #[BootOnly] public static function requireMorphMap(bool $requireMorphMap = true): void { static::$requireMorphMap = $requireMorphMap; @@ -543,12 +545,13 @@ public static function requiresMorphMap(): bool /** * Define the morph map for polymorphic relations and require all morphed models to be explicitly mapped. * - * Boot-only. Sets both worker-wide morph state (requireMorphMap + morphMap) - * shared by every coroutine. + * Sets both worker-wide morph state (requireMorphMap + morphMap) shared by + * every coroutine. * * @param array> $map * @return array> */ + #[BootOnly] public static function enforceMorphMap(array $map, bool $merge = true): array { static::requireMorphMap(); @@ -559,13 +562,13 @@ public static function enforceMorphMap(array $map, bool $merge = true): array /** * Set or get the morph map for polymorphic relations. * - * Boot-only when setting. The map persists in a static property for the - * worker lifetime and applies to every polymorphic resolution across all - * coroutines. + * A given map persists in a static property for the worker lifetime and + * applies to every polymorphic resolution across all coroutines. * * @param null|array> $map * @return array> */ + #[BootOnly(whenNotDefault: 'map')] public static function morphMap(?array $map = null, bool $merge = true): array { $map = static::buildMorphMapFromModels($map); diff --git a/src/database/src/Eloquent/SoftDeletes.php b/src/database/src/Eloquent/SoftDeletes.php index a1d82fe52a..1e2b6ed0e9 100644 --- a/src/database/src/Eloquent/SoftDeletes.php +++ b/src/database/src/Eloquent/SoftDeletes.php @@ -5,6 +5,7 @@ namespace Hypervel\Database\Eloquent; use Hypervel\Events\QueuedClosure; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection as BaseCollection; /** @@ -209,7 +210,11 @@ public function trashed(): bool /** * Register a "softDeleted" model event callback with the dispatcher. + * + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. */ + #[BootOnly] public static function softDeleted(QueuedClosure|callable|string $callback): void { static::registerModelEvent('trashed', $callback); @@ -217,7 +222,11 @@ public static function softDeleted(QueuedClosure|callable|string $callback): voi /** * Register a "restoring" model event callback with the dispatcher. + * + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. */ + #[BootOnly] public static function restoring(QueuedClosure|callable|string $callback): void { static::registerModelEvent('restoring', $callback); @@ -225,7 +234,11 @@ public static function restoring(QueuedClosure|callable|string $callback): void /** * Register a "restored" model event callback with the dispatcher. + * + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. */ + #[BootOnly] public static function restored(QueuedClosure|callable|string $callback): void { static::registerModelEvent('restored', $callback); @@ -233,7 +246,11 @@ public static function restored(QueuedClosure|callable|string $callback): void /** * Register a "forceDeleting" model event callback with the dispatcher. + * + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. */ + #[BootOnly] public static function forceDeleting(QueuedClosure|callable|string $callback): void { static::registerModelEvent('forceDeleting', $callback); @@ -241,7 +258,11 @@ public static function forceDeleting(QueuedClosure|callable|string $callback): v /** * Register a "forceDeleted" model event callback with the dispatcher. + * + * Listener persists on the worker-wide event dispatcher for the worker + * lifetime; runtime use leaks listeners across every subsequent request. */ + #[BootOnly] public static function forceDeleted(QueuedClosure|callable|string $callback): void { static::registerModelEvent('forceDeleted', $callback); diff --git a/src/database/src/Migrations/MigrationCreator.php b/src/database/src/Migrations/MigrationCreator.php index 9574394dce..e2e1449fda 100644 --- a/src/database/src/Migrations/MigrationCreator.php +++ b/src/database/src/Migrations/MigrationCreator.php @@ -8,6 +8,7 @@ use Exception; use Hypervel\Context\CoroutineContext; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Date; use Hypervel\Support\Str; use InvalidArgumentException; @@ -172,11 +173,12 @@ protected function firePostCreateHooks(?string $table, string $path): void /** * Register a post migration create hook. * - * Boot-only. Hooks persist on the migration creator for the worker - * lifetime and run for every subsequent migration creation. + * Hooks persist on the shared migration creator for the worker lifetime and + * run for every subsequent migration creation. * * @param Closure(?string, string): void $callback */ + #[BootOnly(whenShared: true)] public function afterCreate(Closure $callback): void { $this->postCreate[] = $callback; diff --git a/src/database/src/Migrations/Migrator.php b/src/database/src/Migrations/Migrator.php index fdda985410..c52b17bcaa 100755 --- a/src/database/src/Migrations/Migrator.php +++ b/src/database/src/Migrations/Migrator.php @@ -27,6 +27,8 @@ use Hypervel\Database\Schema\Grammars\Grammar as SchemaGrammar; use Hypervel\Filesystem\Filesystem; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use InvalidArgumentException; @@ -646,8 +648,9 @@ public function getMigrationName(string $path): string /** * Register a custom migration path. * - * Boot-only. Paths remain on the shared migrator and affect every subsequent run. + * Paths remain on the shared migrator and affect every subsequent run. */ + #[BootOnly(whenShared: true)] public function path(string $path): void { $this->paths = array_unique(array_merge($this->paths, [$path])); @@ -666,11 +669,12 @@ public function paths(): array /** * Set the pending migrations to skip. * - * Boot or tests only. The list persists in a static property for the worker - * lifetime and applies to every subsequent migration run. + * The list persists in a static property for the worker lifetime and + * applies to every subsequent migration run. * * @param list $migrations */ + #[BootOrTestsOnly] public static function withoutMigrations(array $migrations): void { static::$withoutMigrations = $migrations; @@ -844,11 +848,12 @@ public function resolveConnection(?string $connection): Connection /** * Set a connection resolver callback. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every migration's connection resolution. + * The callback persists in a static property for the worker lifetime + * and runs on every migration's connection resolution. * * @param Closure(Resolver, ?string): Connection $callback */ + #[BootOnly] public static function resolveConnectionsUsing(Closure $callback): void { static::$connectionResolverCallback = $callback; diff --git a/src/database/src/PdoConnection.php b/src/database/src/PdoConnection.php index 8c25404035..e530b78be0 100755 --- a/src/database/src/PdoConnection.php +++ b/src/database/src/PdoConnection.php @@ -10,6 +10,7 @@ use Generator; use Hypervel\Coroutine\Coroutine; use Hypervel\Database\Events\StatementPrepared; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; use PDO; use PDOException; @@ -1026,10 +1027,10 @@ protected function resolveMaxBindings(): int /** * Register a database session configurator. * - * Boot-only. The configurator persists in a static property for the worker - * lifetime and runs on every subsequent synchronized PDO hand-out across all - * coroutines. + * The configurator persists in a static property for the worker lifetime and + * runs on every subsequent synchronized PDO hand-out across all coroutines. */ + #[BootOnly] public static function configureSessionUsing(SessionConfigurator $configurator): void { static::$sessionConfigurators[] = $configurator; diff --git a/src/database/src/Pool/DatabasePool.php b/src/database/src/Pool/DatabasePool.php index 19a7f99543..1654fdcace 100644 --- a/src/database/src/Pool/DatabasePool.php +++ b/src/database/src/Pool/DatabasePool.php @@ -17,6 +17,7 @@ use Hypervel\Database\Connectors\ConnectionFactory; use Hypervel\Database\SQLiteDatabase; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; use LogicException; use PDO; @@ -137,8 +138,9 @@ public function getSharedInMemorySqlitePdo(): ?PDO /** * Restore the shared in-memory database retained between tests. * - * Tests only. Replaces the database used by every subsequent pool borrower. + * Replaces the database used by every subsequent pool borrower. */ + #[TestsOnly(whenShared: true)] public function restoreSharedInMemorySqlitePdo(PDO $pdo): void { if ($this->getBorrowedCount() > 0) { diff --git a/src/database/src/Pool/PoolManager.php b/src/database/src/Pool/PoolManager.php index eb80d00850..7cdb2e6cfa 100644 --- a/src/database/src/Pool/PoolManager.php +++ b/src/database/src/Pool/PoolManager.php @@ -8,6 +8,7 @@ use Hypervel\Coroutine\Coroutine; use Hypervel\Database\ConnectionName; use Hypervel\Database\Connectors\ConnectionFactory; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Throwable; class PoolManager @@ -146,9 +147,10 @@ public function has(string $name): bool /** * Remove a pool and close its connections. * - * Boot or tests only. Closes a worker-shared pool; connections already - * checked out by concurrent coroutines are destroyed on release. + * Closes a worker-shared pool; connections already checked out by + * concurrent coroutines are destroyed on release. */ + #[BootOrTestsOnly] public function purge(string $name): void { $poolName = $this->getExistingPoolName($name); @@ -173,9 +175,10 @@ protected function getExistingPoolName(string $name): string /** * Remove every pool for a configured connection. * - * Boot or tests only. This closes shared worker pools and affects every - * coroutine that later resolves the same configured connection. + * This closes shared worker pools and affects every coroutine that + * later resolves the same configured connection. */ + #[BootOrTestsOnly] public function purgeForConnection(string $name): void { $base = ConnectionName::parse($name)->base; @@ -194,9 +197,10 @@ public function purgeForConnection(string $name): void /** * Remove all pools and close their connections. * - * Boot or tests only. Closes every worker-shared pool; connections already - * checked out by concurrent coroutines are destroyed on release. + * Closes every worker-shared pool; connections already checked out by + * concurrent coroutines are destroyed on release. */ + #[BootOrTestsOnly] public function purgeAll(): void { $pools = $this->pools; diff --git a/src/database/src/Query/Grammars/MariaDbGrammar.php b/src/database/src/Query/Grammars/MariaDbGrammar.php index cc9bc4b98d..a0889c9c0b 100755 --- a/src/database/src/Query/Grammars/MariaDbGrammar.php +++ b/src/database/src/Query/Grammars/MariaDbGrammar.php @@ -7,6 +7,7 @@ use Hypervel\Contracts\Database\Query\Expression; use Hypervel\Database\Query\Builder; use Hypervel\Database\Query\JoinLateralClause; +use Hypervel\Support\Attributes\Unsupported; use Override; use RuntimeException; @@ -26,6 +27,7 @@ protected function compileSelectTimeout(Builder $query, string $sql): string /** * Compile a "lateral join" clause. */ + #[Unsupported] public function compileJoinLateral(JoinLateralClause $join, string $expression): string { throw new RuntimeException('This database engine does not support lateral joins.'); diff --git a/src/database/src/Query/Grammars/PostgresGrammar.php b/src/database/src/Query/Grammars/PostgresGrammar.php index 89e5086923..29a49c0df4 100755 --- a/src/database/src/Query/Grammars/PostgresGrammar.php +++ b/src/database/src/Query/Grammars/PostgresGrammar.php @@ -8,6 +8,7 @@ use Hypervel\Database\Query\Builder; use Hypervel\Database\Query\JoinLateralClause; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Override; @@ -738,11 +739,12 @@ public function getOperators(): array /** * Set any Postgres grammar specific custom operators. * - * Boot-only. The operator list persists in a static property for the worker - * lifetime and applies to every Postgres query built across all coroutines. + * The operator list persists in a static property for the worker lifetime + * and applies to every Postgres query built across all coroutines. * * @param string[] $operators */ + #[BootOnly] public static function customOperators(array $operators): void { static::$customOperators = array_values( @@ -753,9 +755,10 @@ public static function customOperators(array $operators): void /** * Enable or disable the "cascade" option when compiling the truncate statement. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every truncate() across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every truncate() across all coroutines. */ + #[BootOnly] public static function cascadeOnTruncate(bool $value = true): void { static::$cascadeTruncate = $value; diff --git a/src/database/src/Schema/Builder.php b/src/database/src/Schema/Builder.php index 157a7cf3fc..fc3bd54333 100755 --- a/src/database/src/Schema/Builder.php +++ b/src/database/src/Schema/Builder.php @@ -11,6 +11,7 @@ use Hypervel\Database\MultipleColumnsSelectedException; use Hypervel\Database\PostgresConnection; use Hypervel\Database\Schema\Grammars\Grammar; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; use Hypervel\Support\Traits\Macroable; use InvalidArgumentException; @@ -75,11 +76,12 @@ public function __construct(Connection $connection) /** * Set the default string length for migrations. * - * Boot-only. The length persists in a static property for the worker - * lifetime and applies to every Blueprint::string() across all coroutines. + * The length persists in a static property for the worker lifetime and + * applies to every Blueprint::string() across all coroutines. * * @param non-negative-int $length */ + #[BootOnly] public static function defaultStringLength(int $length): void { static::$defaultStringLength = $length; @@ -88,9 +90,10 @@ public static function defaultStringLength(int $length): void /** * Set the default time precision for migrations. * - * Boot-only. The precision persists in a static property for the worker - * lifetime and applies to every time/datetime column across all coroutines. + * The precision persists in a static property for the worker lifetime and + * applies to every time/datetime column across all coroutines. */ + #[BootOnly] public static function defaultTimePrecision(?int $precision): void { static::$defaultTimePrecision = $precision; @@ -99,11 +102,12 @@ public static function defaultTimePrecision(?int $precision): void /** * Set the default morph key type for migrations. * - * Boot-only. The type persists in a static property for the worker lifetime - * and applies to every morph() column across all coroutines. + * The type persists in a static property for the worker lifetime and + * applies to every morph() column across all coroutines. * * @throws InvalidArgumentException */ + #[BootOnly] public static function defaultMorphKeyType(string $type): void { if (! in_array($type, ['int', 'uuid', 'ulid'])) { @@ -116,8 +120,9 @@ public static function defaultMorphKeyType(string $type): void /** * Set the default morph key type for migrations to UUIDs. * - * Boot-only. Sets the worker-wide morph type via defaultMorphKeyType(). + * Sets the worker-wide morph type via defaultMorphKeyType(). */ + #[BootOnly] public static function morphUsingUuids(): void { static::defaultMorphKeyType('uuid'); @@ -126,8 +131,9 @@ public static function morphUsingUuids(): void /** * Set the default morph key type for migrations to ULIDs. * - * Boot-only. Sets the worker-wide morph type via defaultMorphKeyType(). + * Sets the worker-wide morph type via defaultMorphKeyType(). */ + #[BootOnly] public static function morphUsingUlids(): void { static::defaultMorphKeyType('ulid'); diff --git a/src/database/src/Schema/ColumnDefinition.php b/src/database/src/Schema/ColumnDefinition.php index df7c145d3f..97431a0d0d 100644 --- a/src/database/src/Schema/ColumnDefinition.php +++ b/src/database/src/Schema/ColumnDefinition.php @@ -5,6 +5,7 @@ namespace Hypervel\Database\Schema; use Hypervel\Contracts\Database\Query\Expression; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Fluent; use LogicException; @@ -44,6 +45,7 @@ class ColumnDefinition extends Fluent /** * Reject partial index modifiers on column definitions. */ + #[Unsupported('Chain it onto an index: $table->index(...)->whereNotNull(...).')] public function whereNotNull(string $column): never { throw new LogicException( diff --git a/src/database/src/Schema/Grammars/SQLiteGrammar.php b/src/database/src/Schema/Grammars/SQLiteGrammar.php index 589a46edde..5d63961bab 100644 --- a/src/database/src/Schema/Grammars/SQLiteGrammar.php +++ b/src/database/src/Schema/Grammars/SQLiteGrammar.php @@ -9,6 +9,7 @@ use Hypervel\Database\Schema\IndexDefinition; use Hypervel\Database\Schema\SQLiteBuilder; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Collection; use Hypervel\Support\Fluent; use Override; @@ -635,6 +636,7 @@ public function compileIndex(Blueprint $blueprint, Fluent $command): string * * @throws RuntimeException */ + #[Unsupported] public function compileSpatialIndex(Blueprint $blueprint, Fluent $command): void { throw new RuntimeException('The database driver in use does not support spatial indexes.'); @@ -754,6 +756,7 @@ public function compileDropIndex(Blueprint $blueprint, Fluent $command): string * * @throws RuntimeException */ + #[Unsupported] public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command): void { throw new RuntimeException('The database driver in use does not support spatial indexes.'); diff --git a/src/database/src/Schema/SchemaProxy.php b/src/database/src/Schema/SchemaProxy.php index 7e0833d7e4..b5eb9f4eaf 100644 --- a/src/database/src/Schema/SchemaProxy.php +++ b/src/database/src/Schema/SchemaProxy.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Contracts\Container\Container; use Hypervel\Database\Connection; +use Hypervel\Support\Attributes\BootOnly; /** * @mixin Builder @@ -57,11 +58,12 @@ public function connection(?string $name = null): Builder /** * Set the default Schema Blueprint resolver callback. * - * Boot-only. The callback persists on the shared proxy for the worker - * lifetime and applies to every subsequent schema builder it creates. + * The callback persists on the shared proxy for the worker lifetime + * and applies to every subsequent schema builder it creates. * * @param Closure(Connection, string, null|Closure): Blueprint $resolver */ + #[BootOnly(whenShared: true)] public function blueprintResolver(Closure $resolver): void { $this->resolver = $resolver; diff --git a/src/di/composer.json b/src/di/composer.json index 46f6e77401..fb3ca1ce87 100644 --- a/src/di/composer.json +++ b/src/di/composer.json @@ -34,6 +34,7 @@ "require": { "php": "^8.4", "composer-runtime-api": "^2.2", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/container": "^0.4", "hypervel/filesystem": "^0.4", diff --git a/src/di/src/Aop/AspectCollector.php b/src/di/src/Aop/AspectCollector.php index 61445f6c5c..a849a48249 100644 --- a/src/di/src/Aop/AspectCollector.php +++ b/src/di/src/Aop/AspectCollector.php @@ -5,6 +5,9 @@ namespace Hypervel\Di\Aop; use Hypervel\Di\Exceptions\InvalidDefinitionException; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\ClassMetadataCache; use ReflectionProperty; @@ -25,11 +28,12 @@ class AspectCollector /** * Register an aspect using its public default targeting rules and priority. * - * Boot or tests only. Register before application proxy generation, or explicitly - * reopen registration for focused tests. Loaded classes cannot be rewritten. + * Register before application proxy generation, or explicitly reopen registration + * for focused tests. Loaded classes cannot be rewritten. * * @param class-string $aspect */ + #[BootOrTestsOnly] public static function register(string $aspect): void { $reflectionClass = ClassMetadataCache::reflectClass($aspect); @@ -52,10 +56,11 @@ public static function register(string $aspect): void /** * Register an aspect with its class targeting rules. * - * Boot or tests only. Aspect rules persist in a static property used by the - * compile-time proxy generator; runtime use cannot retroactively apply - * aspects to already-loaded classes. + * Aspect rules persist in a static property used by the compile-time proxy + * generator; runtime use cannot retroactively apply aspects to + * already-loaded classes. */ + #[BootOrTestsOnly] public static function setAround(string $aspect, array $classes, ?int $priority = null): void { if (static::$sealed) { @@ -83,8 +88,9 @@ public static function setAround(string $aspect, array $classes, ?int $priority /** * Close aspect registration after application proxy generation. * - * Boot-only. Later registrations in this worker are rejected. + * Later registrations in this worker are rejected. */ + #[BootOnly] public static function seal(): void { static::$sealed = true; @@ -93,9 +99,10 @@ public static function seal(): void /** * Reopen aspect registration while retaining the registered rules. * - * Boot or tests only. This permits changes to worker-wide rules for a new - * application bootstrap or focused tests; it does not rewrite loaded classes. + * This permits changes to worker-wide rules for a new application bootstrap + * or focused tests; it does not rewrite loaded classes. */ + #[BootOrTestsOnly] public static function unseal(): void { static::$sealed = false; @@ -112,9 +119,10 @@ public static function hasAspects(): bool /** * Remove a specific aspect from the registry. * - * Tests only. Mutates the worker-wide aspect registry; runtime use cannot + * Mutates the worker-wide aspect registry; runtime use cannot * retroactively update already-generated proxies. */ + #[TestsOnly] public static function forgetAspect(string $aspect): void { unset(static::$aspectRules[$aspect]); diff --git a/src/di/src/Aop/AspectManager.php b/src/di/src/Aop/AspectManager.php index 3eeee0d7fc..ffb0a60bfc 100644 --- a/src/di/src/Aop/AspectManager.php +++ b/src/di/src/Aop/AspectManager.php @@ -39,8 +39,8 @@ public static function set(string $class, string $method, Closure $value): void /** * Clear the compiled aspect chains. * - * 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. */ public static function clear(): void { diff --git a/src/di/src/Aop/AstVisitorRegistry.php b/src/di/src/Aop/AstVisitorRegistry.php index 811a4d9631..7b0b13525b 100644 --- a/src/di/src/Aop/AstVisitorRegistry.php +++ b/src/di/src/Aop/AstVisitorRegistry.php @@ -4,6 +4,7 @@ namespace Hypervel\Di\Aop; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\SplPriorityQueue; use InvalidArgumentException; @@ -36,9 +37,10 @@ public static function __callStatic(string $name, array $arguments): mixed /** * Insert a visitor class name with optional priority. * - * Boot-only. Visitor registrations persist in a static queue for the worker - * lifetime and are used by proxy generation. + * Visitor registrations persist in a static queue for the worker lifetime + * and are used by proxy generation. */ + #[BootOnly] public static function insert(string $value, int $priority = 0): bool { static::$values[] = $value; diff --git a/src/di/src/Bootstrap/GenerateProxies.php b/src/di/src/Bootstrap/GenerateProxies.php index 235acf7727..a088570a99 100644 --- a/src/di/src/Bootstrap/GenerateProxies.php +++ b/src/di/src/Bootstrap/GenerateProxies.php @@ -15,6 +15,7 @@ use Hypervel\Di\Aop\ProxyMethod; use Hypervel\Di\ClassMap\ClassMapManager; use Hypervel\Di\Exceptions\InvalidDefinitionException; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Composer; use ReflectionClass; @@ -55,8 +56,9 @@ public function bootstrap(ApplicationContract $app): void /** * Generate and register proxies in the given directory before their targets load. * - * Boot or tests only. Loaded classes cannot be replaced in the running process. + * Loaded classes cannot be replaced in the running process. */ + #[BootOrTestsOnly] public function generate(string $proxyDir): void { if (! AspectCollector::hasAspects()) { diff --git a/src/di/src/ClassMap/ClassMapManager.php b/src/di/src/ClassMap/ClassMapManager.php index fe85db97b2..299c160956 100644 --- a/src/di/src/ClassMap/ClassMapManager.php +++ b/src/di/src/ClassMap/ClassMapManager.php @@ -6,6 +6,8 @@ use Composer\Autoload\ClassLoader; use Hypervel\Di\Aop\ProxySource; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use ReflectionClass; use RuntimeException; @@ -33,11 +35,12 @@ class ClassMapManager * Each entry maps an original class name to the path of its replacement file. * Fails if a target is already loaded from a different source. * - * Boot-only. Class-map overrides mutate the worker's Composer autoloader and - * must be registered before application proxy generation and target loading. + * Class-map overrides mutate the worker's Composer autoloader and must be + * registered before application proxy generation and target loading. * * @param array $map */ + #[BootOnly] public static function add(array $map): void { if (static::$sealed) { @@ -79,8 +82,9 @@ public static function add(array $map): void /** * Close class map registration after application proxy generation. * - * Boot-only. Later registrations in this worker are rejected. + * Later registrations in this worker are rejected. */ + #[BootOnly] public static function seal(): void { static::$sealed = true; @@ -89,9 +93,10 @@ public static function seal(): void /** * Reopen class map registration while retaining the registered overrides. * - * Boot or tests only. This permits worker-wide autoloader changes for a new - * application bootstrap; already-loaded classes cannot be replaced. + * This permits worker-wide autoloader changes for a new application + * bootstrap; already-loaded classes cannot be replaced. */ + #[BootOrTestsOnly] public static function unseal(): void { static::$sealed = false; @@ -100,10 +105,11 @@ public static function unseal(): void /** * Publish generated replacements with the lifetime of their source overrides. * - * Boot-only. These autoload entries affect every request in the worker. + * These autoload entries affect every request in the worker. * * @param array $proxies */ + #[BootOnly] public static function applyProxies(array $proxies): void { static::$loader?->addClassMap(array_intersect_key($proxies, static::$entries)); diff --git a/src/docs/static-analysis.md b/src/docs/static-analysis.md index 5534500390..a61ba2123e 100644 --- a/src/docs/static-analysis.md +++ b/src/docs/static-analysis.md @@ -16,6 +16,7 @@ - [Model Serialization](#model-serialization) - [Relationships](#relationships) - [Custom Builders and Collections](#custom-builders-and-collections) + - [Method Attributes](#method-attributes) - [Custom PHPDoc Types](#custom-phpdoc-types) - [The `view-string` Type](#view-string) - [The `model-property` Type](#model-property) @@ -464,6 +465,58 @@ parameters: Your entries are added to the built-in mappings for `SoftDeletes` and Scout's `Searchable` trait. + +### Method Attributes + +Some public methods change state the whole worker shares and should only run while your application boots or in tests. Mark these methods with `#[BootOnly]`, `#[TestsOnly]` or `#[BootOrTestsOnly]` from `Hypervel\Support\Attributes`, and keep a short docblock explaining what goes wrong if the method runs later. The [worker state](#worker-state) checks do not report writes in these methods: + +```php +use Hypervel\Support\Attributes\TestsOnly; + +/** + * Set the request used to build URLs. + * + * The request stays on the shared service until a test replaces it. + */ +#[TestsOnly] +public function setRequestForTesting(Request $request): void +{ + $this->request = $request; +} +``` + +When only some calls are restricted, the attribute's arguments say which. `whenNotDefault` restricts calls that pass the named parameter a value other than its default, so calls that only read the current value are still allowed at runtime. `whenProvided` restricts any call that passes the parameter. `whenShared` restricts calls only on a worker-shared instance, one the container keeps for the whole worker rather than a scoped or newly created one, and suits methods that change only their own object. Use either `whenNotDefault` or `whenProvided` for a parameter with a default value; either may be combined with `whenShared`, which then restricts only calls meeting both conditions: + +```php +use Hypervel\Support\Attributes\BootOnly; + +#[BootOnly(whenNotDefault: 'map')] +public static function morphMap(?array $map = null, bool $merge = true): array +{ + // ... +} + +#[BootOnly(whenShared: true)] +public function globalOptions(array|Closure $options): static +{ + // ... +} +``` + +A public method that must exist but always throws, such as an interface method your implementation cannot support, may be marked with `#[Unsupported]`. Pass the supported alternative as its message when there is one: + +```php +use Hypervel\Support\Attributes\Unsupported; + +#[Unsupported('Use call() instead.')] +public function callAsync(string $method, array $arguments = []): never +{ + throw new LogicException('This client does not support asynchronous calls.'); +} +``` + +These attributes describe intended usage. They are not enforced at runtime, and the analysis does not check calls to marked methods. + ## Custom PHPDoc Types @@ -882,19 +935,7 @@ Request and job checks cover middleware, controller actions, route closures, for Shared services are identified from your application's container registrations, resolved classes, handlers and constructor dependencies, following the container's [resolution lifecycles](/docs/{{version}}/container#resolution-lifecycles). A constructor-injected model used as a fresh prototype is allowed. Objects created inside binding closures are not inspected, and job `handle` dependencies are followed only for jobs using `Dispatchable`. -Methods that only run while the application boots or in tests may say so with a docblock line starting with "Boot-only", "Tests only" or "Boot or tests only". Writes in these methods, and in `flushState` methods, are not reported: - -```php -/** - * Set the request used to build URLs. - * - * Tests only. The request stays on the shared service until a test replaces it. - */ -public function setRequestForTesting(Request $request): void -{ - $this->request = $request; -} -``` +Writes in methods marked as [boot-time or test-only](#method-attributes), and in `flushState` methods, are not reported. Other worker state, such as a lazily created connection or a registry filled while the application boots, is not reported. When a reported write is intended, ignore it with a reason. You may disable these checks using the `checkWorkerSharedState`, `checkStaticStateMutation` and `checkUnboundedWorkerCaches` parameters. diff --git a/src/doctor/composer.json b/src/doctor/composer.json index 4a29883bf3..0526bc7039 100644 --- a/src/doctor/composer.json +++ b/src/doctor/composer.json @@ -28,6 +28,7 @@ "php": "^8.4", "composer-runtime-api": "^2.2", "composer/semver": "^3.5.1", + "hypervel/attributes": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", diff --git a/src/doctor/src/Doctor.php b/src/doctor/src/Doctor.php index ffba7137c7..b4881a60a4 100644 --- a/src/doctor/src/Doctor.php +++ b/src/doctor/src/Doctor.php @@ -13,6 +13,7 @@ use Hypervel\Doctor\Results\DiagnosticReport; use Hypervel\Doctor\Results\DiagnosticResult; use Hypervel\Doctor\Results\FixResult; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use InvalidArgumentException; use LogicException; @@ -40,10 +41,11 @@ public function __construct(protected Container $container) /** * Register a diagnostic. * - * Boot-only. The registry belongs to the shared Doctor service and affects subsequent runs. + * On the shared Doctor service, the registration affects subsequent runs. * * @param class-string $diagnostic */ + #[BootOnly(whenShared: true)] public function diagnostic(string $diagnostic): self { if (! is_subclass_of($diagnostic, Diagnostic::class)) { @@ -58,10 +60,11 @@ public function diagnostic(string $diagnostic): self /** * Register multiple diagnostics. * - * Boot-only. The registry belongs to the shared Doctor service and affects subsequent runs. + * On the shared Doctor service, the registration affects subsequent runs. * * @param iterable $diagnostics */ + #[BootOnly(whenShared: true)] public function diagnostics(iterable $diagnostics): self { foreach ($diagnostics as $diagnostic) { diff --git a/src/encryption/composer.json b/src/encryption/composer.json index 3d05526efc..e6715b96b9 100644 --- a/src/encryption/composer.json +++ b/src/encryption/composer.json @@ -27,6 +27,7 @@ "php": "^8.4", "ext-mbstring": "*", "ext-openssl": "*", + "hypervel/attributes": "^0.4", "hypervel/config": "^0.4", "hypervel/console": "^0.4", "hypervel/contracts": "^0.4", diff --git a/src/encryption/src/Encrypter.php b/src/encryption/src/Encrypter.php index 6b6c9bda62..15a1ee6ab0 100644 --- a/src/encryption/src/Encrypter.php +++ b/src/encryption/src/Encrypter.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Encryption\Encrypter as EncrypterContract; use Hypervel\Contracts\Encryption\EncryptException; use Hypervel\Contracts\Encryption\StringEncrypter; +use Hypervel\Support\Attributes\BootOnly; use RuntimeException; use SensitiveParameter; @@ -338,11 +339,12 @@ public function getPreviousKeys(): array /** * Set the previous / legacy encryption keys that should be utilized if decryption fails. * - * Boot-only. Mutates the previous-key list; the encrypter is registered as - * a container singleton, so per-request use races across coroutines. + * Mutates the previous-key list; the encrypter is registered as a + * container singleton, so per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function previousKeys(#[SensitiveParameter] array $keys): static { foreach ($keys as $key) { diff --git a/src/engine/composer.json b/src/engine/composer.json index adf3b2a766..361060414c 100644 --- a/src/engine/composer.json +++ b/src/engine/composer.json @@ -27,9 +27,10 @@ "php": "^8.4", "ext-sockets": "*", "ext-swoole": "^6.3.0RC1", - "psr/http-message": "^2.0", + "hypervel/attributes": "^0.4", "hypervel/contracts": "^0.4", - "hypervel/support": "^0.4" + "hypervel/support": "^0.4", + "psr/http-message": "^2.0" }, "autoload": { "psr-4": { diff --git a/src/engine/src/Coroutine.php b/src/engine/src/Coroutine.php index 82fa7d83cc..c614bedc63 100644 --- a/src/engine/src/Coroutine.php +++ b/src/engine/src/Coroutine.php @@ -10,6 +10,7 @@ use Hypervel\Engine\Exceptions\CoroutineDestroyedException; use Hypervel\Engine\Exceptions\RunningInNonCoroutineException; use Hypervel\Engine\Exceptions\RuntimeException; +use Hypervel\Support\Attributes\BootOnly; use Swoole\Coroutine as SwooleCo; class Coroutine implements CoroutineInterface @@ -98,9 +99,10 @@ public static function pid(?int $id = null): int /** * Set the coroutine configuration. * - * Boot-only. Mutates Swoole's process-global coroutine configuration; calling - * this per-request changes settings for every concurrent coroutine in the worker. + * Mutates Swoole's process-global coroutine configuration; calling this + * per-request changes settings for every concurrent coroutine in the worker. */ + #[BootOnly] public static function set(array $config): void { SwooleCo::set($config); diff --git a/src/engine/src/Http/Stream.php b/src/engine/src/Http/Stream.php index 4f6f86c6aa..dfff8a7276 100755 --- a/src/engine/src/Http/Stream.php +++ b/src/engine/src/Http/Stream.php @@ -5,6 +5,7 @@ namespace Hypervel\Engine\Http; use Hypervel\Engine\Exceptions\RuntimeException; +use Hypervel\Support\Attributes\Unsupported; use Psr\Http\Message\StreamInterface; use Stringable; use Throwable; @@ -76,11 +77,11 @@ public function getSize(): ?int } /** - * Returns the current position of the file read/write pointer. + * Return the current position of the file read/write pointer. * - * @return int Position of the file pointer - * @throws RuntimeException on error + * @throws RuntimeException */ + #[Unsupported] public function tell(): int { throw new RuntimeException('Cannot determine the position of a SwooleStream'); @@ -110,28 +111,25 @@ public function isSeekable(): bool * Seek to a position in the stream. * * @see http://www.php.net/manual/en/function.fseek.php - * @param int $offset Stream offset * @param int $whence Specifies how the cursor position will be calculated * based on the seek offset. Valid values are identical to the built-in * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to * offset bytes SEEK_CUR: Set position to current location plus offset * SEEK_END: Set position to end-of-stream plus offset. - * @throws RuntimeException on failure + * @throws RuntimeException */ - public function seek($offset, $whence = SEEK_SET): void + #[Unsupported] + public function seek(int $offset, int $whence = SEEK_SET): void { throw new RuntimeException('Cannot seek a SwooleStream'); } /** * Seek to the beginning of the stream. - * If the stream is not seekable, this method will raise an exception; - * otherwise, it will perform a seek(0). * - * @throws RuntimeException on failure - * @see http://www.php.net/manual/en/function.fseek.php - * @see seek() + * @throws RuntimeException */ + #[Unsupported] public function rewind(): void { $this->seek(0); diff --git a/src/events/composer.json b/src/events/composer.json index 80cc87861c..aa6a38d201 100644 --- a/src/events/composer.json +++ b/src/events/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/bus": "^0.4", "hypervel/collections": "^0.4", "hypervel/container": "^0.4", diff --git a/src/events/src/Dispatcher.php b/src/events/src/Dispatcher.php index 108d9b5e63..2209fd83fb 100755 --- a/src/events/src/Dispatcher.php +++ b/src/events/src/Dispatcher.php @@ -41,6 +41,8 @@ use Hypervel\Queue\Attributes\Tries; use Hypervel\Queue\Attributes\UniqueFor; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Queue\Concerns\ResolvesQueueRoutes; use Hypervel\Support\Str; @@ -185,9 +187,10 @@ public function __construct(?ContainerContract $container = null) /** * Register an event listener with the dispatcher. * - * Boot-only. The listener registry persists on the singleton Dispatcher for - * the worker lifetime; per-request registration races across coroutines. + * The listener registry persists on the singleton Dispatcher for the worker + * lifetime; per-request registration races across coroutines. */ + #[BootOnly(whenShared: true)] public function listen(array|Closure|QueuedClosure|string $events, array|object|string|null $listener = null): void { if ($events instanceof Closure) { @@ -247,8 +250,8 @@ protected function setupWildcardListen(string $event, array|object|string $liste /** * Register a passive event observer. * - * Boot-only. The observer registry persists on the singleton Dispatcher for - * the worker lifetime; per-request registration races across coroutines. + * The observer registry persists on the singleton Dispatcher for the worker + * lifetime; per-request registration races across coroutines. * * Observers receive dispatched events but are not counted by hasListeners(). * They run after active listener processing ends, including ordinary listener @@ -259,6 +262,7 @@ protected function setupWildcardListen(string $event, array|object|string $liste * Use this for observability tooling (tracing, metrics, logging) that must * not influence whether guarded events fire. */ + #[BootOnly(whenShared: true)] public function observe(array|string $events, array|object|string $observer): void { foreach ((array) $events as $event) { @@ -373,9 +377,10 @@ public function flush(string $event): void /** * Register an event subscriber with the dispatcher. * - * Boot-only. Subscriber registrations persist on the singleton Dispatcher for - * the worker lifetime; per-request registration races across coroutines. + * Subscriber registrations persist on the singleton Dispatcher for the worker + * lifetime; per-request registration races across coroutines. */ + #[BootOnly(whenShared: true)] public function subscribe(object|string $subscriber): void { $subscriber = $this->resolveSubscriber($subscriber); @@ -1106,10 +1111,11 @@ protected function propagateListenerOptions(mixed $listener, CallQueuedListener /** * Remove a set of listeners from the dispatcher. * - * Boot or tests only. The listener registry persists on the singleton - * Dispatcher for the worker lifetime; per-request removal races across - * coroutines and can remove application listeners for other requests. + * The listener registry persists on the singleton Dispatcher for the + * worker lifetime; per-request removal races across coroutines and can + * remove application listeners for other requests. */ + #[BootOrTestsOnly(whenShared: true)] public function forget(string $event): void { if (str_contains($event, '*')) { @@ -1158,12 +1164,13 @@ protected function queueRoutesContainer(): ContainerContract /** * Set the queue resolver implementation. * - * Boot-only. The resolver persists on the singleton Dispatcher for the - * worker lifetime and is consulted whenever a queued listener is dispatched. + * The resolver persists on the singleton Dispatcher for the worker lifetime + * and is consulted whenever a queued listener is dispatched. * * @param callable(): QueueFactory $resolver * @return $this */ + #[BootOnly(whenShared: true)] public function setQueueResolver(callable $resolver): static { $this->queueResolver = $resolver; @@ -1186,12 +1193,13 @@ protected function resolveTransactionManager(): ?DatabaseTransactionsManager /** * Set the database transaction manager resolver implementation. * - * Boot-only. The resolver persists on the singleton Dispatcher for the - * worker lifetime and is consulted for every transaction-aware dispatch. + * The resolver persists on the singleton Dispatcher for the worker + * lifetime and is consulted for every transaction-aware dispatch. * * @param callable(): ?DatabaseTransactionsManager $resolver * @return $this */ + #[BootOnly(whenShared: true)] public function setTransactionManagerResolver(callable $resolver): static { $this->transactionManagerResolver = $resolver; diff --git a/src/events/src/NullDispatcher.php b/src/events/src/NullDispatcher.php index 9cb8f9109b..1bd3a526b2 100644 --- a/src/events/src/NullDispatcher.php +++ b/src/events/src/NullDispatcher.php @@ -50,6 +50,8 @@ public function until(object|string $event, mixed $payload = []): mixed /** * Register an event listener with the dispatcher. + * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. */ public function listen(array|Closure|QueuedClosure|string $events, array|object|string|null $listener = null): void { @@ -58,6 +60,8 @@ public function listen(array|Closure|QueuedClosure|string $events, array|object| /** * Register a passive event observer with the dispatcher. + * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. */ public function observe(array|string $events, array|object|string $observer): void { @@ -74,6 +78,8 @@ public function hasListeners(string $eventName): bool /** * Register an event subscriber with the dispatcher. + * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. */ public function subscribe(object|string $subscriber): void { @@ -90,6 +96,8 @@ public function flush(string $event): void /** * Remove a set of listeners from the dispatcher. + * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. */ public function forget(string $event): void { diff --git a/src/filesystem/composer.json b/src/filesystem/composer.json index 05eb34d767..cde888c021 100644 --- a/src/filesystem/composer.json +++ b/src/filesystem/composer.json @@ -27,6 +27,7 @@ "php": "^8.4", "aws/aws-sdk-php": "^3.322.9", "guzzlehttp/psr7": "^3.1", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/container": "^0.4", diff --git a/src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php b/src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php index 97cccd56f2..67331bf199 100644 --- a/src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php +++ b/src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php @@ -22,6 +22,8 @@ use Hypervel\Image\Image; use Hypervel\Image\ImageException; use Hypervel\ObjectPool\Lease; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Traits\Conditionable; use League\Flysystem\FilesystemOperator; use Psr\Http\Message\StreamInterface; @@ -506,6 +508,7 @@ public function deleteDirectory(string $directory): bool /** * Reject access to the borrowed Flysystem driver. */ + #[Unsupported('Use withDriver() for borrow-scoped access.')] public function getDriver(): never { throw $this->borrowedInternalsException(); @@ -514,6 +517,7 @@ public function getDriver(): never /** * Reject access to the borrowed Flysystem adapter. */ + #[Unsupported('Use withAdapter() for borrow-scoped access.')] public function getAdapter(): never { throw $this->borrowedInternalsException(); @@ -522,6 +526,7 @@ public function getAdapter(): never /** * Reject access to a borrowed client. */ + #[Unsupported('Use withClient() for borrow-scoped access.')] public function getClient(): never { throw $this->borrowedInternalsException(); @@ -592,9 +597,10 @@ public function getConfig(): array /** * Define a custom callback that generates file download responses. * - * Boot-only. The callback persists on the manager-cached disk for the - * worker lifetime and is written to every borrowed adapter. + * The callback persists on the manager-cached disk for the worker + * lifetime and is written to every borrowed adapter. */ + #[BootOnly(whenShared: true)] public function serveUsing(?Closure $callback): void { $this->serveCallback = $callback; @@ -603,9 +609,10 @@ public function serveUsing(?Closure $callback): void /** * Define a custom temporary URL builder callback. * - * Boot-only. The callback persists on the manager-cached disk for the - * worker lifetime and is written to every borrowed adapter. + * The callback persists on the manager-cached disk for the worker + * lifetime and is written to every borrowed adapter. */ + #[BootOnly(whenShared: true)] public function buildTemporaryUrlsUsing(?Closure $callback): void { $this->temporaryUrlCallback = $callback; @@ -614,9 +621,10 @@ public function buildTemporaryUrlsUsing(?Closure $callback): void /** * Define a custom temporary upload URL builder callback. * - * Boot-only. The callback persists on the manager-cached disk for the - * worker lifetime and is written to every borrowed adapter. + * The callback persists on the manager-cached disk for the worker + * lifetime and is written to every borrowed adapter. */ + #[BootOnly(whenShared: true)] public function buildTemporaryUploadUrlsUsing(?Closure $callback): void { $this->temporaryUploadUrlCallback = $callback; diff --git a/src/filesystem/src/FilesystemAdapter.php b/src/filesystem/src/FilesystemAdapter.php index 064e8fa9cc..8bae174f53 100644 --- a/src/filesystem/src/FilesystemAdapter.php +++ b/src/filesystem/src/FilesystemAdapter.php @@ -23,6 +23,7 @@ use Hypervel\Image\Image; use Hypervel\Image\ImageException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Json; use Hypervel\Support\Str; use Hypervel\Support\Traits\Conditionable; @@ -1143,9 +1144,10 @@ protected function parseVisibility(?string $visibility): ?string /** * Define a custom callback that generates file download responses. * - * Boot-only. The callback persists on the cached disk adapter for the - * worker lifetime and runs on every subsequent serve() call for that disk. + * The callback persists on the cached disk adapter for the worker lifetime + * and runs on every subsequent serve() call for that disk. */ + #[BootOnly(whenShared: true)] public function serveUsing(?Closure $callback): void { $this->serveCallback = $callback; @@ -1154,10 +1156,10 @@ public function serveUsing(?Closure $callback): void /** * Define a custom temporary URL builder callback. * - * Boot-only. The callback persists on the cached disk adapter for the - * worker lifetime and runs on every subsequent temporary URL generation for - * that disk. + * The callback persists on the cached disk adapter for the worker lifetime + * and runs on every subsequent temporary URL generation for that disk. */ + #[BootOnly(whenShared: true)] public function buildTemporaryUrlsUsing(?Closure $callback): void { $this->temporaryUrlCallback = $this->prepareTemporaryUrlCallback($callback); @@ -1166,10 +1168,11 @@ public function buildTemporaryUrlsUsing(?Closure $callback): void /** * Define a custom temporary upload URL builder callback. * - * Boot-only. The callback persists on the cached disk adapter for the - * worker lifetime and runs on every subsequent temporary upload URL + * The callback persists on the cached disk adapter for the worker + * lifetime and runs on every subsequent temporary upload URL * generation for that disk. */ + #[BootOnly(whenShared: true)] public function buildTemporaryUploadUrlsUsing(?Closure $callback): void { $this->temporaryUploadUrlCallback = $this->prepareTemporaryUrlCallback($callback); diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php index 80e8552fb5..f7eb3a57cb 100644 --- a/src/filesystem/src/FilesystemManager.php +++ b/src/filesystem/src/FilesystemManager.php @@ -20,6 +20,9 @@ use Hypervel\ObjectPool\Concerns\HasPoolProxy; use Hypervel\ObjectPool\PoolDefinition; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Aws\SerializedCredentialProvider; use Hypervel\Support\RebindsCallbacksToSelf; use Hypervel\Support\Str; @@ -986,13 +989,14 @@ protected function createFlysystem(FlysystemAdapter $adapter, array $config): Fi /** * Set the given disk instance. * - * Boot or tests only. Mutates the singleton's disk cache; concurrent - * coroutines may already hold a reference to the prior disk and next - * resolution will return the replacement. Any shared pool remains - * available until its idle TTL expires or purge() invalidates it. + * Mutates the singleton's disk cache; concurrent coroutines may + * already hold a reference to the prior disk and next resolution + * will return the replacement. Any shared pool remains available + * until its idle TTL expires or purge() invalidates it. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function set(UnitEnum|string $name, mixed $disk): static { $this->disks[enum_value($name)] = $disk; @@ -1033,14 +1037,15 @@ public function getDefaultDriver(): string /** * Unset the given disk instances. * - * Boot or tests only. Mutates the singleton's disk cache; concurrent - * coroutines may already hold a reference to the disk and next resolution - * will rebuild a wrapper. Shared pools remain available until their idle - * TTL expires or purge() deliberately invalidates them. + * Mutates the singleton's disk cache; concurrent coroutines may already + * hold a reference to the disk and next resolution will rebuild a + * wrapper. Shared pools remain available until their idle TTL expires or + * purge() deliberately invalidates them. * * @param array|string|UnitEnum $disk * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetDisk(UnitEnum|array|string $disk): static { foreach (Arr::wrap($disk) as $diskName) { @@ -1053,9 +1058,9 @@ public function forgetDisk(UnitEnum|array|string $disk): static /** * Disconnect the given disk, remove it from local cache, and close its pool. * - * Boot or tests only, plus operational recovery of broken pooled resources. - * Closing deliberately invalidates a shared pool; other converged disks - * acquire a fresh pool on their next operation. + * Use this to recover broken pooled resources. Closing deliberately + * invalidates a shared pool; other converged disks acquire a fresh pool on + * their next operation. */ public function purge(UnitEnum|string|null $name = null): void { @@ -1083,12 +1088,13 @@ public function purge(UnitEnum|string|null $name = null): void * Anonymous closures run in this manager's class scope; non-static closures * also receive the manager as $this. * - * Boot-only. The callback persists in the singleton's customCreators array - * (and the poolable list if $poolable is true) for the worker lifetime and - * applies to every subsequent disk resolution. + * The callback persists in the singleton's customCreators array (and the + * poolable list if $poolable is true) for the worker lifetime and applies + * to every subsequent disk resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback, bool $poolable = false): static { if ($poolable) { @@ -1110,11 +1116,12 @@ public function extend(string $driver, Closure $callback, bool $poolable = false /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's container reference; per-request use - * races across coroutines and breaks every concurrent filesystem operation. + * Swaps the singleton's container reference; per-request use races across + * coroutines and breaks every concurrent filesystem operation. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Container $app): static { $this->app = $app; diff --git a/src/filesystem/src/LocalFilesystemAdapter.php b/src/filesystem/src/LocalFilesystemAdapter.php index 6a681e32b7..9817a59f27 100644 --- a/src/filesystem/src/LocalFilesystemAdapter.php +++ b/src/filesystem/src/LocalFilesystemAdapter.php @@ -6,6 +6,7 @@ use Closure; use DateTimeInterface; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Traits\Conditionable; use RuntimeException; @@ -159,11 +160,12 @@ public function diskName(string $disk): static /** * Specify the registered route which serves this disk. * - * Boot-only. The route metadata persists on the cached adapter for the - * worker lifetime and affects every subsequent signed URL request. + * The route metadata persists on the cached adapter for the worker + * lifetime and affects every subsequent signed URL request. * * @return $this */ + #[BootOnly(whenShared: true)] public function servingRoute(?string $disk, string $prefix = ''): static { $this->servingRouteConfigured = true; @@ -176,12 +178,13 @@ public function servingRoute(?string $disk, string $prefix = ''): static /** * Indicate that signed URLs should serve the corresponding files. * - * Boot-only. The flag and resolver persist on the cached local disk adapter - * for the worker lifetime and apply to every subsequent signed URL request - * for that disk. + * The flag and resolver persist on the cached local disk adapter for the + * worker lifetime and apply to every subsequent signed URL request for that + * disk. * * @return $this */ + #[BootOnly(whenShared: true)] public function shouldServeSignedUrls(bool $serve = true, ?Closure $urlGeneratorResolver = null): static { $this->shouldServeSignedUrls = $serve; diff --git a/src/filesystem/src/ScopedFilesystemProxy.php b/src/filesystem/src/ScopedFilesystemProxy.php index 6d45d675f0..0a85954240 100644 --- a/src/filesystem/src/ScopedFilesystemProxy.php +++ b/src/filesystem/src/ScopedFilesystemProxy.php @@ -16,6 +16,7 @@ use Hypervel\Http\UploadedFile; use Hypervel\Image\Image; use Hypervel\Image\ImageException; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Traits\Conditionable; use League\Flysystem\PathNormalizer; use League\Flysystem\WhitespacePathNormalizer; @@ -694,6 +695,7 @@ public function deleteDirectory(string $directory): bool /** * Reject access to the inner Flysystem driver. */ + #[Unsupported] public function getDriver(): never { throw $this->unsupportedMutationOrInternal(__FUNCTION__); @@ -702,6 +704,7 @@ public function getDriver(): never /** * Reject access to the inner Flysystem adapter. */ + #[Unsupported] public function getAdapter(): never { throw $this->unsupportedMutationOrInternal(__FUNCTION__); @@ -710,6 +713,7 @@ public function getAdapter(): never /** * Reject access to the inner driver client. */ + #[Unsupported] public function getClient(): never { throw $this->unsupportedMutationOrInternal(__FUNCTION__); @@ -718,6 +722,7 @@ public function getClient(): never /** * Reject mutation of the shared inner disk's serve callback. */ + #[Unsupported] public function serveUsing(?Closure $callback): never { throw $this->unsupportedMutationOrInternal(__FUNCTION__); @@ -726,6 +731,7 @@ public function serveUsing(?Closure $callback): never /** * Reject mutation of the shared inner disk's temporary URL callback. */ + #[Unsupported] public function buildTemporaryUrlsUsing(?Closure $callback): never { throw $this->unsupportedMutationOrInternal(__FUNCTION__); @@ -734,6 +740,7 @@ public function buildTemporaryUrlsUsing(?Closure $callback): never /** * Reject mutation of the shared inner disk's temporary upload URL callback. */ + #[Unsupported] public function buildTemporaryUploadUrlsUsing(?Closure $callback): never { throw $this->unsupportedMutationOrInternal(__FUNCTION__); diff --git a/src/fortify/composer.json b/src/fortify/composer.json index dc00d849f9..c31b227179 100644 --- a/src/fortify/composer.json +++ b/src/fortify/composer.json @@ -26,6 +26,7 @@ "require": { "php": "^8.4", "chillerlan/php-qrcode": "^6.0", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/collections": "^0.4", "hypervel/config": "^0.4", diff --git a/src/fortify/src/Features.php b/src/fortify/src/Features.php index b4e461d250..f21f8b249b 100644 --- a/src/fortify/src/Features.php +++ b/src/fortify/src/Features.php @@ -6,6 +6,7 @@ use Hypervel\Container\Container; use Hypervel\Contracts\Config\Repository as Config; +use Hypervel\Support\Attributes\BootOnly; class Features { @@ -140,12 +141,12 @@ public static function updatePasswords(): string /** * Enable the two factor authentication feature. * - * Boot-only when options are supplied. Non-empty options mutate the process-global - * configuration repository and affect every subsequent request in the worker; - * calling without options performs no mutation and is safe at request time. + * Non-empty options mutate the process-global configuration repository and + * affect every subsequent request in the worker. * * @param array $options */ + #[BootOnly(whenNotDefault: 'options')] public static function twoFactorAuthentication(array $options = []): string { self::setOptions('two-factor-authentication', $options); @@ -156,12 +157,12 @@ public static function twoFactorAuthentication(array $options = []): string /** * Enable the passkeys feature. * - * Boot-only when options are supplied. Non-empty options mutate the process-global - * configuration repository and affect every subsequent request in the worker; - * calling without options performs no mutation and is safe at request time. + * Non-empty options mutate the process-global configuration repository and + * affect every subsequent request in the worker. * * @param array $options */ + #[BootOnly(whenNotDefault: 'options')] public static function passkeys(array $options = []): string { self::setOptions('passkeys', $options); diff --git a/src/fortify/src/Fortify.php b/src/fortify/src/Fortify.php index d479b739a3..1b9e6c1867 100644 --- a/src/fortify/src/Fortify.php +++ b/src/fortify/src/Fortify.php @@ -25,6 +25,7 @@ use Hypervel\Fortify\Contracts\VerifyEmailViewResponse; use Hypervel\Fortify\Http\Responses\SimpleViewResponse; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use RuntimeException; class Fortify @@ -97,10 +98,11 @@ public static function redirects(string $redirect, mixed $default = null, ?Reque /** * Register a request-aware redirect resolver. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent response for the named Fortify redirect. + * The callback persists in static state for the worker lifetime and affects every subsequent response for the named Fortify redirect. * * @param null|(callable(Request): (null|string)) $callback */ + #[BootOnly] public static function redirectUsing(string $redirect, ?callable $callback): void { if ($callback === null) { @@ -115,8 +117,9 @@ public static function redirectUsing(string $redirect, ?callable $callback): voi /** * Register the views for Fortify using conventional names under the given namespace. * - * Boot-only. The response bindings persist in the container for the worker lifetime and affect every subsequent request. + * The response bindings persist in the container for the worker lifetime and affect every subsequent request. */ + #[BootOnly] public static function viewNamespace(string $namespace): void { static::viewPrefix($namespace . '::'); @@ -125,8 +128,9 @@ public static function viewNamespace(string $namespace): void /** * Register the views for Fortify using conventional names under the given prefix. * - * Boot-only. The response bindings persist in the container for the worker lifetime and affect every subsequent request. + * The response bindings persist in the container for the worker lifetime and affect every subsequent request. */ + #[BootOnly] public static function viewPrefix(string $prefix): void { static::loginView($prefix . 'login'); @@ -141,8 +145,9 @@ public static function viewPrefix(string $prefix): void /** * Specify which view should be used as the login view. * - * Boot-only. The response binding persists in the container for the worker lifetime and affects every subsequent request. + * The response binding persists in the container for the worker lifetime and affects every subsequent request. */ + #[BootOnly] public static function loginView(callable|string $view): void { self::container()->singleton(LoginViewResponse::class, static fn (): SimpleViewResponse => new SimpleViewResponse($view)); @@ -151,8 +156,9 @@ public static function loginView(callable|string $view): void /** * Specify which view should be used as the two factor authentication challenge view. * - * Boot-only. The response binding persists in the container for the worker lifetime and affects every subsequent request. + * The response binding persists in the container for the worker lifetime and affects every subsequent request. */ + #[BootOnly] public static function twoFactorChallengeView(callable|string $view): void { self::container()->singleton(TwoFactorChallengeViewResponse::class, static fn (): SimpleViewResponse => new SimpleViewResponse($view)); @@ -161,8 +167,9 @@ public static function twoFactorChallengeView(callable|string $view): void /** * Specify which view should be used as the new password view. * - * Boot-only. The response binding persists in the container for the worker lifetime and affects every subsequent request. + * The response binding persists in the container for the worker lifetime and affects every subsequent request. */ + #[BootOnly] public static function resetPasswordView(callable|string $view): void { self::container()->singleton(ResetPasswordViewResponse::class, static fn (): SimpleViewResponse => new SimpleViewResponse($view)); @@ -171,8 +178,9 @@ public static function resetPasswordView(callable|string $view): void /** * Specify which view should be used as the registration view. * - * Boot-only. The response binding persists in the container for the worker lifetime and affects every subsequent request. + * The response binding persists in the container for the worker lifetime and affects every subsequent request. */ + #[BootOnly] public static function registerView(callable|string $view): void { self::container()->singleton(RegisterViewResponse::class, static fn (): SimpleViewResponse => new SimpleViewResponse($view)); @@ -181,8 +189,9 @@ public static function registerView(callable|string $view): void /** * Specify which view should be used as the email verification prompt. * - * Boot-only. The response binding persists in the container for the worker lifetime and affects every subsequent request. + * The response binding persists in the container for the worker lifetime and affects every subsequent request. */ + #[BootOnly] public static function verifyEmailView(callable|string $view): void { self::container()->singleton(VerifyEmailViewResponse::class, static fn (): SimpleViewResponse => new SimpleViewResponse($view)); @@ -191,8 +200,9 @@ public static function verifyEmailView(callable|string $view): void /** * Specify which view should be used as the password confirmation prompt. * - * Boot-only. The response binding persists in the container for the worker lifetime and affects every subsequent request. + * The response binding persists in the container for the worker lifetime and affects every subsequent request. */ + #[BootOnly] public static function confirmPasswordView(callable|string $view): void { self::container()->singleton(ConfirmPasswordViewResponse::class, static fn (): SimpleViewResponse => new SimpleViewResponse($view)); @@ -201,8 +211,9 @@ public static function confirmPasswordView(callable|string $view): void /** * Specify which view should be used as the request password reset link view. * - * Boot-only. The response binding persists in the container for the worker lifetime and affects every subsequent request. + * The response binding persists in the container for the worker lifetime and affects every subsequent request. */ + #[BootOnly] public static function requestPasswordResetLinkView(callable|string $view): void { self::container()->singleton(RequestPasswordResetLinkViewResponse::class, static fn (): SimpleViewResponse => new SimpleViewResponse($view)); @@ -211,8 +222,9 @@ public static function requestPasswordResetLinkView(callable|string $view): void /** * Register a callback that is responsible for building the authentication pipeline array. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent login request. + * The callback persists in static state for the worker lifetime and affects every subsequent login request. */ + #[BootOnly] public static function loginThrough(callable $callback): void { static::authenticateThrough($callback); @@ -221,8 +233,9 @@ public static function loginThrough(callable $callback): void /** * Register a callback that is responsible for building the authentication pipeline array. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent login request. + * The callback persists in static state for the worker lifetime and affects every subsequent login request. */ + #[BootOnly] public static function authenticateThrough(callable $callback): void { self::$authenticateThroughCallback = Closure::fromCallable($callback); @@ -239,8 +252,9 @@ public static function authenticateThroughCallback(): ?Closure /** * Register a callback that is responsible for validating incoming authentication credentials. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent login request. + * The callback persists in static state for the worker lifetime and affects every subsequent login request. */ + #[BootOnly] public static function authenticateUsing(callable $callback): void { self::$authenticateUsingCallback = Closure::fromCallable($callback); @@ -257,8 +271,9 @@ public static function authenticateUsingCallback(): ?Closure /** * Register a class / callback that should be used to redirect users for two factor authentication. * - * Boot-only. The scoped binding configuration persists for the worker lifetime and affects future requests. + * The scoped binding configuration persists for the worker lifetime and affects future requests. */ + #[BootOnly] public static function redirectUserForTwoFactorAuthenticationUsing(callable|string $callback): void { self::container()->scoped(RedirectsIfTwoFactorAuthenticatable::class, self::bindingConcrete($callback)); @@ -267,8 +282,9 @@ public static function redirectUserForTwoFactorAuthenticationUsing(callable|stri /** * Register a callback that is responsible for confirming existing user passwords as valid. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent password confirmation request. + * The callback persists in static state for the worker lifetime and affects every subsequent password confirmation request. */ + #[BootOnly] public static function confirmPasswordsUsing(callable $callback): void { self::$confirmPasswordsUsingCallback = Closure::fromCallable($callback); @@ -285,8 +301,9 @@ public static function confirmPasswordsUsingCallback(): ?Closure /** * Register a class / callback that should be used to create new users. * - * Boot-only. The binding persists in the container for the worker lifetime and affects every subsequent registration request. + * The binding persists in the container for the worker lifetime and affects every subsequent registration request. */ + #[BootOnly] public static function createUsersUsing(callable|string $callback): void { self::container()->singleton(CreatesNewUsers::class, self::bindingConcrete($callback)); @@ -295,8 +312,9 @@ public static function createUsersUsing(callable|string $callback): void /** * Register a class / callback that should be used to update user profile information. * - * Boot-only. The binding persists in the container for the worker lifetime and affects every subsequent profile update request. + * The binding persists in the container for the worker lifetime and affects every subsequent profile update request. */ + #[BootOnly] public static function updateUserProfileInformationUsing(callable|string $callback): void { self::container()->singleton(UpdatesUserProfileInformation::class, self::bindingConcrete($callback)); @@ -305,8 +323,9 @@ public static function updateUserProfileInformationUsing(callable|string $callba /** * Register a class / callback that should be used to update user passwords. * - * Boot-only. The binding persists in the container for the worker lifetime and affects every subsequent password update request. + * The binding persists in the container for the worker lifetime and affects every subsequent password update request. */ + #[BootOnly] public static function updateUserPasswordsUsing(callable|string $callback): void { self::container()->singleton(UpdatesUserPasswords::class, self::bindingConcrete($callback)); @@ -315,8 +334,9 @@ public static function updateUserPasswordsUsing(callable|string $callback): void /** * Register a class / callback that should be used to reset user passwords. * - * Boot-only. The binding persists in the container for the worker lifetime and affects every subsequent password reset request. + * The binding persists in the container for the worker lifetime and affects every subsequent password reset request. */ + #[BootOnly] public static function resetUserPasswordsUsing(callable|string $callback): void { self::container()->singleton(ResetsUserPasswords::class, self::bindingConcrete($callback)); @@ -325,8 +345,9 @@ public static function resetUserPasswordsUsing(callable|string $callback): void /** * Register a callback that should be used to generate recovery codes. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent recovery code generation. + * The callback persists in static state for the worker lifetime and affects every subsequent recovery code generation. */ + #[BootOnly] public static function generateRecoveryCodesUsing(callable $callback): void { self::$recoveryCodeGenerator = Closure::fromCallable($callback); @@ -351,8 +372,9 @@ public static function confirmsTwoFactorAuthentication(): bool /** * Set the encrypter instance that will be used to encrypt attributes. * - * Boot-only. The encrypter persists in static state for the worker lifetime and affects every subsequent encrypted Fortify attribute. + * The encrypter persists in static state for the worker lifetime and affects every subsequent encrypted Fortify attribute. */ + #[BootOnly] public static function encryptUsing(?EncrypterContract $encrypter): static { self::$encrypter = $encrypter; @@ -405,8 +427,9 @@ public static function shouldRegisterRoutes(): bool /** * Configure Fortify to not register its routes. * - * Boot-only. The route registration flag persists in static state for the worker lifetime and affects route bootstrapping. + * The route registration flag persists in static state for the worker lifetime and affects route bootstrapping. */ + #[BootOnly] public static function ignoreRoutes(): static { self::$registersRoutes = false; diff --git a/src/foundation/composer.json b/src/foundation/composer.json index 6c178aa8ec..d3cda122de 100644 --- a/src/foundation/composer.json +++ b/src/foundation/composer.json @@ -24,25 +24,11 @@ ], "require": { "php": "^8.4", - "composer-runtime-api": "^2.2", "ext-filter": "*", "ext-posix": "*", + "composer-runtime-api": "^2.2", "brick/math": "^1.0", - "laravel/serializable-closure": "^2.0.11", - "league/flysystem": "^3.25.1", - "league/uri": "^7.5.1", - "monolog/monolog": "^3.10", - "nesbot/carbon": "^3.13.1", - "psr/clock": "^1.0", - "psr/log": "^3.0", - "symfony/console": "^8.1.10", - "symfony/error-handler": "^8.1", - "symfony/finder": "^8.1", - "symfony/http-foundation": "^8.1.11", - "symfony/http-kernel": "^8.1.11", - "symfony/process": "^8.1", - "symfony/var-dumper": "^8.1", - "vlucas/phpdotenv": "^5.6.1", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/bus": "^0.4", @@ -82,7 +68,22 @@ "hypervel/testing": "^0.4", "hypervel/validation": "^0.4", "hypervel/view": "^0.4", - "hypervel/websocket-server": "^0.4" + "hypervel/websocket-server": "^0.4", + "laravel/serializable-closure": "^2.0.11", + "league/flysystem": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.10", + "nesbot/carbon": "^3.13.1", + "psr/clock": "^1.0", + "psr/log": "^3.0", + "symfony/console": "^8.1.10", + "symfony/error-handler": "^8.1", + "symfony/finder": "^8.1", + "symfony/http-foundation": "^8.1.11", + "symfony/http-kernel": "^8.1.11", + "symfony/process": "^8.1", + "symfony/var-dumper": "^8.1", + "vlucas/phpdotenv": "^5.6.1" }, "suggest": { "algolia/algoliasearch-client-php": "Required to use the InteractsWithAlgolia trait (^4.49.0).", diff --git a/src/foundation/src/Application.php b/src/foundation/src/Application.php index e1be94cdd0..ce82a72ee4 100644 --- a/src/foundation/src/Application.php +++ b/src/foundation/src/Application.php @@ -26,6 +26,8 @@ use Hypervel\Log\LogServiceProvider; use Hypervel\Routing\RoutingServiceProvider; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Env; use Hypervel\Support\ServiceProvider; @@ -223,7 +225,11 @@ protected function checkEnvironment(): void /** * Configure and create a new application builder instance. + * + * The builder's provider list persists in a static property used when the + * application registers its providers. */ + #[BootOnly] public static function configure(?string $basePath = null): ApplicationBuilder { $basePath = match (true) { @@ -380,11 +386,12 @@ public function hasBeenBootstrapped(): bool /** * Set the base path for the application. * - * Boot-only. Rebinds path instances on the singleton container; runtime use - * races across coroutines and breaks every concurrent path resolution. + * Rebinds path instances on the singleton container; runtime use races + * across coroutines and breaks every concurrent path resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function setBasePath(string $basePath): static { $this->basePath = rtrim($basePath, '\/'); @@ -396,7 +403,11 @@ public function setBasePath(string $basePath): static /** * Bind all of the application paths in the container. + * + * Rebinds path instances on the singleton container; runtime use races + * across coroutines and breaks every concurrent path resolution. */ + #[BootOnly(whenShared: true)] protected function bindPathsInContainer(): void { $this->instance('path', $this->path()); @@ -427,11 +438,12 @@ public function path(string $path = ''): string /** * Set the application directory. * - * Boot-only. The configured path and its container binding are shared by - * every request in the worker. + * The configured path and its container binding are shared by every + * request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useAppPath(string $path): static { $this->appPath = $path; @@ -460,11 +472,12 @@ public function bootstrapPath(string $path = ''): string /** * Set the bootstrap file directory. * - * Boot-only. The configured path and its container binding are shared by - * every request in the worker. + * The configured path and its container binding are shared by every + * request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useBootstrapPath(string $path): static { $this->bootstrapPath = $path; @@ -493,11 +506,12 @@ public function configPath(string $path = ''): string /** * Set the configuration directory. * - * Boot-only. The configured path and its container binding are shared by - * every request in the worker. + * The configured path and its container binding are shared by every + * request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useConfigPath(string $path): static { $this->configPath = $path; @@ -518,11 +532,12 @@ public function databasePath(string $path = ''): string /** * Set the database directory. * - * Boot-only. The configured path and its container binding are shared by - * every request in the worker. + * The configured path and its container binding are shared by every + * request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useDatabasePath(string $path): static { $this->databasePath = $path; @@ -543,11 +558,12 @@ public function langPath(string $path = ''): string /** * Set the language file directory. * - * Boot-only. The configured path and its container binding are shared by - * every request in the worker. + * The configured path and its container binding are shared by every + * request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useLangPath(string $path): static { $this->langPath = $path; @@ -568,11 +584,12 @@ public function publicPath(string $path = ''): string /** * Set the public / web directory. * - * Boot-only. The configured path and its container binding are shared by - * every request in the worker. + * The configured path and its container binding are shared by every + * request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function usePublicPath(string $path): static { $this->publicPath = $path; @@ -626,11 +643,12 @@ public function storagePath(string $path = ''): string /** * Set the storage directory. * - * Boot-only. The configured path and its container binding are shared by - * every request in the worker. + * The configured path and its container binding are shared by every + * request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useStoragePath(string $path): static { $this->storagePath = $path; @@ -651,10 +669,11 @@ public function environmentPath(): ?string /** * Set the directory for the environment file. * - * Boot-only. The configured path is shared by every request in the worker. + * The configured path is shared by every request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useEnvironmentPath(string $path): static { $this->environmentPath = $path; @@ -665,11 +684,11 @@ public function useEnvironmentPath(string $path): static /** * Set the environment file to be loaded during bootstrapping. * - * Boot-only. The configured filename is shared by every request in the - * worker. + * The configured filename is shared by every request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function loadEnvironmentFrom(string $file): static { $this->environmentFile = $file; @@ -781,11 +800,12 @@ protected function normalizeCachePath(string $key, string $default): string /** * Add new prefix to list of absolute path prefixes. * - * Boot-only. The prefix persists on the application singleton and affects - * cache-path resolution for every request in the worker. + * The prefix persists on the application singleton and affects cache-path + * resolution for every request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function addAbsoluteCachePathPrefix(string $prefix): static { $this->absoluteCachePathPrefixes[] = $prefix; @@ -835,7 +855,10 @@ public function isProduction(): bool /** * Detect the application's current environment. + * + * The detected environment is shared by every request in the worker. */ + #[BootOnly(whenShared: true)] public function detectEnvironment(Closure $callback): string { // Server commands use HTTP semantics during boot but still receive startup arguments. @@ -880,10 +903,10 @@ public function runningConsoleCommand(string|array ...$commands): bool /** * Set whether the application is running in the console. * - * Boot-only. The flag persists on the singleton Application for the worker - * lifetime and is read by every runningInConsole() check across all - * coroutines. + * The flag persists on the singleton Application for the worker lifetime + * and is read by every runningInConsole() check across all coroutines. */ + #[BootOnly(whenShared: true)] public function setRunningInConsole(bool $runningInConsole): void { $this->isRunningInConsole = $runningInConsole; @@ -951,11 +974,12 @@ public function shouldMergeFrameworkConfiguration(): bool /** * Indicate that the framework's base configuration should not be merged. * - * Boot-only. The setting changes configuration bootstrap behavior for the - * entire worker. + * The setting changes configuration bootstrap behavior for the entire + * worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function dontMergeFrameworkConfiguration(): static { $this->mergeFrameworkConfiguration = false; @@ -975,9 +999,10 @@ public function shouldSkipMiddleware(): bool /** * Register a new registered listener. * - * Boot-only. The callback persists in the Application's registeredCallbacks - * array and fires once per service provider registration during boot. + * The callback persists in the Application's registeredCallbacks array and + * fires once per service provider registration during boot. */ + #[BootOnly(whenShared: true)] public function registered(callable $callback): void { $this->registeredCallbacks[] = $callback; @@ -990,7 +1015,11 @@ public function registered(callable $callback): void * 1. Framework providers (Hypervel\* from app.providers) — always first * 2. Discovered providers (from composer packages) — sorted by priority * 3. Application providers (non-Hypervel\* from app.providers) — always last + * + * The providers' bindings persist on the application for every subsequent + * request in the worker. */ + #[BootOnly(whenShared: true)] public function registerConfiguredProviders(): void { $providers = (new Collection($this->make('config')->array('app.providers'))) @@ -1051,7 +1080,11 @@ protected static function sortByPriority(array $providers): array /** * Register a service provider with the application. + * + * The provider's bindings persist on the application for every subsequent + * request in the worker. */ + #[BootOnly(whenShared: true)] public function register(ServiceProvider|string $provider, bool $force = false): ServiceProvider { if (($registered = $this->getProvider($provider)) && ! $force) { @@ -1200,10 +1233,11 @@ protected function bootProvider(ServiceProvider $provider): void /** * Register a new boot listener. * - * Boot-only. The callback fires once during application boot; runtime use - * after boot stores the closure on the singleton Application with no - * future boot to listen for. + * The callback fires once during application boot; runtime use after boot + * stores the closure on the singleton Application with no future boot to + * listen for. */ + #[BootOnly(whenShared: true)] public function booting(callable $callback): void { $this->bootingCallbacks[] = $callback; @@ -1212,10 +1246,11 @@ public function booting(callable $callback): void /** * Register a new "booted" listener. * - * Boot-only. If the app has already booted, the callback fires immediately - * and is never invoked again, but remains stored on the singleton - * Application for the worker lifetime. + * If the app has already booted, the callback fires immediately and is + * never invoked again, but remains stored on the singleton Application for + * the worker lifetime. */ + #[BootOnly(whenShared: true)] public function booted(callable $callback): void { $this->bootedCallbacks[] = $callback; @@ -1228,13 +1263,14 @@ public function booted(callable $callback): void /** * Register a terminating callback with the application. * - * Boot-only. Callbacks persist for the worker's lifetime and fire on every - * subsequent request — per-request use leaks the closure and anything it - * captures across every request the worker handles. Use defer() for - * per-request "after the response" cleanup. + * Callbacks persist for the worker's lifetime and fire on every subsequent + * request — per-request use leaks the closure and anything it captures + * across every request the worker handles. Use defer() for per-request + * "after the response" cleanup. * * @return $this */ + #[BootOnly(whenShared: true)] public function terminating(callable|string $callback): static { $this->terminatingCallbacks[] = $callback; @@ -1369,9 +1405,10 @@ public function setLocale(string $locale): void /** * Set the current application fallback locale. * - * Boot-only. The fallback is stored on the shared translator and affects - * every request in the worker. + * The fallback is stored on the shared translator and affects every + * request in the worker. */ + #[BootOnly(whenShared: true)] public function setFallbackLocale(string $fallbackLocale): void { $this->make(TranslatorContract::class)->setFallback($fallbackLocale); @@ -1555,7 +1592,11 @@ protected function registerCoreContainerAliases(): void /** * Flush the container of all bindings and resolved instances. + * + * Clears the worker-lifetime application registry; runtime use breaks + * every concurrent resolution in the worker. */ + #[TestsOnly] public function flush(): void { parent::flush(); diff --git a/src/foundation/src/Bootstrap/HandleExceptions.php b/src/foundation/src/Bootstrap/HandleExceptions.php index 111fab5bd1..1e0638c57b 100644 --- a/src/foundation/src/Bootstrap/HandleExceptions.php +++ b/src/foundation/src/Bootstrap/HandleExceptions.php @@ -12,6 +12,7 @@ use Hypervel\Coroutine\Coroutine; use Hypervel\Foundation\Configuration\ConfigMutationTracker; use Hypervel\Log\LogManager; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Env; use Monolog\Handler\NullHandler; use PHPUnit\Framework\TestCase; @@ -351,11 +352,12 @@ protected function getExceptionHandler(): ExceptionHandler /** * Release the handlers installed for an application that is being discarded. * - * Boot or tests only. Restores the handlers, display_errors setting and - * application that were active before the application was bootstrapped, so - * later errors are reported without its flushed container. Handlers and - * settings changed by other code stay in place. + * Restores the handlers, display_errors setting and application that were + * active before the application was bootstrapped, so later errors are + * reported without its flushed container. Handlers and settings changed by + * other code stay in place. */ + #[BootOrTestsOnly] public static function release(Application $app): void { $owner = static::$owner; diff --git a/src/foundation/src/Bootstrap/LoadConfiguration.php b/src/foundation/src/Bootstrap/LoadConfiguration.php index f48e815d14..a731544930 100644 --- a/src/foundation/src/Bootstrap/LoadConfiguration.php +++ b/src/foundation/src/Bootstrap/LoadConfiguration.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Config\Repository as RepositoryContract; use Hypervel\Contracts\Foundation\Application; use Hypervel\Foundation\Configuration\ConfigMutationTracker; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use RuntimeException; use SplFileInfo; @@ -261,12 +262,13 @@ protected function getBaseConfiguration(): array /** * Set a callback to return the permanent, static configuration values. * - * Boot-only. The callback persists in a static property used during the + * The callback persists in a static property used during the * LoadConfiguration bootstrapper; runtime use has no effect on already- * loaded config. * * @param null|(Closure(Application): array) $alwaysUseConfig */ + #[BootOnly] public static function alwaysUse(?Closure $alwaysUseConfig): void { static::$alwaysUseConfig = $alwaysUseConfig; diff --git a/src/foundation/src/Bootstrap/RegisterProviders.php b/src/foundation/src/Bootstrap/RegisterProviders.php index 7d9dfbce34..f24ea9eb7b 100644 --- a/src/foundation/src/Bootstrap/RegisterProviders.php +++ b/src/foundation/src/Bootstrap/RegisterProviders.php @@ -7,6 +7,7 @@ use Hypervel\Contracts\Foundation\Application as ApplicationContract; use Hypervel\Di\Aop\AspectCollector; use Hypervel\Di\ClassMap\ClassMapManager; +use Hypervel\Support\Attributes\BootOrTestsOnly; class RegisterProviders { @@ -68,12 +69,13 @@ protected function mergeAdditionalProviders(ApplicationContract $app): void /** * Merge the given providers into the provider configuration before registration. * - * Boot or tests only. The provider list persists in a static property used - * during the RegisterProviders bootstrapper; runtime use has no effect on + * The provider list persists in a static property used during the + * RegisterProviders bootstrapper; runtime use has no effect on * already-registered providers. * * @param array $providers */ + #[BootOrTestsOnly] public static function merge(array $providers, ?string $bootstrapProviderPath = null): void { static::$bootstrapProviderPath = $bootstrapProviderPath; diff --git a/src/foundation/src/Concerns/ResolvesDumpSource.php b/src/foundation/src/Concerns/ResolvesDumpSource.php index 4338ad17d1..9ed8b12185 100644 --- a/src/foundation/src/Concerns/ResolvesDumpSource.php +++ b/src/foundation/src/Concerns/ResolvesDumpSource.php @@ -4,6 +4,8 @@ namespace Hypervel\Foundation\Concerns; +use Hypervel\Support\Attributes\BootOnly; + /** * Resolve dump sources for consumers that provide $basePath and $compiledViewPath properties. */ @@ -133,11 +135,12 @@ protected function getOriginalFileForCompiledView(string $file): string /** * Set the resolver that resolves the source of the dump call. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every dump source resolution across all coroutines. + * The resolver persists in a static property for the worker lifetime and + * runs on every dump source resolution across all coroutines. * * @param null|(callable(): (null|array{0: string, 1: string, 2: null|int})) $callable */ + #[BootOnly] public static function resolveDumpSourceUsing(?callable $callable): void { static::$dumpSourceResolver = $callable; @@ -146,9 +149,10 @@ public static function resolveDumpSourceUsing(?callable $callable): void /** * Don't include the location / file of the dump in dumps. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every dump source resolution across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every dump source resolution across all coroutines. */ + #[BootOnly] public static function dontIncludeSource(): void { static::$dumpSourceResolver = false; diff --git a/src/foundation/src/Configuration/ApplicationBuilder.php b/src/foundation/src/Configuration/ApplicationBuilder.php index f7bb0873cb..78c0626c02 100644 --- a/src/foundation/src/Configuration/ApplicationBuilder.php +++ b/src/foundation/src/Configuration/ApplicationBuilder.php @@ -18,6 +18,8 @@ use Hypervel\Foundation\Support\Providers\EventServiceProvider as AppEventServiceProvider; use Hypervel\Foundation\Support\Providers\RouteServiceProvider as AppRouteServiceProvider; use Hypervel\Http\Middleware\PrefersJsonResponses; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Broadcast; use Hypervel\Support\Facades\Route; @@ -44,12 +46,13 @@ public function __construct(protected Application $app) /** * Register the standard kernel classes for the application. * - * Boot-only. Re-registering the kernels replaces shared bindings and can - * discard middleware or console configuration used by subsequent requests - * and commands. + * Re-registering the kernels replaces shared bindings and can discard + * middleware or console configuration used by subsequent requests and + * commands. * * @return $this */ + #[BootOnly] public function withKernels(): static { // Explicitly sharing the concrete kernels keeps one instance when they are @@ -74,8 +77,12 @@ public function withKernels(): static /** * Register additional service providers. * + * The provider list persists in a static property used when the + * application registers its providers. + * * @return $this */ + #[BootOrTestsOnly] public function withProviders(array $providers = [], bool $withBootstrapProviders = true): static { RegisterProviders::merge( @@ -91,9 +98,13 @@ public function withProviders(array $providers = [], bool $withBootstrapProvider /** * Register the core event service provider for the application. * + * The discovery settings persist in static properties, and the provider + * registers on the application for the worker lifetime. + * * @param bool|iterable $discover * @return $this */ + #[BootOnly] public function withEvents(iterable|bool $discover = true): static { if (is_iterable($discover)) { @@ -118,8 +129,12 @@ public function withEvents(iterable|bool $discover = true): static /** * Register the broadcasting services for the application. * + * The broadcast routes and channels register on the application for the + * worker lifetime. + * * @return $this */ + #[BootOnly] public function withBroadcasting(string $channels, array $attributes = []): static { $this->app->booted(function () use ($channels, $attributes) { @@ -136,8 +151,11 @@ public function withBroadcasting(string $channels, array $attributes = []): stat /** * Register the routing services for the application. * + * The routes and their loading callback persist for the worker lifetime. + * * @return $this */ + #[BootOnly] public function withRouting( ?Closure $using = null, array|string|null $web = null, @@ -227,8 +245,12 @@ protected function buildRoutingCallback( /** * Register the global middleware, middleware groups, and middleware aliases for the application. * + * The middleware configuration applies to the shared kernels for the worker + * lifetime. + * * @return $this */ + #[BootOnly] public function withMiddleware(?callable $callback = null): static { $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { @@ -272,8 +294,12 @@ public function withMiddleware(?callable $callback = null): static /** * Register additional Artisan commands with the application. * + * The commands register on the shared console kernel for the worker + * lifetime. + * * @return $this */ + #[BootOnly] public function withCommands(array $commands = []): static { if (empty($commands)) { @@ -297,8 +323,12 @@ public function withCommands(array $commands = []): static /** * Register additional Artisan route paths. * + * The route paths register on the shared console kernel for the worker + * lifetime. + * * @return $this */ + #[BootOnly] protected function withCommandRouting(array $paths): static { $this->app->afterResolving(ConsoleKernel::class, function ($kernel) use ($paths) { @@ -311,8 +341,12 @@ protected function withCommandRouting(array $paths): static /** * Register the scheduled tasks for the application. * + * The callback persists for the worker lifetime and configures the shared + * schedule. + * * @return $this */ + #[BootOnly] public function withSchedule(callable $callback): static { Artisan::starting(function () use ($callback) { @@ -329,12 +363,13 @@ public function withSchedule(callable $callback): static /** * Register and configure the application's exception handler. * - * Boot-only. The exception handler binding and resolution callback persist - * in the application container and affect subsequent requests. + * The exception handler binding and resolution callback persist in the + * application container and affect subsequent requests. * * @param null|(callable(Exceptions): mixed) $using * @return $this */ + #[BootOnly] public function withExceptions(?callable $using = null): static { // Share the concrete handler so callbacks registered on it reach the handler @@ -356,8 +391,11 @@ public function withExceptions(?callable $using = null): static /** * Register an array of container bindings to be bound when the application is booting. * + * The bindings persist on the application for the worker lifetime. + * * @return $this */ + #[BootOnly] public function withBindings(array $bindings): static { return $this->registered(function ($app) use ($bindings) { @@ -370,8 +408,11 @@ public function withBindings(array $bindings): static /** * Register an array of singleton container bindings to be bound when the application is booting. * + * The bindings persist on the application for the worker lifetime. + * * @return $this */ + #[BootOnly] public function withSingletons(array $singletons): static { return $this->registered(function ($app) use ($singletons) { @@ -388,8 +429,11 @@ public function withSingletons(array $singletons): static /** * Register an array of scoped singleton container bindings to be bound when the application is booting. * + * The bindings persist on the application for the worker lifetime. + * * @return $this */ + #[BootOnly] public function withScopedSingletons(array $scopedSingletons): static { return $this->registered(function ($app) use ($scopedSingletons) { @@ -406,8 +450,12 @@ public function withScopedSingletons(array $scopedSingletons): static /** * Globally prefer JSON responses when the incoming "Accept" header is broad. * + * The middleware joins the shared kernel's global stack for the worker + * lifetime. + * * @return $this */ + #[BootOnly] public function prefersJsonResponses(bool $prefer = true): static { if (! $prefer) { @@ -424,8 +472,11 @@ public function prefersJsonResponses(bool $prefer = true): static /** * Register a callback to be invoked when the application's service providers are registered. * + * The callback persists on the application for the worker lifetime. + * * @return $this */ + #[BootOnly] public function registered(callable $callback): static { $this->app->registered($callback); @@ -436,8 +487,11 @@ public function registered(callable $callback): static /** * Register a callback to be invoked when the application is "booting". * + * The callback persists on the application for the worker lifetime. + * * @return $this */ + #[BootOnly] public function booting(callable $callback): static { $this->app->booting($callback); @@ -448,8 +502,11 @@ public function booting(callable $callback): static /** * Register a callback to be invoked when the application is "booted". * + * The callback persists on the application for the worker lifetime. + * * @return $this */ + #[BootOnly] public function booted(callable $callback): static { $this->app->booted($callback); diff --git a/src/foundation/src/Configuration/ConfigMutationTracker.php b/src/foundation/src/Configuration/ConfigMutationTracker.php index df06eec5e0..33f5210282 100644 --- a/src/foundation/src/Configuration/ConfigMutationTracker.php +++ b/src/foundation/src/Configuration/ConfigMutationTracker.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Config\Repository; use Hypervel\Contracts\Config\Repository as RepositoryContract; +use Hypervel\Support\Attributes\BootOnly; /** * Tracks configuration mutations made during application boot for worker replay. @@ -30,9 +31,10 @@ class ConfigMutationTracker /** * Observe configuration mutations made during application boot. * - * Boot-only. Calling this outside application bootstrap replaces - * Foundation's mutation observer and can track the wrong lifecycle. + * Calling this outside application bootstrap replaces Foundation's + * mutation observer and can track the wrong lifecycle. */ + #[BootOnly] public function observe(Repository $config): void { $config->setMutationObserver(function (array $values): void { @@ -45,11 +47,12 @@ public function observe(Repository $config): void /** * Apply and record a configuration operation for worker replay. * - * Boot-only. The operation is retained for worker-start replay and is + * The operation is retained for worker-start replay and is * re-evaluated against the freshly rebuilt configuration repository. * * @param Closure(RepositoryContract): void $mutation */ + #[BootOnly] public function applyAndRecord(RepositoryContract $config, Closure $mutation): void { $wasRecording = $this->recording; @@ -69,9 +72,10 @@ public function applyAndRecord(RepositoryContract $config, Closure $mutation): v /** * Replay the recorded mutations and stop tracking changes in this worker. * - * Boot-only. Calling this outside the before-worker-start boundary + * Calling this outside the before-worker-start boundary * permanently stops recording and can omit later boot mutations. */ + #[BootOnly] public function replay(Repository $config): void { $this->recording = false; diff --git a/src/foundation/src/Configuration/Exceptions.php b/src/foundation/src/Configuration/Exceptions.php index 5c80b32e92..3cf31d95ae 100644 --- a/src/foundation/src/Configuration/Exceptions.php +++ b/src/foundation/src/Configuration/Exceptions.php @@ -10,6 +10,7 @@ use Hypervel\Http\Client\RequestException; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use Psr\Log\LogLevel; use Symfony\Component\HttpFoundation\Response; @@ -27,7 +28,7 @@ public function __construct(public Handler $handler) /** * Register a reportable callback. * - * Boot-only. The callback persists on the shared handler and affects exception + * On the application's worker-shared handler, the callback affects exception * reporting for every subsequent request and job in the worker. */ public function report(callable $using): ReportableHandler @@ -38,7 +39,7 @@ public function report(callable $using): ReportableHandler /** * Register a reportable callback. * - * Boot-only. The callback persists on the shared handler and affects exception + * On the application's worker-shared handler, the callback affects exception * reporting for every subsequent request and job in the worker. */ public function reportable(callable $reportUsing): ReportableHandler @@ -49,7 +50,7 @@ public function reportable(callable $reportUsing): ReportableHandler /** * Register a renderable callback. * - * Boot-only. The callback persists on the shared handler and affects exception + * On the application's worker-shared handler, the callback affects exception * rendering for every subsequent request in the worker. * * @return $this @@ -64,7 +65,7 @@ public function render(callable $using): static /** * Register a renderable callback. * - * Boot-only. The callback persists on the shared handler and affects exception + * On the application's worker-shared handler, the callback affects exception * rendering for every subsequent request in the worker. * * @return $this @@ -79,8 +80,8 @@ public function renderable(callable $renderUsing): static /** * Register a callback to prepare the final, rendered exception response. * - * Boot-only. The callback replaces the shared handler's response callback for - * every subsequent request in the worker. + * On the application's worker-shared handler, the callback replaces the + * response callback for every subsequent request in the worker. * * @return $this */ @@ -94,8 +95,8 @@ public function respond(callable $using): static /** * Register a callback before final exception response customization. * - * Boot-only. The callback is shared by every request in the worker. - * Callback return values are ignored. + * On the application's worker-shared handler, the callback runs for every + * subsequent exception response in the worker. Callback return values are ignored. * * @param callable(Response, Throwable, Request): mixed $callback * @return $this @@ -110,7 +111,7 @@ public function beforeResponding(callable $callback): static /** * Specify the callback that should be used to throttle reportable exceptions. * - * Boot-only. The callback persists on the shared handler and affects exception + * On the application's worker-shared handler, the callback affects exception * reporting for every subsequent request and job in the worker. * * @return $this @@ -125,7 +126,7 @@ public function throttle(callable $throttleUsing): static /** * Register a new exception mapping. * - * Boot-only. The mapping persists on the shared handler and affects exception + * On the application's worker-shared handler, the mapping affects exception * reporting and rendering for every subsequent request and job in the worker. * * @return $this @@ -142,7 +143,7 @@ public function map(Closure|string $from, Closure|string|null $to = null): stati /** * Set the log level for the given exception type. * - * Boot-only. The level persists on the shared handler and affects how later + * On the application's worker-shared handler, the level affects how later * exceptions of that type are logged in the worker. * * @param class-string $type @@ -159,7 +160,7 @@ public function level(string $type, string $level): static /** * Register a closure that should be used to build exception context data. * - * Boot-only. The closure persists on the shared handler and runs for every + * On the application's worker-shared handler, the closure runs for every * subsequently logged exception in the worker. * * @return $this @@ -174,7 +175,7 @@ public function context(Closure $contextCallback): static /** * Indicate that the given exception type should not be reported. * - * Boot-only. The exception types persist on the shared handler and affect + * On the application's worker-shared handler, the exception types affect * exception reporting for every subsequent request and job in the worker. * * @return $this @@ -191,7 +192,7 @@ public function dontReport(array|string $class): static /** * Indicate that the given exception type should stop job retries. * - * Boot-only. The exception types persist on the shared handler and affect + * On the application's worker-shared handler, the exception types affect * every subsequently failed job in the worker. * * @return $this @@ -208,7 +209,7 @@ public function dontRetry(array|string $class): static /** * Register a callback to determine if an exception should not be reported. * - * Boot-only. The callback persists on the shared handler and affects exception + * On the application's worker-shared handler, the callback affects exception * reporting for every subsequent request and job in the worker. * * @param (Closure(Throwable): bool) $dontReportWhen @@ -224,7 +225,7 @@ public function dontReportWhen(Closure $dontReportWhen): static /** * Register a callback to determine if an exception should stop job retries. * - * Boot-only. The callback persists on the shared handler and affects job retry + * On the application's worker-shared handler, the callback affects job retry * decisions for every subsequent job in the worker. * * @template TException of Throwable @@ -242,7 +243,7 @@ public function dontRetryWhen(Closure $dontRetryWhen): static /** * Do not report duplicate exceptions. * - * Boot-only. The setting persists on the shared handler and applies to every + * On the application's worker-shared handler, the setting applies to every * subsequent request and job in the worker. * * @return $this @@ -257,8 +258,8 @@ public function dontReportDuplicates(): static /** * Indicate that the given attributes should never be flashed to the session on validation errors. * - * Boot-only. The attributes persist on the shared handler and are omitted - * from every subsequent validation redirect in the worker. + * On the application's worker-shared handler, the attributes are omitted from + * every subsequent validation redirect in the worker. * * @return $this */ @@ -272,8 +273,8 @@ public function dontFlash(array|string $attributes): static /** * Register the callable that determines if the exception handler response should be JSON. * - * Boot-only. The callable replaces the shared handler's JSON check for every - * subsequent request in the worker. + * On the application's worker-shared handler, the callable replaces the JSON + * check for every subsequent request in the worker. * * @return $this */ @@ -287,7 +288,7 @@ public function shouldRenderJsonWhen(callable $callback): static /** * Indicate that the given exception class should not be ignored. * - * Boot-only. The exception lists persist on the shared handler and affect + * On the application's worker-shared handler, the exception lists affect * exception reporting for every subsequent request and job in the worker. * * @param array>|class-string $class @@ -303,11 +304,12 @@ public function stopIgnoring(array|string $class): static /** * Set the truncation length for request exception messages. * - * Boot-only. The global default persists for the worker lifetime across all + * The global default persists for the worker lifetime across all * coroutines; per-request truncation settings take precedence. * * @return $this */ + #[BootOnly] public function truncateRequestExceptionsAt(int $length): static { RequestException::truncateAt($length); @@ -318,11 +320,12 @@ public function truncateRequestExceptionsAt(int $length): static /** * Disable truncation of request exception messages. * - * Boot-only. The global default persists for the worker lifetime across all + * The global default persists for the worker lifetime across all * coroutines; per-request truncation settings take precedence. * * @return $this */ + #[BootOnly] public function dontTruncateRequestExceptions(): static { RequestException::dontTruncate(); diff --git a/src/foundation/src/Configuration/Middleware.php b/src/foundation/src/Configuration/Middleware.php index 334c4a31c7..6b781379b3 100644 --- a/src/foundation/src/Configuration/Middleware.php +++ b/src/foundation/src/Configuration/Middleware.php @@ -17,6 +17,7 @@ use Hypervel\Http\Request; use Hypervel\Routing\Middleware\ValidateSignature; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; class Middleware @@ -483,12 +484,13 @@ public function getMiddlewareGroups(): array /** * Configure where guests are redirected by the "auth" middleware. * - * Boot-only. The callback persists in authentication middleware and - * exception static properties for the worker lifetime and affects every + * The callback persists in authentication middleware and exception + * static properties for the worker lifetime and affects every * subsequent unauthenticated or session-mismatch request. * * @return $this */ + #[BootOnly] public function redirectGuestsTo(callable|string|null $redirect): static { AuthenticationRedirects::redirectGuestsTo($redirect); @@ -499,12 +501,13 @@ public function redirectGuestsTo(callable|string|null $redirect): static /** * Configure where users are redirected by the "guest" middleware. * - * Boot-only. The callback persists in the guest middleware static property - * for the worker lifetime and affects every subsequent already-authenticated + * The callback persists in the guest middleware static property for the + * worker lifetime and affects every subsequent already-authenticated * guest-route request. * * @return $this */ + #[BootOnly] public function redirectUsersTo(callable|string $redirect): static { return $this->redirectTo(users: $redirect); @@ -513,12 +516,13 @@ public function redirectUsersTo(callable|string $redirect): static /** * Configure where users are redirected by the authentication and guest middleware. * - * Boot-only. The callbacks persist in authentication middleware and - * exception static properties for the worker lifetime and affect every + * The callbacks persist in authentication middleware and exception + * static properties for the worker lifetime and affect every * subsequent matching request. * * @return $this */ + #[BootOnly] public function redirectTo(callable|string|null $guests = null, callable|string|null $users = null): static { AuthenticationRedirects::redirectTo(guests: $guests, users: $users); @@ -529,10 +533,14 @@ public function redirectTo(callable|string|null $guests = null, callable|string| /** * Configure the cookie encryption middleware. * + * The cookie lists persist in static properties for the worker lifetime + * and apply to every subsequent request. + * * @param array $except * @param array $only * @return $this */ + #[BootOnly] public function encryptCookies(array $except = [], array $only = []): static { if ($except !== []) { @@ -549,8 +557,12 @@ public function encryptCookies(array $except = [], array $only = []): static /** * Configure the request forgery prevention middleware. * + * The settings persist in static properties for the worker lifetime and + * apply to every subsequent request. + * * @return $this */ + #[BootOnly] public function preventRequestForgery(array $except = [], bool $originOnly = false, bool $allowSameSite = false): static { if (! empty($except)) { @@ -569,9 +581,13 @@ public function preventRequestForgery(array $except = [], bool $originOnly = fal /** * Configure the URL signature validation middleware. * + * The parameter list persists in a static property for the worker lifetime + * and applies to every subsequent request. + * * @param array $except * @return $this */ + #[BootOnly] public function validateSignatures(array $except = []): static { ValidateSignature::except($except); @@ -582,9 +598,13 @@ public function validateSignatures(array $except = []): static /** * Configure the empty string conversion middleware. * + * Skip callbacks persist in a static property for the worker lifetime and + * run on every subsequent request. + * * @param array $except * @return $this */ + #[BootOnly] public function convertEmptyStringsToNull(array $except = []): static { (new Collection($except))->each(fn (Closure $callback) => ConvertEmptyStringsToNull::skipWhen($callback)); @@ -595,9 +615,13 @@ public function convertEmptyStringsToNull(array $except = []): static /** * Configure the string trimming middleware. * + * The exceptions persist in static properties for the worker lifetime and + * apply to every subsequent request. + * * @param array $except * @return $this */ + #[BootOnly] public function trimStrings(array $except = []): static { [$skipWhen, $except] = (new Collection($except))->partition(fn ($value) => $value instanceof Closure); @@ -612,9 +636,13 @@ public function trimStrings(array $except = []): static /** * Indicate that the trusted host middleware should be enabled. * + * Given hosts persist in static properties for the worker lifetime and + * apply to every subsequent request. + * * @param null|array|(callable(): array) $at * @return $this */ + #[BootOnly] public function trustHosts(array|callable|null $at = null, bool $subdomains = true): static { $this->trustHosts = true; @@ -629,9 +657,13 @@ public function trustHosts(array|callable|null $at = null, bool $subdomains = tr /** * Configure the trusted proxies for the application. * + * Given proxies and headers persist in static properties for the worker + * lifetime and apply to every subsequent request. + * * @param null|array|string $at * @return $this */ + #[BootOnly] public function trustProxies(array|string|null $at = null, ?int $headers = null): static { if (! is_null($at)) { @@ -648,9 +680,13 @@ public function trustProxies(array|string|null $at = null, ?int $headers = null) /** * Configure the middleware that prevents requests during maintenance mode. * + * The URI list persists in a static property for the worker lifetime and + * applies to every subsequent request. + * * @param array $except * @return $this */ + #[BootOnly] public function preventRequestsDuringMaintenance(array $except = []): static { PreventRequestsDuringMaintenance::except($except); diff --git a/src/foundation/src/Console/AboutCommand.php b/src/foundation/src/Console/AboutCommand.php index 2613c1613e..e811410a84 100644 --- a/src/foundation/src/Console/AboutCommand.php +++ b/src/foundation/src/Console/AboutCommand.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Console\Command; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Composer; use Hypervel\Support\Str; @@ -263,9 +264,10 @@ protected function hasPhpFiles(string $path, string $extension = 'php'): bool /** * Add additional data to the output of the "about" command. * - * Boot-only. The data resolver persists in a static property for the worker - * lifetime and affects every subsequent about command run. + * The data resolver persists in a static property for the worker lifetime + * and affects every subsequent about command run. */ + #[BootOnly] public static function add(string $section, callable|string|array $data, ?string $value = null): void { static::$customDataResolvers[] = fn () => static::addToSection($section, $data, $value); diff --git a/src/foundation/src/Console/ChannelListCommand.php b/src/foundation/src/Console/ChannelListCommand.php index 0de80ab1c1..36235a5ec9 100644 --- a/src/foundation/src/Console/ChannelListCommand.php +++ b/src/foundation/src/Console/ChannelListCommand.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Console\Command; use Hypervel\Contracts\Broadcasting\Broadcaster; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Terminal; @@ -122,9 +123,10 @@ public static function getTerminalWidth(): int /** * Set a callback that should be used when resolving the terminal width. * - * Tests only. The resolver persists in a static property for the worker - * lifetime and affects every subsequent channel:list render. + * The resolver persists in a static property for the worker lifetime + * and affects every subsequent channel:list render. */ + #[TestsOnly] public static function resolveTerminalWidthUsing(?Closure $resolver): void { static::$terminalWidthResolver = $resolver; diff --git a/src/foundation/src/Console/CliDumper.php b/src/foundation/src/Console/CliDumper.php index 4fdd284008..5844c683d4 100644 --- a/src/foundation/src/Console/CliDumper.php +++ b/src/foundation/src/Console/CliDumper.php @@ -6,6 +6,7 @@ use Hypervel\Context\CoroutineContext; use Hypervel\Foundation\Concerns\ResolvesDumpSource; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarDumper\Caster\ReflectionCaster; @@ -47,9 +48,9 @@ public function __construct( /** * Create a new CLI dumper instance and register it as the default dumper. * - * Boot-only. Registers a process-wide VarDumper handler for the worker - * lifetime. + * Registers a process-wide VarDumper handler for the worker lifetime. */ + #[BootOnly] public static function register(string $basePath, string $compiledViewPath): void { $cloner = tap(new VarCloner)->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); diff --git a/src/foundation/src/Console/EventListCommand.php b/src/foundation/src/Console/EventListCommand.php index 1070cb43a3..306deb3846 100644 --- a/src/foundation/src/Console/EventListCommand.php +++ b/src/foundation/src/Console/EventListCommand.php @@ -10,6 +10,7 @@ use Hypervel\Contracts\Container\BindingResolutionException; use Hypervel\Contracts\Queue\ShouldQueue; use Hypervel\Events\Dispatcher; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use ReflectionFunction; @@ -235,9 +236,10 @@ public function getEventsDispatcher() /** * Set a callback that should be used when resolving the events dispatcher. * - * Tests only. The resolver persists in a static property for the worker - * lifetime and affects every subsequent event:list run. + * The resolver persists in a static property for the worker lifetime + * and affects every subsequent event:list run. */ + #[TestsOnly] public static function resolveEventsUsing(?Closure $resolver): void { static::$eventsResolver = $resolver; diff --git a/src/foundation/src/Console/Kernel.php b/src/foundation/src/Console/Kernel.php index 57a445dc26..9f30be4ff6 100644 --- a/src/foundation/src/Console/Kernel.php +++ b/src/foundation/src/Console/Kernel.php @@ -22,6 +22,7 @@ use Hypervel\Foundation\Bus\PendingDispatch; use Hypervel\Foundation\Events\Terminating; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\InteractsWithTime; @@ -355,10 +356,11 @@ protected function commands(): void /** * Register a Closure based command with the application. * - * Boot or tests only. Commands remain registered for the kernel's lifetime. + * Commands remain registered for the kernel's lifetime. * * @param-closure-this ClosureCommand $callback */ + #[BootOrTestsOnly(whenShared: true)] public function command(string $signature, Closure $callback): ClosureCommand { $command = new ClosureCommand($signature, $callback); diff --git a/src/foundation/src/Console/RouteListCommand.php b/src/foundation/src/Console/RouteListCommand.php index 9cec3d0168..8da306a25e 100644 --- a/src/foundation/src/Console/RouteListCommand.php +++ b/src/foundation/src/Console/RouteListCommand.php @@ -11,6 +11,7 @@ use Hypervel\Routing\Router; use Hypervel\Routing\ViewController; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Hypervel\Support\Stringable; @@ -493,9 +494,10 @@ public static function getTerminalWidth(): int /** * Set a callback that should be used when resolving the terminal width. * - * Tests only. The resolver persists in a static property for the worker - * lifetime and affects every subsequent route:list render. + * The resolver persists in a static property for the worker lifetime + * and affects every subsequent route:list render. */ + #[TestsOnly] public static function resolveTerminalWidthUsing(?Closure $resolver): void { static::$terminalWidthResolver = $resolver; diff --git a/src/foundation/src/Console/VendorPublishCommand.php b/src/foundation/src/Console/VendorPublishCommand.php index 81ee3228a4..710b95c4fa 100644 --- a/src/foundation/src/Console/VendorPublishCommand.php +++ b/src/foundation/src/Console/VendorPublishCommand.php @@ -10,6 +10,7 @@ use Hypervel\Filesystem\Filesystem; use Hypervel\Foundation\Events\VendorTagPublished; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\ServiceProvider; use Hypervel\Support\Str; @@ -458,9 +459,10 @@ protected function status(string $from, string $to, string $type): void /** * Instruct the command to not update the dates on migrations when publishing. * - * Boot-only. The publishing mode persists in a static property for the - * worker lifetime and affects every subsequent vendor:publish run. + * The publishing mode persists in a static property for the worker + * lifetime and affects every subsequent vendor:publish run. */ + #[BootOnly] public static function dontUpdateMigrationDates(): void { static::$updateMigrationDates = false; diff --git a/src/foundation/src/DevCommands.php b/src/foundation/src/DevCommands.php index 05ba31d5a2..073740211c 100644 --- a/src/foundation/src/DevCommands.php +++ b/src/foundation/src/DevCommands.php @@ -6,6 +6,7 @@ use Composer\InstalledVersions; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\File; use Hypervel\Support\NodePackageManager; use ReflectionClass; @@ -96,9 +97,10 @@ class DevCommands /** * Register the default development commands. * - * Boot-only. The commands persist in static properties for the worker lifetime - * and affect every subsequent development command invocation. + * The commands persist in static properties for the worker lifetime and affect + * every subsequent development command invocation. */ + #[BootOnly] public static function registerDefaults(): void { if (! app()->runningInConsole()) { @@ -117,9 +119,10 @@ public static function registerDefaults(): void /** * Register a development command. * - * Boot-only. The command persists in a static property for the worker lifetime - * and affects every subsequent development command invocation. + * The command persists in a static property for the worker lifetime and + * affects every subsequent development command invocation. */ + #[BootOnly] public static function register(string $command, ?string $name = null): DevCommand { if (! app()->runningInConsole()) { @@ -144,9 +147,10 @@ public static function register(string $command, ?string $name = null): DevComma /** * Register an Artisan command, automatically prefixing it with "php artisan". * - * Boot-only. The command persists in a static property for the worker lifetime - * and affects every subsequent development command invocation. + * The command persists in a static property for the worker lifetime and + * affects every subsequent development command invocation. */ + #[BootOnly] public static function artisan(string $command, ?string $name = null): DevCommand { return self::register("php artisan {$command}", $name ?? DevCommand::nameFromCommand($command)); @@ -155,9 +159,10 @@ public static function artisan(string $command, ?string $name = null): DevComman /** * Register a Node command, automatically prefixing it with the detected package manager's run command. * - * Boot-only. The command persists in a static property for the worker lifetime - * and affects every subsequent development command invocation. + * The command persists in a static property for the worker lifetime and + * affects every subsequent development command invocation. */ + #[BootOnly] public static function node(string $command, ?string $name = null): DevCommand { return self::register(self::getPackageManager()->getRunCommand($command), $name ?? DevCommand::nameFromCommand($command)); @@ -166,9 +171,10 @@ public static function node(string $command, ?string $name = null): DevCommand /** * Register a Node command, automatically prefixing it with the detected package manager's exec command. * - * Boot-only. The command persists in a static property for the worker lifetime - * and affects every subsequent development command invocation. + * The command persists in a static property for the worker lifetime and + * affects every subsequent development command invocation. */ + #[BootOnly] public static function nodeExec(string $command, ?string $name = null): DevCommand { return self::register(self::getPackageManager()->getExecCommand($command), $name ?? DevCommand::nameFromCommand($command)); @@ -209,8 +215,9 @@ public static function commands(): array /** * Set the mode to inline, where all commands are run in the same terminal window. * - * Boot-only. The mode is shared by all development command invocations in the worker. + * The mode is shared by all development command invocations in the worker. */ + #[BootOnly] public static function inline(): void { self::$mode = DevCommandMode::INLINE; @@ -219,8 +226,9 @@ public static function inline(): void /** * Set the mode to stream, where all commands are run in the same terminal window, but their output is interactive within a TUI. * - * Boot-only. The mode is shared by all development command invocations in the worker. + * The mode is shared by all development command invocations in the worker. */ + #[BootOnly] public static function stream(): void { self::$mode = DevCommandMode::STREAM; @@ -229,8 +237,9 @@ public static function stream(): void /** * Set the mode to tabs, where each command is run in its own terminal tab. * - * Boot-only. The mode is shared by all development command invocations in the worker. + * The mode is shared by all development command invocations in the worker. */ + #[BootOnly] public static function tabs(): void { self::$mode = DevCommandMode::TABS; @@ -247,8 +256,9 @@ public static function mode(): DevCommandMode /** * Enable timestamps in the output of the "dev" command. * - * Boot-only. The setting is shared by all development command invocations in the worker. + * The setting is shared by all development command invocations in the worker. */ + #[BootOnly] public static function withTimestamps(): void { self::$withTimestamps = true; @@ -265,8 +275,9 @@ public static function shouldIncludeTimestamps(): bool /** * Disable automatic restart of a "dev" command when it fails. * - * Boot-only. The setting is shared by all development command invocations in the worker. + * The setting is shared by all development command invocations in the worker. */ + #[BootOnly] public static function disableAutoRestart(): void { self::$autoRestart = false; @@ -283,8 +294,9 @@ public static function shouldAutoRestart(): bool /** * Set the number of lines of output to buffer for each command when running in tabbed mode. * - * Boot-only. The buffer size is shared by all development command invocations in the worker. + * The buffer size is shared by all development command invocations in the worker. */ + #[BootOnly] public static function bufferSize(int $lines): void { self::$bufferSize = $lines; @@ -301,8 +313,9 @@ public static function getBufferSize(): ?int /** * Set the number of lines of output to buffer total when running in stream mode. * - * Boot-only. The buffer size is shared by all development command invocations in the worker. + * The buffer size is shared by all development command invocations in the worker. */ + #[BootOnly] public static function streamBufferSize(int $lines): void { self::$streamBufferSize = $lines; @@ -471,9 +484,10 @@ protected static function isWithinPath(string $path, string $directory): bool /** * Set the commands that should be included when running the "dev" command. * - * Boot-only. The filter persists in a static property for the worker lifetime - * and affects every subsequent development command invocation. + * The filter persists in a static property for the worker lifetime and + * affects every subsequent development command invocation. */ + #[BootOnly] public static function only(string ...$names): void { self::$only = $names; @@ -482,9 +496,10 @@ public static function only(string ...$names): void /** * Set the commands that should be excluded when running the "dev" command. * - * Boot-only. The filter persists in a static property for the worker lifetime - * and affects every subsequent development command invocation. + * The filter persists in a static property for the worker lifetime and + * affects every subsequent development command invocation. */ + #[BootOnly] public static function except(string ...$names): void { self::$except = $names; @@ -493,10 +508,11 @@ public static function except(string ...$names): void /** * Set the order in which named commands should run when running the "dev" command. * - * Boot-only. The order is shared by all development command invocations in the worker. + * The order is shared by all development command invocations in the worker. * * @param array $names */ + #[BootOnly] public static function order(array $names): void { self::$order = $names; @@ -505,8 +521,9 @@ public static function order(array $names): void /** * Exclude any commands from the vendor directory. * - * Boot-only. The filter is shared by all development command invocations in the worker. + * The filter is shared by all development command invocations in the worker. */ + #[BootOnly] public static function withoutVendorCommands(): void { self::$withoutVendorCommands = true; @@ -515,8 +532,9 @@ public static function withoutVendorCommands(): void /** * Exclude the framework's default commands. * - * Boot-only. The filter is shared by all development command invocations in the worker. + * The filter is shared by all development command invocations in the worker. */ + #[BootOnly] public static function withoutDefaultCommands(): void { self::$withoutDefaultCommands = true; diff --git a/src/foundation/src/Events/DiscoverEvents.php b/src/foundation/src/Events/DiscoverEvents.php index c211524bcb..c4c7de5b28 100644 --- a/src/foundation/src/Events/DiscoverEvents.php +++ b/src/foundation/src/Events/DiscoverEvents.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Events\ShouldBeDiscovered; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Reflector; use Hypervel\Support\Str; @@ -122,11 +123,12 @@ protected static function classFromFile(SplFileInfo $file, string $basePath): st /** * Specify a callback to be used to guess class names. * - * Boot-only. The callback persists in a static property used during event - * discovery; runtime use has no effect on already-discovered listeners. + * The callback persists in a static property used during event discovery; + * runtime use has no effect on already-discovered listeners. * * @param callable(SplFileInfo, string): class-string $callback */ + #[BootOnly] public static function guessClassNamesUsing(callable $callback): void { static::$guessClassNamesUsingCallback = $callback; diff --git a/src/foundation/src/Exceptions/Handler.php b/src/foundation/src/Exceptions/Handler.php index 360e6332fe..02ca90f4e1 100644 --- a/src/foundation/src/Exceptions/Handler.php +++ b/src/foundation/src/Exceptions/Handler.php @@ -36,6 +36,7 @@ use Hypervel\Session\Store; use Hypervel\Session\TokenMismatchException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Auth; use Hypervel\Support\Lottery; @@ -233,9 +234,10 @@ public function register(): void * The callback receives exceptions matching its first parameter type, along with * the exception's log context. Returning false stops further reporting. * - * Boot-only. The callback persists on the shared handler and affects exception - * reporting for every subsequent request and job in the worker. + * The callback persists on the shared handler and affects exception reporting + * for every subsequent request and job in the worker. */ + #[BootOnly(whenShared: true)] public function reportable(callable $reportUsing): ReportableHandler { if (! $reportUsing instanceof Closure) { @@ -250,11 +252,12 @@ public function reportable(callable $reportUsing): ReportableHandler /** * Register a renderable callback. * - * Boot-only. The callback persists on the shared handler and affects exception - * rendering for every subsequent request in the worker. + * The callback persists on the shared handler and affects exception rendering + * for every subsequent request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function renderable(callable $renderUsing): static { if (! $renderUsing instanceof Closure) { @@ -269,13 +272,14 @@ public function renderable(callable $renderUsing): static /** * Register a new exception mapping. * - * Boot-only. The mapping persists on the shared handler and affects exception - * reporting and rendering for every subsequent request and job in the worker. + * The mapping persists on the shared handler and affects exception reporting + * and rendering for every subsequent request and job in the worker. * * @return $this * * @throws InvalidArgumentException */ + #[BootOnly(whenShared: true)] public function map(callable|string $from, Closure|string|null $to = null): static { if (is_string($to)) { @@ -300,11 +304,12 @@ public function map(callable|string $from, Closure|string|null $to = null): stat * * Alias of "ignore". * - * Boot-only. The exception types persist on the shared handler and affect - * exception reporting for every subsequent request and job in the worker. + * The exception types persist on the shared handler and affect exception + * reporting for every subsequent request and job in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function dontReport(array|string $exceptions): static { return $this->ignore($exceptions); @@ -313,12 +318,13 @@ public function dontReport(array|string $exceptions): static /** * Register a callback to determine if an exception should not be reported. * - * Boot-only. The callback persists on the shared handler and affects exception - * reporting for every subsequent request and job in the worker. + * The callback persists on the shared handler and affects exception reporting + * for every subsequent request and job in the worker. * * @param (callable(Throwable): bool) $dontReportWhen * @return $this */ + #[BootOnly(whenShared: true)] public function dontReportWhen(callable $dontReportWhen): static { if (! $dontReportWhen instanceof Closure) { @@ -333,11 +339,12 @@ public function dontReportWhen(callable $dontReportWhen): static /** * Indicate that the given exception type should not be reported. * - * Boot-only. The exception types persist on the shared handler and affect - * exception reporting for every subsequent request and job in the worker. + * The exception types persist on the shared handler and affect exception + * reporting for every subsequent request and job in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function ignore(array|string $exceptions): static { $exceptions = Arr::wrap($exceptions); @@ -350,11 +357,12 @@ public function ignore(array|string $exceptions): static /** * Indicate that jobs should stop retrying for the given exception types. * - * Boot-only. The exception types persist on the shared handler and affect - * every subsequently failed job in the worker. + * The exception types persist on the shared handler and affect every + * subsequently failed job in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function dontRetry(array|string $exceptions): static { $exceptions = Arr::wrap($exceptions); @@ -367,14 +375,15 @@ public function dontRetry(array|string $exceptions): static /** * Register a callback to determine if jobs should stop retrying for an exception. * - * Boot-only. The callback persists on the shared handler and affects job retry - * decisions for every subsequent job in the worker. + * The callback persists on the shared handler and affects job retry decisions + * for every subsequent job in the worker. * * @template TException of Throwable * * @param callable(TException): bool $dontRetryWhen * @return $this */ + #[BootOnly(whenShared: true)] public function dontRetryWhen(callable $dontRetryWhen): static { if (! $dontRetryWhen instanceof Closure) { @@ -446,11 +455,12 @@ public function shouldStopRetries(Throwable $e): bool /** * Indicate that the given attributes should never be flashed to the session on validation errors. * - * Boot-only. The attributes persist on the shared handler and are omitted - * from every subsequent validation redirect in the worker. + * The attributes persist on the shared handler and are omitted from every + * subsequent validation redirect in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function dontFlash(array|string $attributes): static { $this->dontFlash = array_values(array_unique( @@ -463,13 +473,14 @@ public function dontFlash(array|string $attributes): static /** * Set the log level for the given exception type. * - * Boot-only. The level persists on the shared handler and affects how later - * exceptions of that type are logged in the worker. + * The level persists on the shared handler and affects how later exceptions + * of that type are logged in the worker. * * @param class-string $type * @param LogLevel::* $level * @return $this */ + #[BootOnly(whenShared: true)] public function level(string $type, string $level): static { $this->levels[$type] = $level; @@ -724,11 +735,12 @@ protected function throttle(Throwable $e): Lottery|AdmissionPolicy|null /** * Specify the callback that should be used to throttle reportable exceptions. * - * Boot-only. The callback persists on the shared handler and affects exception - * reporting for every subsequent request and job in the worker. + * The callback persists on the shared handler and affects exception reporting + * for every subsequent request and job in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function throttleUsing(callable $throttleUsing): static { if (! $throttleUsing instanceof Closure) { @@ -743,11 +755,12 @@ public function throttleUsing(callable $throttleUsing): static /** * Remove the given exception class from the list of exceptions that should be ignored. * - * Boot-only. The exception lists persist on the shared handler and affect - * exception reporting for every subsequent request and job in the worker. + * The exception lists persist on the shared handler and affect exception + * reporting for every subsequent request and job in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function stopIgnoring(array|string $exceptions): static { $exceptions = Arr::wrap($exceptions); @@ -837,12 +850,13 @@ protected function context(): array /** * Register a closure that should be used to build exception context data. * - * Boot-only. The closure persists on the shared handler and runs for every + * The closure persists on the shared handler and runs for every * subsequently logged exception in the worker. * * @param Closure(Throwable, array): array $contextCallback * @return $this */ + #[BootOnly(whenShared: true)] public function buildContextUsing(Closure $contextCallback): static { $this->contextCallbacks[] = $contextCallback; @@ -914,12 +928,13 @@ protected function finalizeRenderedResponse(Request $request, SymfonyResponse $r /** * Register a callback before final exception response customization. * - * Boot-only. The callback is shared by every request in the worker. - * Callback return values are ignored. + * The callback is shared by every request in the worker. Callback + * return values are ignored. * * @param callable(SymfonyResponse, Throwable, Request): mixed $callback * @return $this */ + #[BootOnly(whenShared: true)] public function beforeResponding(callable $callback): static { $this->beforeRespondingCallbacks[] = Closure::fromCallable($callback); @@ -951,11 +966,12 @@ protected function afterResponseCallbacks(): array /** * Prepare the final, rendered response for an exception using the given callback. * - * Boot-only. The callback replaces the shared handler's response callback for - * every subsequent request in the worker. + * The callback replaces the shared handler's response callback for every + * subsequent request in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function respondUsing(callable $callback): static { $this->finalizeResponseCallback = $callback; @@ -1107,12 +1123,13 @@ protected function shouldReturnJson(Request $request, Throwable $e): bool /** * Register the callable that determines if the exception handler response should be JSON. * - * Boot-only. The callable replaces the shared handler's JSON check for every - * subsequent request in the worker. + * The callable replaces the shared handler's JSON check for every subsequent + * request in the worker. * * @param callable(Request $request, Throwable): bool $callback * @return $this */ + #[BootOnly(whenShared: true)] public function shouldRenderJsonWhen(callable $callback): static { $this->shouldRenderJsonWhenCallback = $callback; @@ -1339,11 +1356,12 @@ public function renderForConsole(OutputInterface $output, Throwable $e): void /** * Do not report duplicate exceptions. * - * Boot-only. The setting persists on the shared handler and applies to every - * subsequent request and job in the worker. + * The setting persists on the shared handler and applies to every subsequent + * request and job in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function dontReportDuplicates(): static { $this->withoutDuplicates = true; diff --git a/src/foundation/src/Exceptions/ReportableHandler.php b/src/foundation/src/Exceptions/ReportableHandler.php index 7dddd327e3..18cf8e8a34 100644 --- a/src/foundation/src/Exceptions/ReportableHandler.php +++ b/src/foundation/src/Exceptions/ReportableHandler.php @@ -4,6 +4,7 @@ namespace Hypervel\Foundation\Exceptions; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Traits\ReflectsClosures; use Throwable; @@ -64,11 +65,12 @@ public function handles(Throwable $e): bool /** * Indicate that report handling should stop after invoking this callback. * - * Boot-only. The flag persists on the registered callback and stops report - * handling for every exception it subsequently handles in the worker. + * The flag persists on the registered callback and stops report handling + * for every exception it subsequently handles in the worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function stop(): static { $this->shouldStop = true; diff --git a/src/foundation/src/Http/FormRequest.php b/src/foundation/src/Http/FormRequest.php index 5425d3e04a..cc96fc836c 100644 --- a/src/foundation/src/Http/FormRequest.php +++ b/src/foundation/src/Http/FormRequest.php @@ -20,6 +20,7 @@ use Hypervel\Http\Request; use Hypervel\Routing\Redirector; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\ValidatedInput; use Hypervel\Validation\UnknownFields; use Hypervel\Validation\ValidatesWhenResolvedTrait; @@ -408,9 +409,10 @@ protected function nearestClassWithAttribute(array $attributes, array $propertie /** * Enable or disable unknown-field rejection globally for all form requests. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The flag persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function failOnUnknownFields(bool $value = true): void { static::$globalFailOnUnknownFields = $value; diff --git a/src/foundation/src/Http/HtmlDumper.php b/src/foundation/src/Http/HtmlDumper.php index 597d817f11..5f93d55846 100644 --- a/src/foundation/src/Http/HtmlDumper.php +++ b/src/foundation/src/Http/HtmlDumper.php @@ -6,6 +6,7 @@ use Hypervel\Context\CoroutineContext; use Hypervel\Foundation\Concerns\ResolvesDumpSource; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\VarCloner; @@ -41,9 +42,9 @@ public function __construct( /** * Create a new HTML dumper instance and register it as the default dumper. * - * Boot-only. Registers a process-wide VarDumper handler for the worker - * lifetime. + * Registers a process-wide VarDumper handler for the worker lifetime. */ + #[BootOnly] public static function register(string $basePath, string $compiledViewPath): void { $cloner = tap(new VarCloner)->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); diff --git a/src/foundation/src/Http/Kernel.php b/src/foundation/src/Http/Kernel.php index 217de21581..8b5e4dafd0 100644 --- a/src/foundation/src/Http/Kernel.php +++ b/src/foundation/src/Http/Kernel.php @@ -18,6 +18,8 @@ use Hypervel\Http\Request; use Hypervel\Routing\Pipeline; use Hypervel\Routing\Router; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\InteractsWithTime; use InvalidArgumentException; @@ -472,12 +474,12 @@ public function hasMiddleware(string $middleware): bool /** * Add a new middleware to the beginning of the stack if it does not already exist. * - * Boot-only. Middleware persists in the singleton Kernel's global stack for - * the worker lifetime and runs on every subsequent request across all - * coroutines. + * Middleware persists in the singleton Kernel's global stack for the worker + * lifetime and runs on every subsequent request across all coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function prependMiddleware(string $middleware): static { if (array_search($middleware, $this->middleware) === false) { @@ -490,12 +492,12 @@ public function prependMiddleware(string $middleware): static /** * Add a new middleware to end of the stack if it does not already exist. * - * Boot-only. Middleware persists in the singleton Kernel's global stack for - * the worker lifetime and runs on every subsequent request across all - * coroutines. + * Middleware persists in the singleton Kernel's global stack for the worker + * lifetime and runs on every subsequent request across all coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function pushMiddleware(string $middleware): static { if (array_search($middleware, $this->middleware) === false) { @@ -508,13 +510,14 @@ public function pushMiddleware(string $middleware): static /** * Prepend the given middleware to the given middleware group. * - * Boot-only. The middleware group persists in the singleton Kernel for the - * worker lifetime and runs on every subsequent request matching the group. + * The middleware group persists in the singleton Kernel for the worker + * lifetime and runs on every subsequent request matching the group. * * @return $this * * @throws InvalidArgumentException */ + #[BootOnly] public function prependMiddlewareToGroup(string $group, string $middleware): static { if (! isset($this->middlewareGroups[$group])) { @@ -533,13 +536,14 @@ public function prependMiddlewareToGroup(string $group, string $middleware): sta /** * Append the given middleware to the given middleware group. * - * Boot-only. The middleware group persists in the singleton Kernel for the - * worker lifetime and runs on every subsequent request matching the group. + * The middleware group persists in the singleton Kernel for the worker + * lifetime and runs on every subsequent request matching the group. * * @return $this * * @throws InvalidArgumentException */ + #[BootOnly] public function appendMiddlewareToGroup(string $group, string $middleware): static { if (! isset($this->middlewareGroups[$group])) { @@ -558,12 +562,13 @@ public function appendMiddlewareToGroup(string $group, string $middleware): stat /** * Prepend the given middleware to the middleware priority list. * - * Boot-only. The middleware priority list persists in the singleton Kernel - * for the worker lifetime and affects middleware ordering for every - * subsequent request. + * The middleware priority list persists in the singleton Kernel for the + * worker lifetime and affects middleware ordering for every subsequent + * request. * * @return $this */ + #[BootOnly] public function prependToMiddlewarePriority(string $middleware): static { if (! in_array($middleware, $this->middlewarePriority)) { @@ -578,12 +583,13 @@ public function prependToMiddlewarePriority(string $middleware): static /** * Append the given middleware to the middleware priority list. * - * Boot-only. The middleware priority list persists in the singleton Kernel - * for the worker lifetime and affects middleware ordering for every - * subsequent request. + * The middleware priority list persists in the singleton Kernel for the + * worker lifetime and affects middleware ordering for every subsequent + * request. * * @return $this */ + #[BootOnly] public function appendToMiddlewarePriority(string $middleware): static { if (! in_array($middleware, $this->middlewarePriority)) { @@ -598,13 +604,14 @@ public function appendToMiddlewarePriority(string $middleware): static /** * Add the given middleware to the middleware priority list before other middleware. * - * Boot-only. The middleware priority list persists in the singleton Kernel - * for the worker lifetime and affects middleware ordering for every - * subsequent request. + * The middleware priority list persists in the singleton Kernel for the + * worker lifetime and affects middleware ordering for every subsequent + * request. * * @param array|string $before * @return $this */ + #[BootOnly] public function addToMiddlewarePriorityBefore(string|array $before, string $middleware): static { return $this->addToMiddlewarePriorityRelative($before, $middleware, after: false); @@ -613,13 +620,14 @@ public function addToMiddlewarePriorityBefore(string|array $before, string $midd /** * Add the given middleware to the middleware priority list after other middleware. * - * Boot-only. The middleware priority list persists in the singleton Kernel - * for the worker lifetime and affects middleware ordering for every - * subsequent request. + * The middleware priority list persists in the singleton Kernel for the + * worker lifetime and affects middleware ordering for every subsequent + * request. * * @param array|string $after * @return $this */ + #[BootOnly] public function addToMiddlewarePriorityAfter(string|array $after, string $middleware): static { return $this->addToMiddlewarePriorityRelative($after, $middleware); @@ -728,13 +736,13 @@ public function getGlobalMiddleware(): array /** * Set the application's global middleware. * - * Boot-only. Middleware persists in the singleton Kernel's global stack for - * the worker lifetime and runs on every subsequent request across all - * coroutines. + * Middleware persists in the singleton Kernel's global stack for the worker + * lifetime and runs on every subsequent request across all coroutines. * * @param array $middleware * @return $this */ + #[BootOnly] public function setGlobalMiddleware(array $middleware): static { $this->middleware = $middleware; @@ -757,12 +765,13 @@ public function getMiddlewareGroups(): array /** * Set the application's middleware groups. * - * Boot-only. Middleware groups persist in the singleton Kernel for the - * worker lifetime and affect every subsequent request matching those groups. + * Middleware groups persist in the singleton Kernel for the worker lifetime + * and affect every subsequent request matching those groups. * * @param array> $groups * @return $this */ + #[BootOnly] public function setMiddlewareGroups(array $groups): static { $this->middlewareGroups = $groups; @@ -787,12 +796,13 @@ public function getMiddlewareAliases(): array /** * Set the application's route middleware aliases. * - * Boot-only. Middleware aliases persist in the singleton Kernel for the - * worker lifetime and affect every subsequent request that uses them. + * Middleware aliases persist in the singleton Kernel for the worker + * lifetime and affect every subsequent request that uses them. * * @param array $aliases * @return $this */ + #[BootOnly] public function setMiddlewareAliases(array $aliases): static { $this->middlewareAliases = $aliases; @@ -805,13 +815,14 @@ public function setMiddlewareAliases(array $aliases): static /** * Set the application's middleware priority. * - * Boot-only. The middleware priority list persists in the singleton Kernel - * for the worker lifetime and affects middleware ordering for every - * subsequent request. + * The middleware priority list persists in the singleton Kernel for the + * worker lifetime and affects middleware ordering for every subsequent + * request. * * @param string[] $priority * @return $this */ + #[BootOnly] public function setMiddlewarePriority(array $priority): static { $this->middlewarePriority = $priority; @@ -832,12 +843,13 @@ public function getApplication(): Application /** * Set the application instance. * - * Tests only. Swaps the singleton Kernel's application reference; per-request - * use races across coroutines and breaks every concurrent request the worker - * is handling. + * Swaps the singleton Kernel's application reference; per-request use races + * across coroutines and breaks every concurrent request the worker is + * handling. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Application $app): static { $this->app = $app; diff --git a/src/foundation/src/Http/Middleware/ConvertEmptyStringsToNull.php b/src/foundation/src/Http/Middleware/ConvertEmptyStringsToNull.php index cb5074d0a1..b203c0de59 100644 --- a/src/foundation/src/Http/Middleware/ConvertEmptyStringsToNull.php +++ b/src/foundation/src/Http/Middleware/ConvertEmptyStringsToNull.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; class ConvertEmptyStringsToNull extends TransformsRequest { @@ -41,9 +42,10 @@ protected function transform(string $key, mixed $value): mixed /** * Register a callback that instructs the middleware to be skipped. * - * Boot-only. Skip callbacks persist in a static property for the worker - * lifetime and run on every subsequent request. + * Skip callbacks persist in a static property for the worker lifetime + * and run on every subsequent request. */ + #[BootOnly] public static function skipWhen(Closure $callback): void { static::$skipCallbacks[] = $callback; diff --git a/src/foundation/src/Http/Middleware/PreventRequestForgery.php b/src/foundation/src/Http/Middleware/PreventRequestForgery.php index edf4fc89e8..9b8e6c7f50 100644 --- a/src/foundation/src/Http/Middleware/PreventRequestForgery.php +++ b/src/foundation/src/Http/Middleware/PreventRequestForgery.php @@ -16,6 +16,7 @@ use Hypervel\Http\Request; use Hypervel\Session\TokenMismatchException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\InteractsWithTime; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; @@ -213,9 +214,10 @@ protected function newCookie(Request $request, array $config): Cookie /** * Indicate that the given URIs should be excluded from CSRF verification. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The list persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function except(array|string $uris): void { static::$neverVerify = array_values(array_unique( @@ -226,9 +228,10 @@ public static function except(array|string $uris): void /** * Indicate that requests from the same site should be allowed. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The flag persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function allowSameSite(bool $allow = true): void { static::$allowSameSite = $allow; @@ -237,9 +240,10 @@ public static function allowSameSite(bool $allow = true): void /** * Indicate that only origin verification should be used. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The flag persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function useOriginOnly(bool $originOnly = true): void { static::$originOnly = $originOnly; diff --git a/src/foundation/src/Http/Middleware/PreventRequestsDuringMaintenance.php b/src/foundation/src/Http/Middleware/PreventRequestsDuringMaintenance.php index d9d916092e..ac022f15c8 100644 --- a/src/foundation/src/Http/Middleware/PreventRequestsDuringMaintenance.php +++ b/src/foundation/src/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -10,6 +10,7 @@ use Hypervel\Foundation\Http\Middleware\Concerns\ExcludesPaths; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\HttpKernel\Exception\HttpException; class PreventRequestsDuringMaintenance @@ -143,9 +144,10 @@ public function getExcludedPaths(): array /** * Indicate that the given URIs should always be accessible. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The list persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function except(array|string $uris): void { static::$neverPrevent = array_values(array_unique( diff --git a/src/foundation/src/Http/Middleware/TrimStrings.php b/src/foundation/src/Http/Middleware/TrimStrings.php index 2a5f05346c..31cc12c9ea 100644 --- a/src/foundation/src/Http/Middleware/TrimStrings.php +++ b/src/foundation/src/Http/Middleware/TrimStrings.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; class TrimStrings extends TransformsRequest @@ -79,9 +80,10 @@ protected function shouldSkip(string $key, array $except): bool /** * Indicate that the given attributes should never be trimmed. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The list persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function except(array|string $attributes): void { static::$neverTrim = array_values(array_unique( @@ -92,9 +94,10 @@ public static function except(array|string $attributes): void /** * Register a callback that instructs the middleware to be skipped. * - * Boot-only. Skip callbacks persist in a static property for the worker - * lifetime and run on every subsequent request. + * Skip callbacks persist in a static property for the worker lifetime + * and run on every subsequent request. */ + #[BootOnly] public static function skipWhen(Closure $callback): void { static::$skipCallbacks[] = $callback; diff --git a/src/foundation/src/PackageManifest.php b/src/foundation/src/PackageManifest.php index 9c06f04692..701c25a5fe 100644 --- a/src/foundation/src/PackageManifest.php +++ b/src/foundation/src/PackageManifest.php @@ -6,6 +6,7 @@ use Exception; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Composer; use Hypervel\Support\Json; @@ -407,12 +408,12 @@ protected function packagesToIgnore(): array * Used by testbench to suppress package discovery at runtime * without modifying the project's composer.json. * - * Boot or tests only. The list persists in a static property used during - * package discovery; runtime use has no effect on already-discovered - * providers. + * The list persists in a static property used during package discovery; + * runtime use has no effect on already-discovered providers. * * @param array $packages */ + #[BootOrTestsOnly] public static function ignorePackageDiscoveriesFrom(array $packages): void { static::$packagesToIgnore = $packages; diff --git a/src/foundation/src/Providers/FoundationServiceProvider.php b/src/foundation/src/Providers/FoundationServiceProvider.php index f101cb017e..bfc2fb8914 100644 --- a/src/foundation/src/Providers/FoundationServiceProvider.php +++ b/src/foundation/src/Providers/FoundationServiceProvider.php @@ -92,6 +92,7 @@ use Hypervel\Http\Request; use Hypervel\Log\Events\MessageLogged; use Hypervel\Queue\Events\JobAttempted; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Composer; use Hypervel\Support\Defer\DeferredCallback; @@ -242,9 +243,10 @@ protected function registerClock(): void /** * Register the console schedule implementation. * - * Boot-only. The schedule binding is shared for the worker lifetime, and - * calling this again replaces the schedule used by later resolutions. + * The schedule binding is shared for the worker lifetime, and calling + * this again replaces the schedule used by later resolutions. */ + #[BootOnly] public function registerConsoleSchedule(): void { $this->app->singleton(Schedule::class, function ($app) { @@ -255,9 +257,10 @@ public function registerConsoleSchedule(): void /** * Register a var dumper (with source) to debug variables. * - * Boot-only. The dumper handler and casters are process-global and apply to - * every later dump in the worker. + * The dumper handler and casters are process-global and apply to every + * later dump in the worker. */ + #[BootOnly] public function registerDumper(): void { AbstractCloner::$defaultCasters[ConnectionInterface::class] ??= [StubCaster::class, 'cutInternals']; @@ -319,11 +322,11 @@ protected function registerDeferHandler(): void /** * Register the "validate" macro on the request. * - * Boot-only. Request macros are static and shared by every request in the - * worker. + * Request macros are static and shared by every request in the worker. * * @throws ValidationException */ + #[BootOnly] public function registerRequestValidation(): void { Request::macro('validate', function (array $rules, ...$params) { @@ -351,9 +354,9 @@ public function registerRequestValidation(): void /** * Register the "hasValidSignature" macro on the request. * - * Boot-only. Request macros are static and shared by every request in the - * worker. + * Request macros are static and shared by every request in the worker. */ + #[BootOnly] public function registerRequestSignatureValidation(): void { Request::macro('hasValidSignature', function ($absolute = true) { @@ -376,9 +379,10 @@ public function registerRequestSignatureValidation(): void /** * Register the maintenance mode manager and its caching decorator. * - * Boot-only. The maintenance mode binding is shared for the worker lifetime, - * and calling this again replaces the instance used by later resolutions. + * The maintenance mode binding is shared for the worker lifetime, and + * calling this again replaces the instance used by later resolutions. */ + #[BootOnly] public function registerMaintenanceModeManager(): void { $this->app->singleton( diff --git a/src/foundation/src/Support/Providers/EventServiceProvider.php b/src/foundation/src/Support/Providers/EventServiceProvider.php index c3d752e123..2d7402590d 100644 --- a/src/foundation/src/Support/Providers/EventServiceProvider.php +++ b/src/foundation/src/Support/Providers/EventServiceProvider.php @@ -8,6 +8,7 @@ use Hypervel\Auth\Listeners\SendEmailVerificationNotification; use Hypervel\Foundation\Events\DiscoverEvents; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Event; use Hypervel\Support\LazyCollection; use Hypervel\Support\ServiceProvider; @@ -157,11 +158,12 @@ protected function discoverEventsWithin(): iterable /** * Add the given event discovery paths to the application's event discovery paths. * - * Boot-only. The path list persists in a static property used during - * provider boot; runtime use has no effect on already-discovered events. + * The path list persists in a static property used during provider boot; + * runtime use has no effect on already-discovered events. * * @param iterable|string $paths */ + #[BootOnly] public static function addEventDiscoveryPaths(iterable|string $paths): void { static::$eventDiscoveryPaths = (new LazyCollection(static::$eventDiscoveryPaths)) @@ -173,11 +175,12 @@ public static function addEventDiscoveryPaths(iterable|string $paths): void /** * Set the globally configured event discovery paths. * - * Boot-only. The path list persists in a static property used during - * provider boot; runtime use has no effect on already-discovered events. + * The path list persists in a static property used during provider boot; + * runtime use has no effect on already-discovered events. * * @param iterable $paths */ + #[BootOnly] public static function setEventDiscoveryPaths(iterable $paths): void { static::$eventDiscoveryPaths = $paths; @@ -194,9 +197,10 @@ protected function eventDiscoveryBasePath(): string /** * Disable event discovery for the application. * - * Boot-only. The flag persists in a static property used during provider - * boot; runtime use has no effect on already-discovered events. + * The flag persists in a static property used during provider boot; + * runtime use has no effect on already-discovered events. */ + #[BootOnly] public static function disableEventDiscovery(): void { static::$shouldDiscoverEvents = false; diff --git a/src/foundation/src/Support/Providers/RouteServiceProvider.php b/src/foundation/src/Support/Providers/RouteServiceProvider.php index aaddf40f0d..4a6e6e2e91 100644 --- a/src/foundation/src/Support/Providers/RouteServiceProvider.php +++ b/src/foundation/src/Support/Providers/RouteServiceProvider.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Contracts\Routing\UrlGenerator; use Hypervel\Routing\Router; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\ServiceProvider; use Hypervel\Support\Traits\ForwardsCalls; @@ -88,9 +89,10 @@ protected function routes(Closure $routesCallback): static /** * Register the callback that will be used to load the application's routes. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs during route loading at boot. + * The callback persists in a static property for the worker lifetime + * and runs during route loading at boot. */ + #[BootOnly] public static function loadRoutesUsing(?Closure $routesCallback): void { self::$alwaysLoadRoutesUsing = $routesCallback; @@ -99,9 +101,10 @@ public static function loadRoutesUsing(?Closure $routesCallback): void /** * Register the callback that will be used to load the application's cached routes. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs during route loading at boot when route cache exists. + * The callback persists in a static property for the worker lifetime and + * runs during route loading at boot when route cache exists. */ + #[BootOnly] public static function loadCachedRoutesUsing(?Closure $routesCallback): void { self::$alwaysLoadCachedRoutesUsing = $routesCallback; @@ -116,9 +119,10 @@ public static function loadCachedRoutesUsing(?Closure $routesCallback): void * though an application route with the same method, domain and URI replaces * them. Cached routes already contain them, so the callback does not run. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs during route loading at boot. + * The callback persists in a static property for the worker lifetime + * and runs during route loading at boot. */ + #[BootOnly] public static function beforeLoadingRoutes(Closure $callback): void { self::$beforeLoadingRoutes[] = $callback; diff --git a/src/foundation/src/Testing/Concerns/RequiresHashFieldExpiration.php b/src/foundation/src/Testing/Concerns/RequiresHashFieldExpiration.php index b01f3e938e..eb5bb5e73a 100644 --- a/src/foundation/src/Testing/Concerns/RequiresHashFieldExpiration.php +++ b/src/foundation/src/Testing/Concerns/RequiresHashFieldExpiration.php @@ -5,6 +5,7 @@ namespace Hypervel\Foundation\Testing\Concerns; use Hypervel\Redis\RedisRequirements; +use Hypervel\Support\Attributes\TestsOnly; /** * Skips tests when the environment doesn't support Redis hash field @@ -76,8 +77,9 @@ protected function detectedServerInfo(): array /** * Flush the memoized hash field expiration support check. * - * Tests only. Resets the capability result shared by tests in this class. + * Resets the capability result shared by tests in this class. */ + #[TestsOnly] public static function flushHashFieldExpirationState(): void { self::$hashFieldExpirationSupported = null; diff --git a/src/foundation/src/Testing/DatabaseConnectionResolver.php b/src/foundation/src/Testing/DatabaseConnectionResolver.php index 8c4b35e51f..2469da7d03 100644 --- a/src/foundation/src/Testing/DatabaseConnectionResolver.php +++ b/src/foundation/src/Testing/DatabaseConnectionResolver.php @@ -17,6 +17,7 @@ use Hypervel\Database\ConnectionResolver; use Hypervel\Database\Pool\DatabasePool; use Hypervel\Database\Pool\PooledConnection; +use Hypervel\Support\Attributes\TestsOnly; use PDO; use Throwable; use UnitEnum; @@ -67,9 +68,10 @@ class DatabaseConnectionResolver extends ConnectionResolver implements CachedCon * services. A rebinding hook is registered so Event::fake() automatically * updates cached connections with the new dispatcher. * - * Tests only. The cached connections are process-global, so runtime use can - * reset or discard a connection borrowed by another coroutine. + * The cached connections are process-global, so runtime use can reset or + * discard a connection borrowed by another coroutine. */ + #[TestsOnly] public static function resetCachedConnections(): void { $container = Container::getInstance(); @@ -107,9 +109,10 @@ public static function resetCachedConnections(): void /** * Discard all cached connections and clear resolver lifecycle state. * - * Tests only. The cached connections are process-global, so runtime use can - * discard a connection borrowed by another coroutine. + * The cached connections are process-global, so runtime use can discard a + * connection borrowed by another coroutine. */ + #[TestsOnly] public static function flushCachedConnections(): void { static::discardCachedConnections(); @@ -312,8 +315,9 @@ public function connection(UnitEnum|string|null $name = null): ConnectionInterfa /** * Restore a pooled in-memory database, returning false when pooling is disabled. * - * Tests only. Replaces the database shared by this application's pool borrowers. + * Replaces the database shared by this application's pool borrowers. */ + #[TestsOnly] public function restoreInMemoryPdoIfPooled(UnitEnum|string|null $name, PDO $pdo): bool { $connectionName = ConnectionName::parse($this->connectionName($name)); diff --git a/src/foundation/src/Vite.php b/src/foundation/src/Vite.php index 7a4308d5d8..600633f74c 100644 --- a/src/foundation/src/Vite.php +++ b/src/foundation/src/Vite.php @@ -7,6 +7,8 @@ use Exception; use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Support\Htmlable; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\HtmlString; use Hypervel\Support\Js; @@ -139,11 +141,12 @@ public function useCspNonce(?string $nonce = null): string /** * Use the given key to detect integrity hashes in the manifest. * - * Boot-only. The key is stored on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The key is stored on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useIntegrityKey(string|false $key): static { $this->integrityKey = $key; @@ -181,11 +184,12 @@ public function mergeEntryPoints(array $entryPoints): static /** * Set the filename for the manifest file. * - * Boot-only. The filename is stored on the shared Vite instance and affects - * every subsequent manifest lookup by this worker. + * The filename is stored on the shared Vite instance and affects every + * subsequent manifest lookup by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useManifestFilename(string $filename): static { $this->manifestFilename = $filename; @@ -196,11 +200,12 @@ public function useManifestFilename(string $filename): static /** * Resolve asset paths using the provided resolver. * - * Boot-only. The resolver is stored on the shared Vite instance and affects - * every subsequent asset path generated by this worker. + * The resolver is stored on the shared Vite instance and affects every + * subsequent asset path generated by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function createAssetPathsUsing(?callable $resolver): static { $this->assetPathResolver = $resolver; @@ -219,11 +224,12 @@ public function hotFile(): string /** * Set the Vite "hot" file path. * - * Boot-only. The path is stored on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The path is stored on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useHotFile(string $path): static { $this->hotFile = $path; @@ -234,11 +240,12 @@ public function useHotFile(string $path): static /** * Set the Vite build directory. * - * Boot-only. The directory is stored on the shared Vite instance and - * affects every subsequent asset render by this worker. + * The directory is stored on the shared Vite instance and affects + * every subsequent asset render by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useBuildDirectory(string $path): static { $this->buildDirectory = $path; @@ -249,12 +256,13 @@ public function useBuildDirectory(string $path): static /** * Use the given callback to resolve attributes for script tags. * - * Boot-only. The resolver persists on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The resolver persists on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @param array|(callable(string, string, ?array, ?array): array) $attributes * @return $this */ + #[BootOnly(whenShared: true)] public function useScriptTagAttributes(callable|array $attributes): static { if (! is_callable($attributes)) { @@ -269,12 +277,13 @@ public function useScriptTagAttributes(callable|array $attributes): static /** * Use the given callback to resolve attributes for style tags. * - * Boot-only. The resolver persists on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The resolver persists on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @param array|(callable(string, string, ?array, ?array): array) $attributes * @return $this */ + #[BootOnly(whenShared: true)] public function useStyleTagAttributes(callable|array $attributes): static { if (! is_callable($attributes)) { @@ -289,12 +298,13 @@ public function useStyleTagAttributes(callable|array $attributes): static /** * Use the given callback to resolve attributes for preload tags. * - * Boot-only. The resolver persists on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The resolver persists on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @param array|(callable(string, string, ?array, ?array): (array|false))|false $attributes * @return $this */ + #[BootOnly(whenShared: true)] public function usePreloadTagAttributes(callable|array|false $attributes): static { if (! is_callable($attributes)) { @@ -309,11 +319,12 @@ public function usePreloadTagAttributes(callable|array|false $attributes): stati /** * Eagerly prefetch assets. * - * Boot-only. The strategy is stored on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The strategy is stored on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function prefetch(?int $concurrency = null, string $event = 'load'): static { $this->prefetchEvent = $event; @@ -326,11 +337,12 @@ public function prefetch(?int $concurrency = null, string $event = 'load'): stat /** * Use the "waterfall" prefetching strategy. * - * Boot-only. The strategy is stored on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The strategy is stored on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useWaterfallPrefetching(?int $concurrency = null): static { return $this->usePrefetchStrategy('waterfall', [ @@ -341,11 +353,12 @@ public function useWaterfallPrefetching(?int $concurrency = null): static /** * Use the "aggressive" prefetching strategy. * - * Boot-only. The strategy is stored on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The strategy is stored on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useAggressivePrefetching(): static { return $this->usePrefetchStrategy('aggressive'); @@ -354,13 +367,14 @@ public function useAggressivePrefetching(): static /** * Set the prefetching strategy. * - * Boot-only. The strategy is stored on the shared Vite instance and affects - * every subsequent asset render by this worker. + * The strategy is stored on the shared Vite instance and affects every + * subsequent asset render by this worker. * * @return $this * * @throws Exception */ + #[BootOnly(whenShared: true)] public function usePrefetchStrategy(?string $strategy, array $config = []): static { $this->prefetchStrategy = $strategy; @@ -1127,11 +1141,12 @@ protected function viteFonts(): ViteFonts /** * Set the font manifest filename. * - * Boot-only. The filename is stored on the shared Vite instance and affects - * every subsequent font manifest lookup by this worker. + * The filename is stored on the shared Vite instance and affects every + * subsequent font manifest lookup by this worker. * * @return $this */ + #[BootOnly(whenShared: true)] public function useFontsManifestFilename(string $filename): static { $this->fontsManifestFilename = $filename; @@ -1160,9 +1175,10 @@ public function toHtml(): string /** * Flush all Vite state. * - * Boot or tests only. Clears worker-wide Vite manifests and request-local - * context used by subsequent asset rendering. + * Clears worker-wide Vite manifests and request-local context used by + * subsequent asset rendering. */ + #[BootOrTestsOnly] public static function flush(): void { CoroutineContext::forget(static::NONCE_CONTEXT_KEY); diff --git a/src/framework/composer.json b/src/framework/composer.json index 984eea1413..2bbc99f1f0 100644 --- a/src/framework/composer.json +++ b/src/framework/composer.json @@ -41,6 +41,7 @@ "ext-tokenizer": "*", "composer-runtime-api": "^2.2", "hypervel/api-client": "^0.4", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/bus": "^0.4", diff --git a/src/grpc/composer.json b/src/grpc/composer.json index baae1f9a4f..d0fc07a2ba 100644 --- a/src/grpc/composer.json +++ b/src/grpc/composer.json @@ -30,12 +30,13 @@ }, "require": { "php": "^8.4", - "composer-runtime-api": "^2.2", "ext-filter": "*", "ext-swoole": "^6.3.0RC1", "ext-zlib": "*", + "composer-runtime-api": "^2.2", "google/common-protos": "^4.14", "google/protobuf": "^5.35", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", "hypervel/container": "^0.4", diff --git a/src/grpc/src/GrpcOperationRunner.php b/src/grpc/src/GrpcOperationRunner.php index a23b0ce27d..ced4b3c5fc 100644 --- a/src/grpc/src/GrpcOperationRunner.php +++ b/src/grpc/src/GrpcOperationRunner.php @@ -5,6 +5,7 @@ namespace Hypervel\Grpc; use Hypervel\Grpc\Contracts\GrpcOperationObserver; +use Hypervel\Support\Attributes\BootOnly; use Throwable; class GrpcOperationRunner @@ -15,9 +16,10 @@ class GrpcOperationRunner /** * Register a logical gRPC operation observer. * - * Boot-only. Observers persist on this singleton for the worker lifetime - * and apply to every subsequent gRPC operation. + * On the shared runner, observers persist for the worker lifetime and + * apply to every subsequent gRPC operation. */ + #[BootOnly(whenShared: true)] public function observe(GrpcOperationObserver $observer): void { $this->observers[] = $observer; diff --git a/src/grpc/src/Health/HealthService.php b/src/grpc/src/Health/HealthService.php index 04c001182b..f160bde179 100644 --- a/src/grpc/src/Health/HealthService.php +++ b/src/grpc/src/Health/HealthService.php @@ -10,6 +10,7 @@ use Hypervel\Grpc\Health\V1\HealthListRequest; use Hypervel\Grpc\Health\V1\HealthListResponse; use Hypervel\Grpc\StatusCode; +use Hypervel\Support\Attributes\Unsupported; readonly class HealthService { @@ -50,6 +51,7 @@ public function list(HealthListRequest $request): HealthListResponse * * @return iterable */ + #[Unsupported('Use check() to poll the health status.')] public function watch(HealthCheckRequest $request): iterable { throw new RpcException( diff --git a/src/grpc/src/Server/GrpcRouter.php b/src/grpc/src/Server/GrpcRouter.php index c1c6047c37..7da8b54dca 100644 --- a/src/grpc/src/Server/GrpcRouter.php +++ b/src/grpc/src/Server/GrpcRouter.php @@ -11,6 +11,7 @@ use Hypervel\Pipeline\Pipeline as BasePipeline; use Hypervel\Routing\Route; use Hypervel\Routing\Router; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; use InvalidArgumentException; use ReflectionClass; @@ -33,8 +34,12 @@ class GrpcRouter extends Router /** * Synchronize application route middleware before worker warmup. * + * On the shared gRPC router, the middleware aliases, groups and priority + * apply to every subsequent gRPC call in the worker. + * * @internal */ + #[BootOnly(whenShared: true)] public function syncMiddlewareFrom(Router $router): void { foreach ($router->getMiddleware() as $name => $middleware) { @@ -50,7 +55,11 @@ public function syncMiddlewareFrom(Router $router): void /** * Validate gRPC route actions before compiling and warming the collection. + * + * Replaces the route collection and registers protobuf descriptors + * process-wide; runtime use breaks concurrent gRPC calls. */ + #[BootOnly] public function compileAndWarm(): void { foreach ($this->getRoutes()->getRoutes() as $route) { diff --git a/src/hashing/composer.json b/src/hashing/composer.json index 435aa9422f..46e0568960 100644 --- a/src/hashing/composer.json +++ b/src/hashing/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/contracts": "^0.4", "hypervel/support": "^0.4" }, diff --git a/src/hashing/src/ArgonHasher.php b/src/hashing/src/ArgonHasher.php index 9e16c80b5c..a538b673e5 100644 --- a/src/hashing/src/ArgonHasher.php +++ b/src/hashing/src/ArgonHasher.php @@ -6,6 +6,7 @@ use Error; use Hypervel\Contracts\Hashing\Hasher as HasherContract; +use Hypervel\Support\Attributes\BootOnly; use RuntimeException; use SensitiveParameter; @@ -151,11 +152,12 @@ protected function isUsingValidOptions(string $hashedValue): bool /** * Set the default password memory factor. * - * Boot-only. Mutates the shared hasher instance held by the HashManager; + * Mutates the shared hasher instance held by the HashManager; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setMemory(int $memory): static { $this->memory = $memory; @@ -166,11 +168,12 @@ public function setMemory(int $memory): static /** * Set the default password timing factor. * - * Boot-only. Mutates the shared hasher instance held by the HashManager; + * Mutates the shared hasher instance held by the HashManager; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setTime(int $time): static { $this->time = $time; @@ -181,11 +184,12 @@ public function setTime(int $time): static /** * Set the default password threads factor. * - * Boot-only. Mutates the shared hasher instance held by the HashManager; + * Mutates the shared hasher instance held by the HashManager; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setThreads(int $threads): static { $this->threads = $threads; diff --git a/src/hashing/src/BcryptHasher.php b/src/hashing/src/BcryptHasher.php index 6bd91679f6..115630acdd 100644 --- a/src/hashing/src/BcryptHasher.php +++ b/src/hashing/src/BcryptHasher.php @@ -6,6 +6,7 @@ use Error; use Hypervel\Contracts\Hashing\Hasher as HasherContract; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use RuntimeException; use SensitiveParameter; @@ -134,11 +135,12 @@ protected function isUsingValidOptions(string $hashedValue): bool /** * Set the default password work factor. * - * Boot-only. Mutates the shared hasher instance held by the HashManager; + * Mutates the shared hasher instance held by the HashManager; * per-request use races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setRounds(int $rounds): static { $this->rounds = (int) $rounds; diff --git a/src/head/composer.json b/src/head/composer.json index 8ef40c78fc..c2bf0fe9b3 100644 --- a/src/head/composer.json +++ b/src/head/composer.json @@ -26,6 +26,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/container": "^0.4", diff --git a/src/head/src/ErrorPages.php b/src/head/src/ErrorPages.php index 6c24998da2..e959c93684 100644 --- a/src/head/src/ErrorPages.php +++ b/src/head/src/ErrorPages.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Head\Routing\RouteAttributeParser; +use Hypervel\Support\Attributes\BootOnly; class ErrorPages { @@ -30,8 +31,9 @@ public function __construct( * Define head metadata for every error page, using either named * route-attribute values or a single head builder callback. * - * Boot-only. These defaults are shared by every request in the worker. + * On the shared manager's error pages, these defaults apply to every request in the worker. */ + #[BootOnly(whenShared: true)] public function defaults(mixed ...$head): static { $this->defaults = $this->apply($this->defaults, $head); @@ -43,8 +45,9 @@ public function defaults(mixed ...$head): static * Define head metadata for a specific error status, using either named * route-attribute values or a single head builder callback. * - * Boot-only. These definitions are shared by every request in the worker. + * On the shared manager's error pages, these definitions apply to every request in the worker. */ + #[BootOnly(whenShared: true)] public function status(int $status, mixed ...$head): static { $this->statuses[$status] = $this->apply(new HeadData, $head); diff --git a/src/head/src/HeadManager.php b/src/head/src/HeadManager.php index 968737e450..ed5cc50b45 100644 --- a/src/head/src/HeadManager.php +++ b/src/head/src/HeadManager.php @@ -18,6 +18,7 @@ use Hypervel\Http\Request; use Hypervel\Inertia\InertiaState; use Hypervel\Routing\Route; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\HtmlString; use Hypervel\Support\Traits\Conditionable; use Hypervel\Support\Traits\Macroable; @@ -66,10 +67,11 @@ public function __construct( /** * Define head tags rendered as server-owned tags on every Inertia document. * - * Boot-only. These tags are shared by every request in the worker. + * On the shared manager, these tags apply to every request in the worker. * * @param callable(HeadBuilder): mixed $callback */ + #[BootOnly(whenShared: true)] public function inertiaGlobals(callable $callback): static { $this->inertiaGlobals = $this->inertiaGlobals->merge($this->define($callback)); @@ -80,10 +82,11 @@ public function inertiaGlobals(callable $callback): static /** * Define default head data merged beneath every page's head. * - * Boot-only. These defaults are shared by every request in the worker. + * On the shared manager, these defaults apply to every request in the worker. * * @param callable(HeadBuilder): mixed $callback */ + #[BootOnly(whenShared: true)] public function defaults(callable $callback): static { $this->defaults = $this->defaults->merge($this->define($callback)->asDefaults()); @@ -94,10 +97,11 @@ public function defaults(callable $callback): static /** * Resolve default head data from the current request context. * - * Boot-only. The callback is shared by every request in the worker. + * On the shared manager, the callback applies to every request in the worker. * * @param null|(Closure(HeadBuilder): mixed) $resolver */ + #[BootOnly(whenShared: true)] public function resolveDefaultsUsing(?Closure $resolver): static { $this->defaultsResolver = $resolver; @@ -108,10 +112,11 @@ public function resolveDefaultsUsing(?Closure $resolver): static /** * Define head data for error pages. * - * Boot-only. These error definitions are shared by every request in the worker. + * On the shared manager, these error definitions apply to every request in the worker. * * @param callable(ErrorPages): mixed $callback */ + #[BootOnly(whenShared: true)] public function errors(callable $callback): static { $callback($this->errorPages); @@ -122,10 +127,11 @@ public function errors(callable $callback): static /** * Resolve error head data from the current request context and status. * - * Boot-only. The callback is shared by every request in the worker. + * On the shared manager, the callback applies to every request in the worker. * * @param null|(Closure(HeadBuilder, int): mixed) $resolver */ + #[BootOnly(whenShared: true)] public function resolveErrorsUsing(?Closure $resolver): static { $this->errorsResolver = $resolver; @@ -136,10 +142,11 @@ public function resolveErrorsUsing(?Closure $resolver): static /** * Resolve the canonical origin from the current request context. * - * Boot-only. The callback persists in a static property and affects every request in the worker. + * The callback persists in a static property and affects every request in the worker. * * @param null|(Closure(): ?string) $resolver */ + #[BootOnly] public function resolveCanonicalOriginUsing(?Closure $resolver): static { Canonical::resolveOriginUsing($resolver); @@ -150,8 +157,9 @@ public function resolveCanonicalOriginUsing(?Closure $resolver): static /** * Customize the Inertia prop the head elements are shared under. * - * Boot-only. The prop name is shared by every request in the worker. + * On the shared manager, the prop name applies to every request in the worker. */ + #[BootOnly(whenShared: true)] public function inertia(string $prop = self::INERTIA_PROP): static { $this->inertiaProp = $prop; @@ -170,7 +178,8 @@ public function inertiaProp(): string /** * Register a custom tag builder to render on every head. * - * Boot-only. Registered builders affect every request in the worker. + * When the registry is the application's worker-shared registry, the builder + * affects every request in the worker. * * @param class-string $builder */ diff --git a/src/head/src/Inertia/Integration.php b/src/head/src/Inertia/Integration.php index ee5c55ae6f..e733a42b84 100644 --- a/src/head/src/Inertia/Integration.php +++ b/src/head/src/Inertia/Integration.php @@ -10,6 +10,7 @@ use Hypervel\Head\Rendering\HeadRenderer; use Hypervel\Inertia\InertiaServiceProvider; use Hypervel\Inertia\Ssr\Gateway; +use Hypervel\Support\Attributes\BootOnly; class Integration { @@ -23,8 +24,9 @@ public function __construct(protected Application $app) /** * Register Hypervel Head's optional Inertia integration. * - * Boot-only. Gateway bindings and middleware affect every request in the worker. + * Gateway bindings and middleware affect every request in the worker. */ + #[BootOnly] public function register(): void { if ($this->app->getProviders(InertiaServiceProvider::class) === []) { diff --git a/src/head/src/Routing/HeadRouteMacros.php b/src/head/src/Routing/HeadRouteMacros.php index 33585afdbd..019e9ee2d6 100644 --- a/src/head/src/Routing/HeadRouteMacros.php +++ b/src/head/src/Routing/HeadRouteMacros.php @@ -11,6 +11,7 @@ use Hypervel\Routing\Route; use Hypervel\Routing\Router; use Hypervel\Routing\RouteRegistrar; +use Hypervel\Support\Attributes\BootOnly; /** * Registers the withHead() route macros. @@ -27,8 +28,9 @@ class HeadRouteMacros /** * Register the withHead() macro on every routing surface. * - * Boot-only. These static macros affect every request in the worker. + * These static macros affect every request in the worker. */ + #[BootOnly] public function register(): void { Router::macro('withHead', $this->routerMacro()); diff --git a/src/head/src/Schema/SchemaFactory.php b/src/head/src/Schema/SchemaFactory.php index 6a30ca7cc6..9a454d1562 100644 --- a/src/head/src/Schema/SchemaFactory.php +++ b/src/head/src/Schema/SchemaFactory.php @@ -4,6 +4,7 @@ namespace Hypervel\Head\Schema; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; use ReflectionClass; @@ -50,10 +51,11 @@ public function __construct() /** * Register a first-class schema object type. * - * Boot-only. Registered types affect every request using this shared factory. + * Registered types affect every request using this shared factory. * * @param class-string $class */ + #[BootOnly(whenShared: true)] public function register(string $class): static { $this->types[Str::camel((new ReflectionClass($class))->getShortName())] = $class; diff --git a/src/head/src/TagRegistry.php b/src/head/src/TagRegistry.php index 95caf73433..d5883b2603 100644 --- a/src/head/src/TagRegistry.php +++ b/src/head/src/TagRegistry.php @@ -18,6 +18,7 @@ use Hypervel\Head\Tags\TagBuilder; use Hypervel\Head\Tags\Title; use Hypervel\Head\Tags\Twitter; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; class TagRegistry @@ -41,10 +42,11 @@ public function builders(): array /** * Register a custom tag builder. * - * Boot-only. Registered builders affect every request using this shared registry. + * Registered builders affect every request using this shared registry. * * @param class-string $builder */ + #[BootOnly(whenShared: true)] public function extend(string $builder): static { if (! is_subclass_of($builder, TagBuilder::class)) { diff --git a/src/head/src/Tags/Canonical.php b/src/head/src/Tags/Canonical.php index 74fa464958..be668aa53d 100644 --- a/src/head/src/Tags/Canonical.php +++ b/src/head/src/Tags/Canonical.php @@ -8,6 +8,7 @@ use Hypervel\Head\Rendering\ResolvedHead; use Hypervel\Head\Rendering\TagRenderer; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; /** * @phpstan-consistent-constructor @@ -163,10 +164,11 @@ private static function routeAttributeUrl(array $attributes): mixed /** * Register a resolver for automatic and relative canonical origins. * - * Boot-only. The callback persists in a static property and affects every request in the worker. + * The callback persists in a static property and affects every request in the worker. * * @param null|(Closure(): ?string) $resolver */ + #[BootOnly] public static function resolveOriginUsing(?Closure $resolver): void { static::$originResolver = $resolver; diff --git a/src/horizon/composer.json b/src/horizon/composer.json index ed880e64f6..9b23f18a80 100644 --- a/src/horizon/composer.json +++ b/src/horizon/composer.json @@ -28,6 +28,7 @@ "ext-pcntl": "*", "ext-posix": "*", "ext-redis": "^6.1", + "hypervel/attributes": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/bus": "^0.4", "hypervel/cache": "^0.4", diff --git a/src/horizon/src/Horizon.php b/src/horizon/src/Horizon.php index fd4a3583d2..567fb4b3f9 100644 --- a/src/horizon/src/Horizon.php +++ b/src/horizon/src/Horizon.php @@ -10,6 +10,7 @@ use Hypervel\Foundation\DevCommands; use Hypervel\Http\Request; use Hypervel\Redis\RedisConnection; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\HtmlString; use Hypervel\Support\Js; use RuntimeException; @@ -67,10 +68,10 @@ public static function check(?Request $request): bool /** * Set the callback that should be used to authenticate Horizon users. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every Horizon dashboard request across all - * coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every Horizon dashboard request across all coroutines. */ + #[BootOnly] public static function auth(Closure $callback): static { static::$authUsing = $callback; @@ -81,11 +82,12 @@ public static function auth(Closure $callback): static /** * Configure the Redis databases that will store Horizon data. * - * Boot-only. Mutates process-global config (database.redis.horizon); + * Mutates process-global config (database.redis.horizon); * per-request use races across coroutines. * * @throws Exception */ + #[BootOnly] public static function use(string $connection): void { $config = config("database.redis.{$connection}"); @@ -174,9 +176,10 @@ public static function scriptVariables(): array /** * Specify the email address to which email notifications should be routed. * - * Boot-only. The address persists in a static property for the worker - * lifetime and is used by every Horizon notification dispatch. + * The address persists in a static property for the worker lifetime + * and is used by every Horizon notification dispatch. */ + #[BootOnly] public static function routeMailNotificationsTo(string $email): static { static::$email = $email; @@ -187,9 +190,10 @@ public static function routeMailNotificationsTo(string $email): static /** * Specify the webhook URL and channel to which Slack notifications should be routed. * - * Boot-only. The URL and channel persist in static properties for the - * worker lifetime and are used by every Horizon notification dispatch. + * The URL and channel persist in static properties for the worker + * lifetime and are used by every Horizon notification dispatch. */ + #[BootOnly] public static function routeSlackNotificationsTo(string $url, ?string $channel = null): static { static::$slackWebhookUrl = $url; @@ -201,9 +205,10 @@ public static function routeSlackNotificationsTo(string $url, ?string $channel = /** * Specify the phone number to which SMS notifications should be routed. * - * Boot-only. The number persists in a static property for the worker - * lifetime and is used by every Horizon notification dispatch. + * The number persists in a static property for the worker lifetime + * and is used by every Horizon notification dispatch. */ + #[BootOnly] public static function routeSmsNotificationsTo(string $number): static { static::$smsNumber = $number; @@ -230,9 +235,10 @@ public static function cspNonce(string $nonce): static /** * Register the Horizon development commands. * - * Boot-only. The registrations persist for the worker lifetime and affect - * every subsequent development command invocation. + * The registrations persist for the worker lifetime and affect every + * subsequent development command invocation. */ + #[BootOnly] public static function registerDevCommands(): void { DevCommands::artisan('horizon', 'horizon'); diff --git a/src/horizon/src/MasterSupervisor.php b/src/horizon/src/MasterSupervisor.php index 937d946741..35214f5a10 100644 --- a/src/horizon/src/MasterSupervisor.php +++ b/src/horizon/src/MasterSupervisor.php @@ -15,6 +15,7 @@ use Hypervel\Horizon\Contracts\SupervisorRepository; use Hypervel\Horizon\Contracts\Terminable; use Hypervel\Horizon\Events\MasterSupervisorLooped; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Str; @@ -103,9 +104,10 @@ public static function basename(): string /** * Use the given callback to resolve master supervisor names. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every master-name lookup. + * The resolver persists in a static property for the worker lifetime + * and runs on every master-name lookup. */ + #[BootOnly] public static function determineNameUsing(Closure $callback): void { static::$nameResolver = $callback; diff --git a/src/http-server/composer.json b/src/http-server/composer.json index 61ea808899..5208d2c643 100644 --- a/src/http-server/composer.json +++ b/src/http-server/composer.json @@ -32,6 +32,7 @@ "require": { "php": "^8.4", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", "hypervel/coordinator": "^0.4", diff --git a/src/http-server/src/Server.php b/src/http-server/src/Server.php index 6eff21c1f4..b677b49f12 100644 --- a/src/http-server/src/Server.php +++ b/src/http-server/src/Server.php @@ -18,6 +18,7 @@ use Hypervel\HttpServer\Events\RequestReceived; use Hypervel\HttpServer\Events\RequestTerminated; use Hypervel\HttpServer\Events\ResponseSent; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\FailureReporter; use Swoole\Coroutine\CanceledException; use Swoole\Http\Request as SwooleRequest; @@ -257,12 +258,13 @@ public function getServerName(): string /** * Set the server name. * - * Boot-only. Mutates the worker-lifetime request handler before Swoole - * starts; runtime use races across requests and changes emitted server + * Mutates the worker-lifetime request handler before Swoole starts; + * runtime use races across requests and changes emitted server * context. * * @return $this */ + #[BootOnly(whenShared: true)] public function setServerName(string $serverName): static { $this->serverName = $serverName; diff --git a/src/http/composer.json b/src/http/composer.json index ee510cded8..8499b97390 100644 --- a/src/http/composer.json +++ b/src/http/composer.json @@ -30,6 +30,7 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/psr7": "^3.1", "guzzlehttp/uri-template": "^1.0 || ^2.0", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/container": "^0.4", diff --git a/src/http/src/Client/CurlStreamingBody.php b/src/http/src/Client/CurlStreamingBody.php index 754c37d4be..d4be73f54f 100644 --- a/src/http/src/Client/CurlStreamingBody.php +++ b/src/http/src/Client/CurlStreamingBody.php @@ -19,6 +19,7 @@ use Hypervel\Database\ConnectionResolver; use Hypervel\Engine\Coroutine; use Hypervel\ObjectPool\PoolErrorReporter; +use Hypervel\Support\Attributes\Unsupported; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; @@ -511,6 +512,7 @@ public function isSeekable(): bool /** * Reject seeking on an incremental response. */ + #[Unsupported] public function seek(int $offset, int $whence = SEEK_SET): void { throw new RuntimeException('Cannot seek a streaming response.'); @@ -519,6 +521,7 @@ public function seek(int $offset, int $whence = SEEK_SET): void /** * Reject rewinding an incremental response. */ + #[Unsupported] public function rewind(): void { $this->seek(0); @@ -535,6 +538,7 @@ public function isWritable(): bool /** * Reject writes to the response body. */ + #[Unsupported] public function write(string $string): int { throw new RuntimeException('Cannot write to a streaming response.'); diff --git a/src/http/src/Client/Factory.php b/src/http/src/Client/Factory.php index 713bc19c1a..57b1e7c125 100644 --- a/src/http/src/Client/Factory.php +++ b/src/http/src/Client/Factory.php @@ -22,6 +22,9 @@ use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Http\Client\Guzzle\HandlerFactory; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Hypervel\Support\Stringable; @@ -110,11 +113,12 @@ public function __construct(protected ?Dispatcher $dispatcher = null) /** * Add middleware to apply to every request. * - * Boot-only. The middleware persists on the factory for the worker lifetime - * and affects every subsequently created pending request. + * The middleware persists on the shared factory for the worker lifetime and + * affects every subsequently created pending request. * * @return $this */ + #[BootOnly(whenShared: true)] public function globalMiddleware(callable $middleware): static { $this->globalMiddleware[] = $middleware; @@ -125,11 +129,12 @@ public function globalMiddleware(callable $middleware): static /** * Add request middleware to apply to every request. * - * Boot-only. The middleware persists on the factory for the worker lifetime - * and affects every subsequently created pending request. + * The middleware persists on the shared factory for the worker lifetime and + * affects every subsequently created pending request. * * @return $this */ + #[BootOnly(whenShared: true)] public function globalRequestMiddleware(callable $middleware): static { $this->globalMiddleware[] = Middleware::mapRequest($middleware); @@ -140,11 +145,12 @@ public function globalRequestMiddleware(callable $middleware): static /** * Add response middleware to apply to every request. * - * Boot-only. The middleware persists on the factory for the worker lifetime - * and affects every subsequently created pending request. + * The middleware persists on the shared factory for the worker lifetime and + * affects every subsequently created pending request. * * @return $this */ + #[BootOnly(whenShared: true)] public function globalResponseMiddleware(callable $middleware): static { $this->globalMiddleware[] = Middleware::mapResponse($middleware); @@ -155,11 +161,12 @@ public function globalResponseMiddleware(callable $middleware): static /** * Set the options to apply to every request. * - * Boot-only. The options persist on the factory for the worker lifetime - * and affect every subsequently created pending request. + * The options persist on the shared factory for the worker lifetime and + * affect every subsequently created pending request. * * @return $this */ + #[BootOnly(whenShared: true)] public function globalOptions(array|Closure $options): static { $this->globalOptions = $options; @@ -326,8 +333,9 @@ public static function failedConnection(?string $message = null): Closure /** * Get an invokable object that returns a sequence of responses in order for use during stubbing. * - * Tests only. The sequence is retained by the factory for the worker lifetime. + * The sequence is retained by the shared factory for the worker lifetime. */ + #[TestsOnly(whenShared: true)] public function sequence(array $responses = []): ResponseSequence { return $this->responseSequences[] = new ResponseSequence($responses); @@ -336,11 +344,12 @@ public function sequence(array $responses = []): ResponseSequence /** * Register a stub callable that will intercept requests and be able to return stub responses. * - * Tests only. Stubs persist on the factory for the worker lifetime - * and affect every subsequently created pending request. + * Stubs persist on the shared factory for the worker lifetime and affect + * every subsequently created pending request. * * @return $this */ + #[TestsOnly(whenShared: true)] public function fake(array|callable|null $callback = null): static { $this->record(); @@ -389,8 +398,10 @@ function ($request, $options) use ($callback) { /** * Register a response sequence for the given URL pattern. * - * Tests only. The sequence and stub persist on the factory for the worker lifetime. + * The sequence and stub persist on the shared factory for the worker + * lifetime. */ + #[TestsOnly(whenShared: true)] public function fakeSequence(string $url = '*'): ResponseSequence { return tap($this->sequence(), function ($sequence) use ($url) { @@ -401,13 +412,14 @@ public function fakeSequence(string $url = '*'): ResponseSequence /** * Stub the given URL using the given callback. * - * Tests only. The stub persists on the factory for the worker lifetime - * and affects every subsequently created pending request. + * The stub persists on the shared factory for the worker lifetime and + * affects every subsequently created pending request. * * @return $this * * @throws InvalidArgumentException */ + #[TestsOnly(whenShared: true)] public function stubUrl(string $url, array|callable|int|PromiseInterface|Response|string $callback): static { return $this->fake(function ($request, $options) use ($url, $callback) { @@ -441,11 +453,12 @@ public function stubUrl(string $url, array|callable|int|PromiseInterface|Respons /** * Indicate that an exception should be thrown if any request is not faked. * - * Boot or tests only. The policy persists on the factory for the worker lifetime - * and affects every subsequently created pending request. + * The policy persists on the shared factory for the worker lifetime and + * affects every subsequently created pending request. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function preventStrayRequests(bool $prevent = true): static { $this->preventStrayRequests = $prevent; @@ -464,11 +477,12 @@ public function preventingStrayRequests(): bool /** * Indicate that an exception should not be thrown if any request is not faked. * - * Boot or tests only. The policy persists on the factory for the worker lifetime - * and affects every subsequently created pending request. + * The policy persists on the shared factory for the worker lifetime and + * affects every subsequently created pending request. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function allowStrayRequests(?array $only = null): static { if (is_null($only)) { @@ -484,11 +498,12 @@ public function allowStrayRequests(?array $only = null): static /** * Begin recording request / response pairs. * - * Tests only. Recording persists on the factory for the worker lifetime - * and retains every subsequently completed request attempt. + * Recording persists on the shared factory for the worker lifetime and + * retains every subsequently completed request attempt. * * @return $this */ + #[TestsOnly(whenShared: true)] public function record(): static { $this->recording = true; @@ -648,9 +663,10 @@ public function getDispatcher(): ?Dispatcher /** * Set the event dispatcher implementation. * - * Boot or tests only. The dispatcher persists on the factory for the - * worker lifetime and receives every subsequent HTTP client event. + * The dispatcher persists on the shared factory for the worker lifetime and + * receives every subsequent HTTP client event. */ + #[BootOrTestsOnly(whenShared: true)] public function setDispatcher(?Dispatcher $dispatcher): void { $this->dispatcher = $dispatcher; @@ -669,11 +685,12 @@ public function getGlobalMiddleware(): array /** * Register a connection with the given name and configuration. * - * Boot-only. The connection preset and shared transport handler registry - * persist on the factory for the worker lifetime. + * The connection preset and shared transport handler registry persist on + * the shared factory for the worker lifetime. * * @return $this */ + #[BootOnly(whenShared: true)] public function registerConnection(string $name, array $config = []): static { return $this->setConnectionConfig($name, $config); @@ -796,11 +813,12 @@ public function getConnectionOptions(string $name): array /** * Set the configuration for a specific connection. * - * Boot-only. Reconfiguration replaces the worker-lifetime preset and + * Reconfiguration replaces the worker-lifetime preset and * invalidates its shared transport handler for subsequent requests. * * @return $this */ + #[BootOnly(whenShared: true)] public function setConnectionConfig(string $name, array $config): static { $this->validateConnectionConfig($config); @@ -814,11 +832,12 @@ public function setConnectionConfig(string $name, array $config): static /** * Forget the resolved default and connection handlers. * - * Boot or tests only. Request-time use forces subsequent requests to - * rebuild warmed keep-alive, DNS, and TLS session state. + * Request-time use forces subsequent requests to rebuild warmed + * keep-alive, DNS, and TLS session state. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetConnectionHandlers(): static { $this->connectionHandlers = []; diff --git a/src/http/src/Client/Guzzle/CoroutineTaskQueue.php b/src/http/src/Client/Guzzle/CoroutineTaskQueue.php index 5c59f2c914..9fbca080d1 100644 --- a/src/http/src/Client/Guzzle/CoroutineTaskQueue.php +++ b/src/http/src/Client/Guzzle/CoroutineTaskQueue.php @@ -10,6 +10,8 @@ use GuzzleHttp\Promise\TaskQueueInterface; use GuzzleHttp\Promise\Utils; use Hypervel\Engine\Coroutine; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; /** * Drain each coroutine's callbacks independently and cancel unfinished work at exit, @@ -27,8 +29,9 @@ public function __construct(protected TaskQueueInterface $outside) /** * Install the routing queue once without replacing an existing installation. * - * Boot or tests only. Guzzle's queue is process-global; install before scheduling work. + * Guzzle's queue is process-global; install before scheduling work. */ + #[BootOrTestsOnly] public static function install(): void { $queue = Utils::queue(); @@ -73,8 +76,9 @@ public function track(PromiseInterface $promise): void /** * Discard outside-coroutine test tasks without retaining a shutdown callback. * - * Tests only. The PHPUnit extension seeds a shutdown-disabled delegate before installation. + * The PHPUnit extension seeds a shutdown-disabled delegate before installation. */ + #[TestsOnly(whenShared: true)] public function resetOutside(): void { $this->outside = new TaskQueue(false); diff --git a/src/http/src/Client/RequestException.php b/src/http/src/Client/RequestException.php index 5aabca0d1e..6d538021c8 100644 --- a/src/http/src/Client/RequestException.php +++ b/src/http/src/Client/RequestException.php @@ -5,6 +5,7 @@ namespace Hypervel\Http\Client; use GuzzleHttp\Psr7\Message; +use Hypervel\Support\Attributes\BootOnly; class RequestException extends HttpClientException { @@ -43,9 +44,10 @@ public function __construct( /** * Enable truncation of request exception messages. * - * Boot-only. The global default persists for the worker lifetime across all + * The global default persists for the worker lifetime across all * coroutines; per-request truncation settings take precedence. */ + #[BootOnly] public static function truncate(): void { static::$truncateAt = self::DEFAULT_TRUNCATE_AT; @@ -54,9 +56,10 @@ public static function truncate(): void /** * Set the truncation length for request exception messages. * - * Boot-only. The global default persists for the worker lifetime across all + * The global default persists for the worker lifetime across all * coroutines; per-request truncation settings take precedence. */ + #[BootOnly] public static function truncateAt(int $length): void { static::$truncateAt = $length; @@ -65,9 +68,10 @@ public static function truncateAt(int $length): void /** * Disable truncation of request exception messages. * - * Boot-only. The global default persists for the worker lifetime across all + * The global default persists for the worker lifetime across all * coroutines; per-request truncation settings take precedence. */ + #[BootOnly] public static function dontTruncate(): void { static::$truncateAt = false; diff --git a/src/http/src/Client/Response.php b/src/http/src/Client/Response.php index 6d92faba1d..3cb8f92ae7 100644 --- a/src/http/src/Client/Response.php +++ b/src/http/src/Client/Response.php @@ -71,8 +71,8 @@ class Response implements ArrayAccess, Stringable /** * The flags passed to `json_decode` by default. * - * Boot-only. Changing this value during request handling affects JSON decoding in every concurrent - * and subsequent request on that worker. + * Setting this during request handling affects JSON decoding in every concurrent and subsequent + * request on that worker. * * @var int-mask */ diff --git a/src/http/src/Middleware/HandleCors.php b/src/http/src/Middleware/HandleCors.php index 633582998b..db671ed5b4 100644 --- a/src/http/src/Middleware/HandleCors.php +++ b/src/http/src/Middleware/HandleCors.php @@ -8,6 +8,7 @@ use Fruitcake\Cors\CorsService; use Hypervel\Contracts\Container\Container; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; use Symfony\Component\HttpFoundation\Response; @@ -115,12 +116,13 @@ protected function getPathsByHost(string $host, array $paths): array /** * Register a closure that resolves CORS configuration for the current request. * - * Boot-only. The closure receives the current request and returns the CORS - * options array; useful for multi-tenant CORS where the config varies by - * host or other request data. Persists for the worker lifetime. + * The closure receives the current request and returns the CORS options + * array; useful for multi-tenant CORS where the config varies by host or + * other request data. Persists for the worker lifetime. * * @param null|Closure(Request): array $callback */ + #[BootOnly] public static function resolveConfigUsing(?Closure $callback): void { static::$configResolver = $callback; @@ -129,9 +131,10 @@ public static function resolveConfigUsing(?Closure $callback): void /** * Register a callback that instructs the middleware to be skipped. * - * Boot-only. Skip callbacks persist for the worker lifetime and run on - * every subsequent request. + * Skip callbacks persist for the worker lifetime and run on every + * subsequent request. */ + #[BootOnly] public static function skipWhen(Closure $callback): void { static::$skipCallbacks[] = $callback; diff --git a/src/http/src/Middleware/TrustHosts.php b/src/http/src/Middleware/TrustHosts.php index d998b7784d..8bbd8f9cc9 100644 --- a/src/http/src/Middleware/TrustHosts.php +++ b/src/http/src/Middleware/TrustHosts.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Contracts\Foundation\Application; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\HttpFoundation\Response; class TrustHosts @@ -102,11 +103,12 @@ protected function resolveTrustedHostPatterns(Request $request): array /** * Specify the hosts that should always be trusted. * - * Boot-only. The list persists in static properties for the worker lifetime - * and applies to every subsequent request. + * The list persists in static properties for the worker lifetime and + * applies to every subsequent request. * * @param array|(callable(): array) $hosts */ + #[BootOnly] public static function at(array|callable $hosts, bool $subdomains = true): void { static::$alwaysTrust = is_callable($hosts) ? $hosts(...) : $hosts; @@ -116,11 +118,12 @@ public static function at(array|callable $hosts, bool $subdomains = true): void /** * Register a closure that resolves trusted hosts for the current request. * - * Boot-only. The callback persists in static state for the worker lifetime - * and fully replaces the static trusted host list for every subsequent request. + * The callback persists in static state for the worker lifetime and fully + * replaces the static trusted host list for every subsequent request. * * @param null|Closure(Request): array $callback */ + #[BootOnly] public static function resolveHostsUsing(?Closure $callback): void { static::$hostsResolver = $callback; diff --git a/src/http/src/Middleware/TrustProxies.php b/src/http/src/Middleware/TrustProxies.php index c5002dcbed..976cb2ead3 100644 --- a/src/http/src/Middleware/TrustProxies.php +++ b/src/http/src/Middleware/TrustProxies.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -147,9 +148,10 @@ protected function proxies(): array|string|null /** * Specify the IP addresses of proxies that should always be trusted. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The list persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function at(array|string $proxies): void { static::$alwaysTrustProxies = $proxies; @@ -158,9 +160,10 @@ public static function at(array|string $proxies): void /** * Specify the proxy headers that should always be trusted. * - * Boot-only. The header set persists in a static property for the worker - * lifetime and applies to every subsequent request. + * The header set persists in a static property for the worker lifetime + * and applies to every subsequent request. */ + #[BootOnly] public static function withHeaders(int $headers): void { static::$alwaysTrustHeaders = $headers; diff --git a/src/http/src/Request.php b/src/http/src/Request.php index 79b97b1d2b..710ef8ba85 100644 --- a/src/http/src/Request.php +++ b/src/http/src/Request.php @@ -12,6 +12,8 @@ use Hypervel\Routing\Route; use Hypervel\Session\SymfonySessionDecorator; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Str; @@ -306,6 +308,7 @@ public function getPathInfo(): string * * @throws RuntimeException always — superglobals don't exist in Swoole workers */ + #[Unsupported('Access the current request through dependency injection or request().')] public static function createFromGlobals(): never { throw new RuntimeException('Request::createFromGlobals() is not supported in Hypervel. Requests are created from Swoole request objects.'); @@ -1322,9 +1325,10 @@ public function setUserResolver(Closure $callback): static /** * Set the default user resolver callback. * - * Boot-only. The callback persists for the worker lifetime and applies to - * every request without an instance-specific user resolver. + * The callback persists for the worker lifetime and applies to every + * request without an instance-specific user resolver. */ + #[BootOnly] public static function setDefaultUserResolver(Closure $callback): void { static::$defaultUserResolver = $callback; diff --git a/src/http/src/Resources/DelegatesToResource.php b/src/http/src/Resources/DelegatesToResource.php index d75b06f2e1..124ba1844c 100644 --- a/src/http/src/Resources/DelegatesToResource.php +++ b/src/http/src/Resources/DelegatesToResource.php @@ -6,6 +6,7 @@ use Exception; use Hypervel\Database\Eloquent\Model; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Traits\ForwardsCalls; use Hypervel\Support\Traits\Macroable; @@ -36,6 +37,7 @@ public function getRouteKeyName(): string * * @throws Exception */ + #[Unsupported] public function resolveRouteBinding(mixed $value, ?string $field = null): never { throw new Exception('Resources may not be implicitly resolved from route bindings.'); @@ -46,6 +48,7 @@ public function resolveRouteBinding(mixed $value, ?string $field = null): never * * @throws Exception */ + #[Unsupported] public function resolveChildRouteBinding(string $childType, mixed $value, ?string $field = null): ?Model { throw new Exception('Resources may not be implicitly resolved from child route bindings.'); diff --git a/src/http/src/Resources/Json/JsonResource.php b/src/http/src/Resources/Json/JsonResource.php index 139cc3b425..be64338347 100644 --- a/src/http/src/Resources/Json/JsonResource.php +++ b/src/http/src/Resources/Json/JsonResource.php @@ -15,6 +15,7 @@ use Hypervel\Http\Resources\Attributes\PreserveKeys; use Hypervel\Http\Resources\ConditionallyLoadsAttributes; use Hypervel\Http\Resources\DelegatesToResource; +use Hypervel\Support\Attributes\BootOnly; use JsonException; use JsonSerializable; use ReflectionClass; @@ -217,10 +218,10 @@ protected function resolveRequestFromContainer(): Request /** * Set the string that should wrap the outer-most resource array. * - * Boot-only. The wrap value persists in a static property for the worker - * lifetime and applies to every resource serialization across all - * coroutines. + * The wrap value persists in a static property for the worker lifetime + * and applies to every resource serialization across all coroutines. */ + #[BootOnly] public static function wrap(string $value): void { static::$wrap = $value; @@ -229,10 +230,10 @@ public static function wrap(string $value): void /** * Disable wrapping of the outer-most resource array. * - * Boot-only. The wrap value persists in a static property for the worker - * lifetime and applies to every resource serialization across all - * coroutines. + * The wrap value persists in a static property for the worker lifetime + * and applies to every resource serialization across all coroutines. */ + #[BootOnly] public static function withoutWrapping(): void { static::$wrap = null; diff --git a/src/http/src/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php b/src/http/src/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php index 08f00a6af4..286174f8c6 100644 --- a/src/http/src/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php +++ b/src/http/src/Resources/JsonApi/Concerns/ResolvesJsonApiElements.php @@ -18,6 +18,7 @@ use Hypervel\Http\Resources\JsonApi\RelationResolver; use Hypervel\Http\Resources\MissingValue; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\LazyCollection; use Hypervel\Support\Str; @@ -78,11 +79,12 @@ trait ResolvesJsonApiElements /** * Specify the maximum relationship depth. * - * Boot-only. The depth limit persists in a static property for the worker - * lifetime and affects every subsequent JSON:API resource resolution. + * The depth limit persists in a static property for the worker lifetime + * and affects every subsequent JSON:API resource resolution. * * @param non-negative-int $depth */ + #[BootOnly] public static function maxRelationshipDepth(int $depth): void { static::$maxRelationshipDepth = max(0, $depth); diff --git a/src/http/src/Resources/JsonApi/JsonApiResource.php b/src/http/src/Resources/JsonApi/JsonApiResource.php index 02b9de895d..20a5052bea 100644 --- a/src/http/src/Resources/JsonApi/JsonApiResource.php +++ b/src/http/src/Resources/JsonApi/JsonApiResource.php @@ -10,6 +10,8 @@ use Hypervel\Http\JsonResponse; use Hypervel\Http\Request; use Hypervel\Http\Resources\Json\JsonResource; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Collection; use JsonSerializable; use Override; @@ -49,9 +51,10 @@ class JsonApiResource extends JsonResource /** * Set the JSON:API version for the request. * - * Boot-only. The configuration persists in a static property for the worker - * lifetime and applies to every JSON:API response across all coroutines. + * The configuration persists in a static property for the worker lifetime + * and applies to every JSON:API response across all coroutines. */ + #[BootOnly] public static function configure(?string $version = null, array $ext = [], array $profile = [], array $meta = []): void { static::$jsonApiInformation = array_filter([ @@ -225,6 +228,7 @@ protected static function newCollection(mixed $resource): AnonymousResourceColle * @throws BadMethodCallException */ #[Override] + #[Unsupported] public static function wrap(string $value): never { throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); @@ -236,6 +240,7 @@ public static function wrap(string $value): never * @throws BadMethodCallException */ #[Override] + #[Unsupported] public static function withoutWrapping(): never { throw new BadMethodCallException(sprintf('Using %s() method is not allowed.', __METHOD__)); diff --git a/src/http/src/Response.php b/src/http/src/Response.php index 92b978f5c8..eab6f80dcd 100755 --- a/src/http/src/Response.php +++ b/src/http/src/Response.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Support\Arrayable; use Hypervel\Contracts\Support\Jsonable; use Hypervel\Contracts\Support\Renderable; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Traits\Macroable; use InvalidArgumentException; use JsonSerializable; @@ -156,6 +157,7 @@ protected function morphToJson(mixed $content): string|false * @throws RuntimeException always — Swoole manages headers via its own API */ #[Override] + #[Unsupported('Return the response from your route or controller so the server can send it.')] public function sendHeaders(?int $statusCode = null): never { throw new RuntimeException('Response::sendHeaders() is not supported in Hypervel. Responses are emitted through Swoole\'s response API.'); @@ -167,6 +169,7 @@ public function sendHeaders(?int $statusCode = null): never * @throws RuntimeException always — Swoole has no SAPI output stream */ #[Override] + #[Unsupported('Return the response from your route or controller so the server can send it.')] public function sendContent(): never { throw new RuntimeException('Response::sendContent() is not supported in Hypervel. Responses are emitted through Swoole\'s response API.'); @@ -178,6 +181,7 @@ public function sendContent(): never * @throws RuntimeException always — Swoole manages response emission */ #[Override] + #[Unsupported('Return the response from your route or controller so the server can send it.')] public function send(bool $flush = true): never { throw new RuntimeException('Response::send() is not supported in Hypervel. Responses are emitted through Swoole\'s response API.'); diff --git a/src/image/composer.json b/src/image/composer.json index aa88dfc8ec..06d2fc8596 100644 --- a/src/image/composer.json +++ b/src/image/composer.json @@ -31,6 +31,7 @@ "require": { "php": "^8.4", "ext-fileinfo": "*", + "hypervel/attributes": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/container": "^0.4", "hypervel/contracts": "^0.4", diff --git a/src/image/src/Drivers/InterventionDriver.php b/src/image/src/Drivers/InterventionDriver.php index f41ca43391..459ae854de 100644 --- a/src/image/src/Drivers/InterventionDriver.php +++ b/src/image/src/Drivers/InterventionDriver.php @@ -22,6 +22,7 @@ use Hypervel\Image\Transformations\Rotate; use Hypervel\Image\Transformations\Scale; use Hypervel\Image\Transformations\Sharpen; +use Hypervel\Support\Attributes\BootOnly; use Intervention\Image\Direction; use Intervention\Image\Encoders\AvifEncoder; use Intervention\Image\Encoders\BmpEncoder; @@ -208,10 +209,11 @@ protected function dominantColorFrom(ImageInterface $image): string /** * Register a transformation handler. * - * Boot-only. The handler persists on this cached driver for the worker lifetime and affects every subsequent image processed by it. + * On a cached driver, the handler persists for the worker lifetime and affects every subsequent image processed by it. * * @param class-string $transformation */ + #[BootOnly(whenShared: true)] public function transformUsing(string $transformation, callable $callback): static { $this->transformationHandlers[$transformation] = $callback; diff --git a/src/image/src/ImageManager.php b/src/image/src/ImageManager.php index b929f8cc75..adafded441 100644 --- a/src/image/src/ImageManager.php +++ b/src/image/src/ImageManager.php @@ -12,6 +12,7 @@ use Hypervel\Http\UploadedFile; use Hypervel\Image\Drivers\GdDriver; use Hypervel\Image\Drivers\ImagickDriver; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Manager; use InvalidArgumentException; use UnitEnum; @@ -148,10 +149,11 @@ protected function createImagickDriver(): ImagickDriver /** * Register a transformation handler for the given driver. * - * Boot-only. The handler persists on a cached driver for the worker lifetime and affects every subsequent image processed by that driver. + * On the shared manager, the handler persists on a cached driver for the worker lifetime and affects every subsequent image processed by that driver. * * @param class-string $transformation */ + #[BootOnly(whenShared: true)] public function transformUsing(string $driver, string $transformation, callable $callback): static { $this->transformationHandlers[$driver][$transformation] = $callback; diff --git a/src/inertia/composer.json b/src/inertia/composer.json index cf26ceeb37..9c079c238e 100644 --- a/src/inertia/composer.json +++ b/src/inertia/composer.json @@ -33,9 +33,7 @@ }, "require": { "php": "^8.4", - "symfony/console": "^8.1.10", - "symfony/http-foundation": "^8.1.11", - "symfony/process": "^8.1", + "hypervel/attributes": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", @@ -53,7 +51,10 @@ "hypervel/session": "^0.4", "hypervel/support": "^0.4", "hypervel/testing": "^0.4", - "hypervel/view": "^0.4" + "hypervel/view": "^0.4", + "symfony/console": "^8.1.10", + "symfony/http-foundation": "^8.1.11", + "symfony/process": "^8.1" }, "config": { "sort-packages": true diff --git a/src/inertia/src/DevTools/EntryStore.php b/src/inertia/src/DevTools/EntryStore.php index 394df9a963..7d324776d1 100644 --- a/src/inertia/src/DevTools/EntryStore.php +++ b/src/inertia/src/DevTools/EntryStore.php @@ -6,6 +6,7 @@ use Hypervel\Coroutine\Coroutine; use Hypervel\Inertia\DevTools\Data\IncomingEntry; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Facades\Log; use Throwable; @@ -106,9 +107,10 @@ public function flush(EntriesRepository $repo): void /** * Reset the circuit breaker so recording resumes immediately. * - * Tests only. The breaker is shared by every request in the worker, so a reset during a - * request resumes writes for all of them while storage may still be failing. + * The breaker is shared by every request in the worker, so a reset during a request + * resumes writes for all of them while storage may still be failing. */ + #[TestsOnly] public static function resetCircuitBreaker(): void { static::$suppressedUntil = null; diff --git a/src/inertia/src/ResponseFactory.php b/src/inertia/src/ResponseFactory.php index 6780213efe..95893256b1 100644 --- a/src/inertia/src/ResponseFactory.php +++ b/src/inertia/src/ResponseFactory.php @@ -22,6 +22,7 @@ use Hypervel\Inertia\Support\SessionKey; use Hypervel\Routing\Router; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\App; use Hypervel\Support\Facades\Redirect; use Hypervel\Support\Facades\Request; @@ -403,9 +404,10 @@ public function location(string|SymfonyRedirectResponse $url): SymfonyResponse /** * Register a callback to handle HTTP exceptions for Inertia requests. * - * Boot-only. The callback is stored on the exception handler singleton - * and applies to all requests for the worker lifetime. + * The callback is stored on the exception handler singleton and + * applies to all requests for the worker lifetime. */ + #[BootOnly] public function handleExceptionsUsing(callable $callback): void { /** @var mixed $handler */ diff --git a/src/jwt/composer.json b/src/jwt/composer.json index 834c144652..8820fb1938 100644 --- a/src/jwt/composer.json +++ b/src/jwt/composer.json @@ -25,8 +25,7 @@ "require": { "php": "^8.4", "ext-openssl": "*", - "lcobucci/jwt": "^5.6", - "nesbot/carbon": "^3.13.1", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", @@ -39,6 +38,8 @@ "hypervel/http": "^0.4", "hypervel/macroable": "^0.4", "hypervel/support": "^0.4", + "lcobucci/jwt": "^5.6", + "nesbot/carbon": "^3.13.1", "symfony/console": "^8.1.10", "vlucas/phpdotenv": "^5.6.1" }, diff --git a/src/jwt/src/Blacklist.php b/src/jwt/src/Blacklist.php index eb97eb7fdc..4f60be67ef 100644 --- a/src/jwt/src/Blacklist.php +++ b/src/jwt/src/Blacklist.php @@ -8,6 +8,7 @@ use Hypervel\Jwt\Contracts\BlacklistContract; use Hypervel\Jwt\Contracts\StorageContract; use Hypervel\Jwt\Exceptions\TokenInvalidException; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Date; class Blacklist implements BlacklistContract @@ -147,11 +148,12 @@ protected function getGraceTimestamp(): int /** * Set the grace period. * - * Boot-only. Blacklist is a worker-lifetime singleton; runtime mutation - * affects every subsequent request and races across coroutines. + * On the shared blacklist, runtime mutation affects every subsequent + * request and races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setGracePeriod(int $gracePeriod): static { $this->gracePeriod = $gracePeriod; @@ -188,11 +190,12 @@ public function getKey(array $payload): string /** * Set the unique key held within the blacklist. * - * Boot-only. Blacklist is a worker-lifetime singleton; runtime mutation - * affects every subsequent request and races across coroutines. + * On the shared blacklist, runtime mutation affects every subsequent + * request and races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setKey(string $key): static { $this->key = $key; @@ -203,11 +206,12 @@ public function setKey(string $key): static /** * Set the refresh time limit. * - * Boot-only. Blacklist is a worker-lifetime singleton; runtime mutation - * affects every subsequent request and races across coroutines. + * On the shared blacklist, runtime mutation affects every subsequent + * request and races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setRefreshTTL(?int $ttl): static { $this->refreshTTL = $ttl; diff --git a/src/jwt/src/JwtGuard.php b/src/jwt/src/JwtGuard.php index f47eeb892f..5ee96b45ec 100644 --- a/src/jwt/src/JwtGuard.php +++ b/src/jwt/src/JwtGuard.php @@ -26,6 +26,7 @@ use Hypervel\Jwt\Exceptions\TokenInvalidException; use Hypervel\Jwt\Exceptions\UserNotDefinedException; use Hypervel\Jwt\Http\Parser\Parser; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Timebox; use Hypervel\Support\Traits\Macroable; use SensitiveParameter; @@ -38,8 +39,6 @@ class JwtGuard implements Guard public const int DEFAULT_TTL = 120; - protected const string GUARD_CONTEXT_KEY_PREFIX = '__auth.guards.'; - private const string NO_EXPIRY = '__jwt.ttl.no_expiry'; /** @@ -311,7 +310,7 @@ public function getUserId(): int|string|null */ public function claims(array $claims): static { - $contextKey = $this->getContextStateKey('claims'); + $contextKey = $this->contextKey('claims'); $existing = CoroutineContext::get($contextKey, []); CoroutineContext::set($contextKey, array_merge($existing, $claims)); @@ -502,7 +501,7 @@ public function forgetUser(): static { CoroutineContext::forget($this->getExplicitUserContextKey()); RequestContext::forgetValue($this->getUserContextKey()); - RequestContext::forgetValue($this->getContextStateKey('user.default')); + RequestContext::forgetValue($this->contextKey('user.default')); return $this; } @@ -522,8 +521,8 @@ public function getAuthContextKeys(): array /** * Register an authentication attempt event listener. * - * 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. */ public function attempting(callable $callback): void { @@ -541,9 +540,10 @@ public function getDispatcher(): ?Dispatcher /** * Set the event dispatcher instance. * - * Boot or tests only. The dispatcher is stored on the worker-lifetime guard - * and affects every subsequent request. + * The dispatcher is stored on the worker-lifetime guard and affects every + * subsequent request. */ + #[BootOrTestsOnly(whenShared: true)] public function setDispatcher(Dispatcher $events): void { $this->events = $events; @@ -591,7 +591,7 @@ protected function cacheUser(AuthenticatableContract $user): void */ protected function getExplicitUserContextKey(): string { - return $this->getContextStateKey('user.explicit'); + return $this->contextKey('user.explicit'); } /** @@ -599,7 +599,7 @@ protected function getExplicitUserContextKey(): string */ protected function pullCustomClaims(): array { - $contextKey = $this->getContextStateKey('claims'); + $contextKey = $this->contextKey('claims'); $claims = CoroutineContext::get($contextKey, []); CoroutineContext::forget($contextKey); @@ -625,7 +625,7 @@ protected function requireToken(): string */ protected function getContextState(string $key, mixed $default = null): mixed { - return CoroutineContext::get($this->getContextStateKey($key), $default); + return CoroutineContext::get($this->contextKey($key), $default); } /** @@ -633,7 +633,7 @@ protected function getContextState(string $key, mixed $default = null): mixed */ protected function setContextState(string $key, mixed $value): void { - CoroutineContext::set($this->getContextStateKey($key), $value); + CoroutineContext::set($this->contextKey($key), $value); } /** @@ -641,15 +641,7 @@ protected function setContextState(string $key, mixed $value): void */ protected function forgetContextState(string $key): void { - CoroutineContext::forget($this->getContextStateKey($key)); - } - - /** - * Get a Context state key. - */ - protected function getContextStateKey(string $key): string - { - return static::GUARD_CONTEXT_KEY_PREFIX . $this->name . '.' . $key; + CoroutineContext::forget($this->contextKey($key)); } /** @@ -660,10 +652,10 @@ protected function getUserContextKey(?string $token = null): string $token ??= $this->getToken(); if ($token === null || $token === '') { - return $this->getContextStateKey('user.default'); + return $this->contextKey('user.default'); } - return $this->getContextStateKey('user.' . hash('xxh128', $token)); + return $this->contextKey('user.' . hash('xxh128', $token)); } /** @@ -671,7 +663,7 @@ protected function getUserContextKey(?string $token = null): string */ protected function getPayloadContextKey(string $token): string { - return $this->getContextStateKey('payload.' . hash('xxh128', $token)); + return $this->contextKey('payload.' . hash('xxh128', $token)); } /** diff --git a/src/jwt/src/Providers/Provider.php b/src/jwt/src/Providers/Provider.php index f9d62a8d5b..dd6553c791 100644 --- a/src/jwt/src/Providers/Provider.php +++ b/src/jwt/src/Providers/Provider.php @@ -5,6 +5,7 @@ namespace Hypervel\Jwt\Providers; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use SensitiveParameter; abstract class Provider @@ -42,11 +43,12 @@ public function __construct( /** * Set the algorithm used to sign the token. * - * Boot-only. Providers are cached on the singleton JwtManager; runtime - * mutation affects every subsequent request and races across coroutines. + * On a provider cached by the shared JwtManager, runtime mutation + * affects every subsequent request and races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setAlgo(string $algo): static { $this->algo = $algo; @@ -66,11 +68,12 @@ public function getAlgo(): string /** * Set the secret used to sign the token. * - * Boot-only. Providers are cached on the singleton JwtManager; runtime - * mutation affects every subsequent request and races across coroutines. + * On a provider cached by the shared JwtManager, runtime mutation + * affects every subsequent request and races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setSecret(#[SensitiveParameter] string $secret): static { $this->secret = $secret; @@ -90,11 +93,12 @@ public function getSecret(): string /** * Set the keys used to sign the token. * - * Boot-only. Providers are cached on the singleton JwtManager; runtime - * mutation affects every subsequent request and races across coroutines. + * On a provider cached by the shared JwtManager, runtime mutation + * affects every subsequent request and races across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setKeys(#[SensitiveParameter] array $keys): static { $this->keys = $keys; diff --git a/src/log/composer.json b/src/log/composer.json index f35a19e8e4..4b2be861d5 100644 --- a/src/log/composer.json +++ b/src/log/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/config": "^0.4", diff --git a/src/log/src/Context/Repository.php b/src/log/src/Context/Repository.php index bc27680543..a8dc699662 100644 --- a/src/log/src/Context/Repository.php +++ b/src/log/src/Context/Repository.php @@ -13,6 +13,7 @@ use Hypervel\Log\Context\Events\ContextDehydrating; use Hypervel\Log\Context\Events\ContextHydrated; use Hypervel\Queue\SerializesAndRestoresModelIdentifiers; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Traits\Conditionable; use Hypervel\Support\Traits\Macroable; @@ -550,7 +551,7 @@ public function replicate(): static /** * Register a callback to execute before context is dehydrated. * - * Boot-only. Registers a listener on the worker-global event dispatcher; + * When the dispatcher is the application's worker-shared dispatcher, * per-request registration persists and affects subsequent requests. * * @param (callable(self): mixed) $callback @@ -566,7 +567,7 @@ public function dehydrating(callable $callback): static /** * Register a callback to execute after context has been hydrated. * - * Boot-only. Registers a listener on the worker-global event dispatcher; + * When the dispatcher is the application's worker-shared dispatcher, * per-request registration persists and affects subsequent requests. * * @param (callable(self): mixed) $callback @@ -582,11 +583,12 @@ public function hydrated(callable $callback): static /** * Set the callback to handle unserialize exceptions. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and handles every subsequent context hydration failure. + * The callback persists in a static property for the worker lifetime + * and handles every subsequent context hydration failure. * * @return $this */ + #[BootOnly] public function handleUnserializeExceptionsUsing(?callable $callback): static { static::$handleUnserializeExceptionsUsing = $callback !== null diff --git a/src/log/src/LogManager.php b/src/log/src/LogManager.php index c473c676d6..9da93296bf 100644 --- a/src/log/src/LogManager.php +++ b/src/log/src/LogManager.php @@ -16,6 +16,9 @@ use Hypervel\Log\Handlers\RotatingFileHandler; use Hypervel\Log\Handlers\StreamHandler; use Hypervel\Log\Processors\UidProcessor; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\RebindsCallbacksToSelf; use Hypervel\Support\Str; @@ -609,8 +612,9 @@ public function getDefaultDriver(): ?string /** * Set the default log driver name. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultDriver(UnitEnum|string $name): void { if ($name instanceof UnitEnum) { @@ -626,11 +630,12 @@ public function setDefaultDriver(UnitEnum|string $name): void * Anonymous closures run in this manager's class scope; non-static closures * also receive the manager as $this. * - * Boot-only. The callback persists in the singleton's customCreators array - * for the worker lifetime and applies to every subsequent channel resolution. + * The callback persists in the singleton's customCreators array for the + * worker lifetime and applies to every subsequent channel resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback): static { try { @@ -648,10 +653,11 @@ public function extend(string $driver, Closure $callback): static /** * Unset the given channel instance. * - * Boot or tests only. Mutates the singleton's channel cache; concurrent - * coroutines may already hold a reference to the channel and next - * resolution will rebuild with fresh handlers (file handles, etc.). + * Mutates the singleton's channel cache; concurrent coroutines may + * already hold a reference to the channel and next resolution will + * rebuild with fresh handlers (file handles, etc.). */ + #[BootOrTestsOnly(whenShared: true)] public function forgetChannel(UnitEnum|string|null $driver = null): void { if ($driver instanceof UnitEnum) { @@ -779,12 +785,13 @@ public function log($level, Arrayable|Jsonable|Stringable|array|string $message, /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's Application reference; per-request use - * races across coroutines and breaks every concurrent request resolving - * services through this manager. + * Swaps the singleton's Application reference; per-request use races + * across coroutines and breaks every concurrent request resolving services + * through this manager. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Application $app): static { $this->app = $app; diff --git a/src/log/src/Logger.php b/src/log/src/Logger.php index 34f1826654..59f6cb08c3 100755 --- a/src/log/src/Logger.php +++ b/src/log/src/Logger.php @@ -11,6 +11,7 @@ use Hypervel\Contracts\Support\Jsonable; use Hypervel\Log\Context\Repository as ContextRepository; use Hypervel\Log\Events\MessageLogged; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Traits\Conditionable; use Monolog\Logger as Monolog; use Psr\Log\LoggerInterface; @@ -252,7 +253,7 @@ protected function state(): LoggerState /** * Register a new callback handler for when a log event is triggered. * - * Boot-only. Registers a listener on the worker-global event dispatcher; + * When the dispatcher is the application's worker-shared dispatcher, * per-request registration persists and affects subsequent requests. * * @throws RuntimeException @@ -321,9 +322,10 @@ public function getEventDispatcher(): ?Dispatcher /** * Set the event dispatcher instance. * - * Boot or tests only. Persists on the cached logger for the worker - * lifetime; per-request use races across coroutines. + * Persists on the cached logger for the worker lifetime; + * per-request use races across coroutines. */ + #[BootOrTestsOnly(whenShared: true)] public function setEventDispatcher(Dispatcher $dispatcher): void { $this->dispatcher = $dispatcher; diff --git a/src/macroable/composer.json b/src/macroable/composer.json index 30bb93d073..a4cf0b0900 100644 --- a/src/macroable/composer.json +++ b/src/macroable/composer.json @@ -23,7 +23,8 @@ "source": "https://github.com/hypervel/components" }, "require": { - "php": "^8.4" + "php": "^8.4", + "hypervel/attributes": "^0.4" }, "autoload": { "psr-4": { diff --git a/src/macroable/src/Traits/Macroable.php b/src/macroable/src/Traits/Macroable.php index 3a1736e1ee..1c4bcb9858 100644 --- a/src/macroable/src/Traits/Macroable.php +++ b/src/macroable/src/Traits/Macroable.php @@ -6,6 +6,8 @@ use BadMethodCallException; use Closure; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use ReflectionClass; use ReflectionException; use ReflectionMethod; @@ -20,11 +22,12 @@ trait Macroable /** * Register a custom macro. * - * Boot-only. Macros persist in a static property for the worker lifetime - * and apply to every subsequent call on the macroable class. + * Macros persist in a static property for the worker lifetime and apply + * to every subsequent call on the macroable class. * * @param-closure-this static $macro */ + #[BootOnly] public static function macro(string $name, callable|object $macro): void { static::$macros[$name] = $macro; @@ -33,11 +36,12 @@ public static function macro(string $name, callable|object $macro): void /** * Mix another object into the class. * - * Boot-only. Delegates to macro() for each method; registered macros persist - * in a static property for the worker lifetime. + * Delegates to macro() for each method; registered macros persist in a + * static property for the worker lifetime. * * @throws ReflectionException */ + #[BootOnly] public static function mixin(object $mixin, bool $replace = true): void { $methods = (new ReflectionClass($mixin))->getMethods( @@ -62,9 +66,10 @@ public static function hasMacro(string $name): bool /** * Flush the existing macros. * - * Boot or tests only. Clears worker-wide macros; concurrent coroutines may - * resolve different methods depending on timing. + * Clears worker-wide macros; concurrent coroutines may resolve different + * methods depending on timing. */ + #[BootOrTestsOnly] public static function flushMacros(): void { static::$macros = []; diff --git a/src/mail/composer.json b/src/mail/composer.json index 9227a71088..125b8dcbb0 100644 --- a/src/mail/composer.json +++ b/src/mail/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/bus": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", diff --git a/src/mail/src/MailManager.php b/src/mail/src/MailManager.php index 765fdcd798..533709b8db 100644 --- a/src/mail/src/MailManager.php +++ b/src/mail/src/MailManager.php @@ -24,6 +24,9 @@ use Hypervel\Mail\Transport\SesV2Transport; use Hypervel\ObjectPool\Concerns\HasPoolProxy; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Aws\SerializedCredentialProvider; use Hypervel\Support\ConfigurationUrlParser; use Hypervel\Support\Str; @@ -702,8 +705,9 @@ public function getDefaultDriver(): string /** * Set the default mail driver name. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultDriver(UnitEnum|string $name): void { $name = $name instanceof UnitEnum ? (string) enum_value($name) : $name; @@ -714,9 +718,8 @@ public function setDefaultDriver(UnitEnum|string $name): void /** * Disconnect the given mailer and close its shared transport pool. * - * Boot or tests only, plus operational recovery of broken pooled - * resources. Other mailers sharing the pool transparently acquire a fresh - * pool on their next operation. + * Use this to recover broken pooled resources. Other mailers sharing the + * pool transparently acquire a fresh pool on their next operation. */ public function purge(UnitEnum|string|null $name = null): void { @@ -764,12 +767,13 @@ public function purge(UnitEnum|string|null $name = null): void /** * Register a custom driver creator Closure. * - * Boot-only. The callback persists in the singleton's customCreators array - * (and the poolable list if $poolable is true) for the worker lifetime and - * applies to every subsequent transport resolution. + * The callback persists in the singleton's customCreators array (and the + * poolable list if $poolable is true) for the worker lifetime and applies + * to every subsequent transport resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback, bool $poolable = false): static { if ($poolable) { @@ -792,12 +796,13 @@ public function getApplication(): Container /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's container and configuration references - * without rebuilding resolved mailers; per-request use races across - * coroutines and breaks every concurrent mail send. + * Swaps the singleton's container and configuration references without + * rebuilding resolved mailers; per-request use races across coroutines and + * breaks every concurrent mail send. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Container $app): static { $this->app = $app; @@ -809,11 +814,12 @@ public function setApplication(Container $app): static /** * Forget all of the resolved mailer instances. * - * Boot or tests only. This is cache-only: pooled transports remain shared - * resources until purged or reclaimed by their idle TTL. + * This is cache-only: pooled transports remain shared resources until + * purged or reclaimed by their idle TTL. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetMailers(): static { $this->mailers = []; diff --git a/src/mail/src/Mailable.php b/src/mail/src/Mailable.php index 818f873304..e34a5a78aa 100644 --- a/src/mail/src/Mailable.php +++ b/src/mail/src/Mailable.php @@ -22,6 +22,7 @@ use Hypervel\Queue\Attributes\Connection; use Hypervel\Queue\Attributes\Delay; use Hypervel\Queue\Attributes\Queue as QueueAttribute; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\EncodedHtmlString; use Hypervel\Support\HtmlString; @@ -1717,9 +1718,10 @@ public function withSymfonyMessage(callable $callback): static /** * Register a callback to be called while building the view data. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every Mailable render across all coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every Mailable render across all coroutines. */ + #[BootOnly] public static function buildViewDataUsing(callable $callback): void { static::$viewDataCallback = $callback; diff --git a/src/mail/src/Mailer.php b/src/mail/src/Mailer.php index 2e8d435068..94f9154e96 100644 --- a/src/mail/src/Mailer.php +++ b/src/mail/src/Mailer.php @@ -19,6 +19,7 @@ use Hypervel\Mail\Events\MessageSending; use Hypervel\Mail\Events\MessageSent; use Hypervel\Mail\Mailables\Address; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\HtmlString; use Hypervel\Support\Traits\Macroable; use InvalidArgumentException; @@ -78,10 +79,11 @@ public function __construct( /** * Set the global from address and name. * - * Boot-only. Mutates the shared Mailer's global from; per-request use - * races across coroutines and applies the change to every subsequent + * Mutates the shared Mailer's global from; per-request use races + * across coroutines and applies the change to every subsequent * message. */ + #[BootOnly(whenShared: true)] public function alwaysFrom(string $address, ?string $name = null): void { $this->from = ['address' => $address, 'name' => $name]; @@ -90,10 +92,10 @@ public function alwaysFrom(string $address, ?string $name = null): void /** * Set the global reply-to address and name. * - * Boot-only. Mutates the shared Mailer's global reply-to; per-request use - * races across coroutines and applies the change to every subsequent - * message. + * Mutates the shared Mailer's global reply-to; per-request use races + * across coroutines and applies the change to every subsequent message. */ + #[BootOnly(whenShared: true)] public function alwaysReplyTo(string $address, ?string $name = null): void { $this->replyTo = ['address' => $address, 'name' => $name]; @@ -102,10 +104,10 @@ public function alwaysReplyTo(string $address, ?string $name = null): void /** * Set the global return path address. * - * Boot-only. Mutates the shared Mailer's global return path; per-request - * use races across coroutines and applies the change to every subsequent - * message. + * Mutates the shared Mailer's global return path; per-request use races + * across coroutines and applies the change to every subsequent message. */ + #[BootOnly(whenShared: true)] public function alwaysReturnPath(string $address): void { $this->returnPath = ['address' => $address]; @@ -114,10 +116,11 @@ public function alwaysReturnPath(string $address): void /** * Set the global to address and name. * - * Boot-only. Mutates the shared Mailer's global to; once set, Mailer::send() - * silently redirects every subsequent email across all coroutines to this - * address. Per-request use is never safe. + * Mutates the shared Mailer's global to; once set, Mailer::send() silently + * redirects every subsequent email across all coroutines to this address. + * Per-request use is never safe. */ + #[BootOnly(whenShared: true)] public function alwaysTo(string $address, ?string $name = null): void { $this->to = ['address' => $address, 'name' => $name]; @@ -526,10 +529,11 @@ public function getViewFactory(): FactoryContract /** * Set the Symfony Transport instance. * - * Boot-only. Replaces the transport on the shared Mailer; per-request use - * races across coroutines and routes every other coroutine's mail through - * the swapped transport. + * Replaces the transport on the shared Mailer; per-request use races + * across coroutines and routes every other coroutine's mail through the + * swapped transport. */ + #[BootOnly(whenShared: true)] public function setSymfonyTransport(TransportInterface $transport): void { $this->transport = $transport; @@ -538,11 +542,12 @@ public function setSymfonyTransport(TransportInterface $transport): void /** * Set the queue manager instance. * - * Boot-only. Replaces the queue factory on the shared Mailer; per-request - * use races across coroutines. + * Replaces the queue factory on the shared Mailer; per-request use races + * across coroutines. * * @return $this */ + #[BootOnly(whenShared: true)] public function setQueue(QueueFactory $queue): static { $this->queue = $queue; diff --git a/src/mail/src/Markdown.php b/src/mail/src/Markdown.php index 1ce8ec1063..87850025ec 100644 --- a/src/mail/src/Markdown.php +++ b/src/mail/src/Markdown.php @@ -5,6 +5,7 @@ namespace Hypervel\Mail; use Hypervel\Contracts\View\Factory as ViewFactory; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\EncodedHtmlString; use Hypervel\Support\HtmlString; use Hypervel\Support\Str; @@ -238,9 +239,10 @@ protected function componentPaths(): array /** * Register new mail component paths. * - * Boot-only. Mutates the shared Markdown renderer's component paths; use - * scoped view namespaces for per-render component path changes. + * Mutates the shared Markdown renderer's component paths; use scoped + * view namespaces for per-render component path changes. */ + #[BootOnly(whenShared: true)] public function loadComponentsFrom(array $paths = []): void { $this->componentPaths = $paths; @@ -249,11 +251,12 @@ public function loadComponentsFrom(array $paths = []): void /** * Set the default theme to be used. * - * Boot-only. Mutates the shared Markdown renderer's default theme; pass a - * per-render theme to render() for message-specific themes. + * Mutates the shared Markdown renderer's default theme; pass a per-render + * theme to render() for message-specific themes. * * @return $this */ + #[BootOnly(whenShared: true)] public function theme(string $theme): static { $this->theme = $theme; @@ -272,9 +275,10 @@ public function getTheme(): string /** * Enable secured encoding when parsing Markdown. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every subsequent Markdown render across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every subsequent Markdown render across all coroutines. */ + #[BootOnly] public static function withSecuredEncoding(): void { static::$withSecuredEncoding = true; @@ -283,9 +287,10 @@ public static function withSecuredEncoding(): void /** * Disable secured encoding when parsing Markdown. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every subsequent Markdown render across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every subsequent Markdown render across all coroutines. */ + #[BootOnly] public static function withoutSecuredEncoding(): void { static::$withSecuredEncoding = false; diff --git a/src/mail/src/Transport/SesV2Transport.php b/src/mail/src/Transport/SesV2Transport.php index ad69392d24..aaab6e16da 100644 --- a/src/mail/src/Transport/SesV2Transport.php +++ b/src/mail/src/Transport/SesV2Transport.php @@ -6,6 +6,7 @@ use Aws\Exception\AwsException; use Aws\SesV2\SesV2Client; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Stringable; use Symfony\Component\Mailer\Exception\TransportException; @@ -147,9 +148,10 @@ public function getOptions(): array /** * Set the transmission options being used by the transport. * - * Boot-only. Mutates the shared transport's SES send options; per-request - * use races across coroutines. + * Mutates the shared transport's SES send options; per-request use races + * across coroutines. */ + #[BootOnly(whenShared: true)] public function setOptions(array $options): array { return $this->options = $options; diff --git a/src/mcp/composer.json b/src/mcp/composer.json index e99409b93f..cc80ab34a9 100644 --- a/src/mcp/composer.json +++ b/src/mcp/composer.json @@ -27,6 +27,7 @@ "php": "^8.4", "ext-swoole": "^6.3.0RC1", "guzzlehttp/psr7": "^3.1", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", diff --git a/src/mcp/src/Client/ClientManager.php b/src/mcp/src/Client/ClientManager.php index b322b7175f..9c134e8f9c 100644 --- a/src/mcp/src/Client/ClientManager.php +++ b/src/mcp/src/Client/ClientManager.php @@ -12,6 +12,7 @@ use Hypervel\Mcp\Exceptions\ClientException; use Hypervel\Mcp\WebClient; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Traits\Macroable; use Throwable; @@ -43,11 +44,13 @@ class ClientManager /** * Register a named client factory. * - * Boot-only. Factories persist across requests. Re-registration disconnects - * only the current owner's client; other owners retain theirs until cleanup. + * On the shared manager, factories persist across requests. Re-registration + * disconnects only the current owner's client; other owners retain theirs + * until cleanup. * * @param Closure(): Client $factory */ + #[BootOnly(whenShared: true)] public function registerClient(string $name, Closure $factory): void { if (isset($this->clients[$name])) { diff --git a/src/mcp/src/Client/OAuth/AuthServerDiscovery.php b/src/mcp/src/Client/OAuth/AuthServerDiscovery.php index a77370eeee..d010e259c5 100644 --- a/src/mcp/src/Client/OAuth/AuthServerDiscovery.php +++ b/src/mcp/src/Client/OAuth/AuthServerDiscovery.php @@ -17,6 +17,7 @@ use Hypervel\Mcp\Client\Middleware\RestrictHeadersToOrigin; use Hypervel\Mcp\Client\OAuth\Concerns\InteractsWithOAuthEndpoints; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\TestsOnly; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; @@ -33,10 +34,11 @@ class AuthServerDiscovery /** * Resolve OAuth destination hostnames with the given callback instead of DNS. * - * Tests only. The callback persists in a static property for the worker lifetime and supplies the addresses for every OAuth request. + * The callback persists in a static property for the worker lifetime and supplies the addresses for every OAuth request. * * @param null|Closure(string): array $callback */ + #[TestsOnly] public static function resolveHostsUsing(?Closure $callback): void { static::$hostResolver = $callback; diff --git a/src/mcp/src/Client/OAuth/OAuthRouteRegistrar.php b/src/mcp/src/Client/OAuth/OAuthRouteRegistrar.php index 021e172cad..27a91621d7 100644 --- a/src/mcp/src/Client/OAuth/OAuthRouteRegistrar.php +++ b/src/mcp/src/Client/OAuth/OAuthRouteRegistrar.php @@ -12,6 +12,7 @@ use Hypervel\Mcp\WebClient; use Hypervel\Routing\Exceptions\InvalidSignatureException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Route as Router; class OAuthRouteRegistrar @@ -29,12 +30,13 @@ public static function url(string $name): string /** * Register connection, callback and public metadata routes for a named client. * - * Boot-only. Routes persist for the worker lifetime and affect subsequent requests. + * Routes persist for the worker lifetime and affect subsequent requests. * * @param array{0: class-string, 1: string}|Closure(string, TokenSet): mixed $handler receives provider or client, token, and optional returnTo by parameter name * @param array|string $middleware * @param array $clientMetadata */ + #[BootOnly] public function register( string $client, Closure|array $handler, diff --git a/src/mcp/src/Server/Content/Blob.php b/src/mcp/src/Server/Content/Blob.php index 4504ca222a..5f398e3936 100644 --- a/src/mcp/src/Server/Content/Blob.php +++ b/src/mcp/src/Server/Content/Blob.php @@ -9,6 +9,7 @@ use Hypervel\Mcp\Server\Prompt; use Hypervel\Mcp\Server\Resource; use Hypervel\Mcp\Server\Tool; +use Hypervel\Support\Attributes\Unsupported; use InvalidArgumentException; class Blob implements Content @@ -27,6 +28,7 @@ public function __construct(protected string $content) * * @return array */ + #[Unsupported('Return blob content from a resource.')] public function toTool(Tool $tool): array { throw new InvalidArgumentException( @@ -39,6 +41,7 @@ public function toTool(Tool $tool): array * * @return array */ + #[Unsupported('Return blob content from a resource.')] public function toPrompt(Prompt $prompt): array { throw new InvalidArgumentException( diff --git a/src/mcp/src/Server/Content/Notification.php b/src/mcp/src/Server/Content/Notification.php index 248ebb0489..ec1cc66582 100644 --- a/src/mcp/src/Server/Content/Notification.php +++ b/src/mcp/src/Server/Content/Notification.php @@ -9,6 +9,7 @@ use Hypervel\Mcp\Server\Prompt; use Hypervel\Mcp\Server\Resource; use Hypervel\Mcp\Server\Tool; +use Hypervel\Support\Attributes\Unsupported; use InvalidArgumentException; class Notification implements Content @@ -29,6 +30,7 @@ public function __construct(protected string $method, protected array $params) * * @return array */ + #[Unsupported('Yield notifications separately from the tool result content.')] public function toTool(Tool $tool): array { throw new InvalidArgumentException('Notifications must be yielded separately from tool result content.'); @@ -39,6 +41,7 @@ public function toTool(Tool $tool): array * * @return array */ + #[Unsupported('Yield notifications separately from the prompt result content.')] public function toPrompt(Prompt $prompt): array { throw new InvalidArgumentException('Notifications must be yielded separately from prompt result content.'); @@ -49,6 +52,7 @@ public function toPrompt(Prompt $prompt): array * * @return array */ + #[Unsupported('Yield notifications separately from the resource result content.')] public function toResource(Resource $resource): array { throw new InvalidArgumentException('Notifications must be yielded separately from resource result content.'); diff --git a/src/mcp/src/Server/Content/ResourceLink.php b/src/mcp/src/Server/Content/ResourceLink.php index 9d001bf4ac..9a569fbe44 100644 --- a/src/mcp/src/Server/Content/ResourceLink.php +++ b/src/mcp/src/Server/Content/ResourceLink.php @@ -10,6 +10,7 @@ use Hypervel\Mcp\Server\Prompt; use Hypervel\Mcp\Server\Resource; use Hypervel\Mcp\Server\Tool; +use Hypervel\Support\Attributes\Unsupported; use InvalidArgumentException; class ResourceLink implements Content @@ -59,6 +60,7 @@ public function toPrompt(Prompt $prompt): array * * @return array */ + #[Unsupported] public function toResource(Resource $resource): array { throw new InvalidArgumentException( diff --git a/src/mcp/src/Server/Registrar.php b/src/mcp/src/Server/Registrar.php index 421b4ae109..2d52ce9fab 100644 --- a/src/mcp/src/Server/Registrar.php +++ b/src/mcp/src/Server/Registrar.php @@ -25,6 +25,7 @@ use Hypervel\Passport\Passport; use Hypervel\Passport\ServerFactory; use Hypervel\Routing\Route; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Route as Router; use Hypervel\Support\Traits\Macroable; use LogicException; @@ -44,10 +45,11 @@ class Registrar /** * Register an HTTP server. * - * Boot-only. Routes and server registrations persist for the worker lifetime. + * Routes and server registrations persist for the worker lifetime. * * @param class-string $serverClass */ + #[BootOnly] public function web(string $route, string $serverClass): Route { // https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server @@ -73,10 +75,11 @@ public function web(string $route, string $serverClass): Route /** * Register a local server. * - * Boot-only. Server factories persist for the worker lifetime. + * On the shared registrar, server factories persist across requests. * * @param class-string $serverClass */ + #[BootOnly(whenShared: true)] public function local(string $handle, string $serverClass): void { $this->localServers[$handle] = fn (): mixed => static::startServer($serverClass, fn (): StdioTransport => new StdioTransport); @@ -85,10 +88,11 @@ public function local(string $handle, string $serverClass): void /** * Register a named client factory. * - * Boot-only. Client factories persist for the worker lifetime. + * Client factories persist for the worker lifetime. * * @param Closure(): Client $factory */ + #[BootOnly] public function registerClient(string $name, Closure $factory): void { $this->clientManager()->registerClient($name, $factory); @@ -105,12 +109,13 @@ public function client(string $name): Client /** * Register OAuth routes for a named client. * - * Boot-only. The routes persist for the worker lifetime. + * The routes persist for the worker lifetime. * * @param array{0: class-string, 1: string}|Closure(string, TokenSet): mixed $handler receives provider or client, token, and optional returnTo by parameter name * @param array|string $middleware * @param array $clientMetadata */ + #[BootOnly] public function oAuthRoutesFor( string $client, Closure|array $handler, @@ -155,8 +160,9 @@ public function servers(): array /** * Register server-side OAuth discovery and registration routes. * - * Boot-only. Routes and Passport's challenge resolver persist for the worker lifetime. + * Routes and Passport's challenge resolver persist for the worker lifetime. */ + #[BootOnly] public function oauthRoutes(string $oauthPrefix = 'oauth', bool $registration = true): void { static::ensureMcpScope(); @@ -243,10 +249,11 @@ protected function hasGetRoute(string $uri): bool /** * Add the MCP scope when Passport is installed. * - * Boot-only. Passport scopes persist for the worker lifetime. + * Passport scopes persist for the worker lifetime. * * @return array */ + #[BootOnly] public static function ensureMcpScope(): array { if (class_exists(Passport::class) === false) { diff --git a/src/object-pool/composer.json b/src/object-pool/composer.json index de1501b43d..6ceee992b3 100644 --- a/src/object-pool/composer.json +++ b/src/object-pool/composer.json @@ -25,12 +25,13 @@ ], "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/container": "^0.4", "hypervel/contracts": "^0.4", "hypervel/coordinator": "^0.4", - "hypervel/coroutine": "^0.4", "hypervel/core": "^0.4", + "hypervel/coroutine": "^0.4", "hypervel/support": "^0.4" }, "autoload": { diff --git a/src/object-pool/src/Concerns/HasPoolProxy.php b/src/object-pool/src/Concerns/HasPoolProxy.php index 1803882052..1354991f41 100644 --- a/src/object-pool/src/Concerns/HasPoolProxy.php +++ b/src/object-pool/src/Concerns/HasPoolProxy.php @@ -11,6 +11,7 @@ use Hypervel\ObjectPool\PoolOptions; use Hypervel\ObjectPool\PoolProxy; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; /** @@ -68,9 +69,10 @@ abstract protected function poolFactory(): Factory; /** * Set the release callback for a pooled driver. * - * Boot-only. The callback persists on the manager for the worker lifetime - * and is captured by every subsequently created proxy for the driver. + * The callback persists on the shared manager for the worker lifetime and + * is captured by every subsequently created proxy for the driver. */ + #[BootOnly(whenShared: true)] public function setReleaseCallback(string $driver, Closure $callback): static { $this->releaseCallbacks[$driver] = $callback; @@ -89,10 +91,11 @@ public function getReleaseCallback(string $driver): ?Closure /** * Add a driver to the poolable-driver list. * - * Boot-only. The list persists on the manager for the worker lifetime and - * is consulted on subsequent driver creation. Per-request use races across + * The list persists on the manager for the worker lifetime and is + * consulted on subsequent driver creation. Per-request use races across * coroutines and does not affect already-cached drivers. */ + #[BootOnly(whenShared: true)] public function addPoolableDriver(string $driver): static { if (! in_array($driver, $this->poolableDrivers, true)) { @@ -105,10 +108,11 @@ public function addPoolableDriver(string $driver): static /** * Remove a driver from the poolable-driver list. * - * Boot-only. The list persists on the manager for the worker lifetime and - * is consulted on subsequent driver creation. Per-request use races across + * The list persists on the manager for the worker lifetime and is + * consulted on subsequent driver creation. Per-request use races across * coroutines and does not affect already-cached drivers. */ + #[BootOnly(whenShared: true)] public function removePoolableDriver(string $driver): static { $index = array_search($driver, $this->poolableDrivers, true); @@ -134,10 +138,11 @@ public function getPoolableDrivers(): array /** * Set the poolable-driver list. * - * Boot-only. The list persists on the manager for the worker lifetime and - * is consulted on subsequent driver creation. Per-request use races across + * The list persists on the manager for the worker lifetime and is + * consulted on subsequent driver creation. Per-request use races across * coroutines and does not affect already-cached drivers. */ + #[BootOnly(whenShared: true)] public function setPoolableDrivers(array $poolableDrivers): static { $this->poolableDrivers = array_values($poolableDrivers); diff --git a/src/object-pool/src/PoolManager.php b/src/object-pool/src/PoolManager.php index dadd91da52..5e5285745f 100644 --- a/src/object-pool/src/PoolManager.php +++ b/src/object-pool/src/PoolManager.php @@ -7,6 +7,7 @@ use Hypervel\Contracts\ObjectPool\Factory as FactoryContract; use Hypervel\Contracts\ObjectPool\ObjectPool; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOrTestsOnly; use JsonException; use RuntimeException; use Throwable; @@ -152,9 +153,10 @@ public function purge(string $identity, ?ObjectPool $expected = null): bool /** * Remove and close every registered pool. * - * Boot or tests only. This clears worker-lifetime pools shared by every - * coroutine; use targeted removal for runtime resource recovery. + * This clears worker-lifetime pools shared by every coroutine; use + * targeted removal for runtime resource recovery. */ + #[BootOrTestsOnly] public function purgeAll(): void { $pools = $this->pools; diff --git a/src/object-pool/src/PoolRecycler.php b/src/object-pool/src/PoolRecycler.php index 7c7f107bb5..d5cda018b8 100644 --- a/src/object-pool/src/PoolRecycler.php +++ b/src/object-pool/src/PoolRecycler.php @@ -8,6 +8,8 @@ use Hypervel\Contracts\ObjectPool\Recycler; use Hypervel\Coordinator\Timer; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use RuntimeException; use Throwable; @@ -44,9 +46,10 @@ public function getInterval(): float /** * Start periodic pool maintenance. * - * Boot-only. Starting during a request schedules worker-wide maintenance - * that affects every subsequently registered pool. + * Starting during a request schedules worker-wide maintenance that + * affects every subsequently registered pool. */ + #[BootOnly] public function start(): void { if ($this->timerId !== null) { @@ -72,9 +75,9 @@ function (): void { /** * Stop periodic pool maintenance. * - * Boot or tests only. Stopping disables automatic maintenance for every - * pool in the worker. + * Stopping disables automatic maintenance for every pool in the worker. */ + #[BootOrTestsOnly] public function stop(): void { if ($this->timerId !== null) { diff --git a/src/opentelemetry/composer.json b/src/opentelemetry/composer.json index e3635188a7..0e2ce8acf3 100644 --- a/src/opentelemetry/composer.json +++ b/src/opentelemetry/composer.json @@ -36,6 +36,7 @@ "ext-mbstring": "*", "ext-swoole": "^6.3.0RC1", "guzzlehttp/psr7": "^3.1", + "hypervel/attributes": "^0.4", "hypervel/cache": "^0.4", "hypervel/config": "^0.4", "hypervel/connection-pool": "^0.4", diff --git a/src/opentelemetry/src/OpenTelemetryManager.php b/src/opentelemetry/src/OpenTelemetryManager.php index 1b65f96c06..400d231353 100644 --- a/src/opentelemetry/src/OpenTelemetryManager.php +++ b/src/opentelemetry/src/OpenTelemetryManager.php @@ -23,6 +23,7 @@ use Hypervel\OpenTelemetry\Support\OperationOrigin; use Hypervel\OpenTelemetry\Support\ProcessIdentity; use Hypervel\OpenTelemetry\Support\ProviderSet; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use LogicException; use OpenTelemetry\API\Logs\LoggerInterface; @@ -413,8 +414,9 @@ public function configuration(): array /** * Register a custom exporter driver. * - * Boot-only. The creator persists for the worker lifetime and must be registered before telemetry binding. + * On the shared manager, the creator persists for the worker lifetime and must be registered before telemetry binding. */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $factory): void { $this->customCreators[$driver] = $factory; @@ -424,8 +426,9 @@ public function extend(string $driver, Closure $factory): void /** * Register an exception-record enricher. * - * Boot-only. The callback persists for the worker lifetime and affects every reported exception. + * On the shared manager, the callback persists for the worker lifetime and affects every reported exception. */ + #[BootOnly(whenShared: true)] public function enrichExceptionsUsing(Closure $enricher): void { $this->exceptionEnrichers[] = $enricher; @@ -445,8 +448,9 @@ public function exceptionEnrichers(): array /** * Set the outgoing URL-template resolver. * - * Boot-only. The callback persists for the worker lifetime and affects every traced outgoing request. + * On the shared manager, the callback persists for the worker lifetime and affects every traced outgoing request. */ + #[BootOnly(whenShared: true)] public function resolveUrlTemplateUsing(Closure $resolver): void { $this->urlTemplateResolver = $resolver; @@ -465,8 +469,9 @@ public function urlTemplateResolver(): ?Closure /** * Set the authenticated-user resolver. * - * Boot-only. The callback persists for the worker lifetime and affects every request that collects user context. + * On the shared manager, the callback persists for the worker lifetime and affects every request that collects user context. */ + #[BootOnly(whenShared: true)] public function resolveUserUsing(Closure $resolver): void { $this->userResolver = $resolver; @@ -485,8 +490,9 @@ public function userResolver(): ?Closure /** * Set the cache-key resolver. * - * Boot-only. The callback persists for the worker lifetime and affects enabled cache-key capture. + * On the shared manager, the callback persists for the worker lifetime and affects enabled cache-key capture. */ + #[BootOnly(whenShared: true)] public function resolveCacheKeyUsing(Closure $resolver): void { $this->cacheKeyResolver = $resolver; @@ -505,8 +511,9 @@ public function cacheKeyResolver(): ?Closure /** * Set the Redis query-text resolver. * - * Boot-only. The callback persists for the worker lifetime and affects enabled Redis query-text capture. + * On the shared manager, the callback persists for the worker lifetime and affects enabled Redis query-text capture. */ + #[BootOnly(whenShared: true)] public function resolveRedisQueryTextUsing(Closure $resolver): void { $this->redisQueryTextResolver = $resolver; diff --git a/src/opentelemetry/src/Support/ExceptionContextRegistry.php b/src/opentelemetry/src/Support/ExceptionContextRegistry.php index 51a21d44df..6d04b5392c 100644 --- a/src/opentelemetry/src/Support/ExceptionContextRegistry.php +++ b/src/opentelemetry/src/Support/ExceptionContextRegistry.php @@ -4,6 +4,7 @@ namespace Hypervel\OpenTelemetry\Support; +use Hypervel\Support\Attributes\BootOnly; use OpenTelemetry\Context\ContextInterface; use Throwable; use WeakMap; @@ -20,8 +21,9 @@ class ExceptionContextRegistry /** * Enable exact exception-context handoff. * - * Boot-only. The registry remains enabled for the worker lifetime. + * The shared registry remains enabled for the worker lifetime. */ + #[BootOnly(whenShared: true)] public function enable(): void { $this->enabled = true; @@ -30,8 +32,11 @@ public function enable(): void /** * Mark direct exception recording as registered on the exception handler. * - * Boot-only. The registration persists on the exception handler for the worker lifetime. + * On the shared registry, the mark persists for the worker lifetime and lets + * the log handler defer exceptions currently being reported by the exception + * handler to the direct recorder. */ + #[BootOnly(whenShared: true)] public function markRecorderRegistered(): void { $this->recorderRegistered = true; diff --git a/src/pagination/composer.json b/src/pagination/composer.json index 2ce046a0b9..c3a6124c39 100644 --- a/src/pagination/composer.json +++ b/src/pagination/composer.json @@ -26,6 +26,7 @@ "require": { "php": "^8.4", "ext-filter": "*", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", diff --git a/src/pagination/src/AbstractCursorPaginator.php b/src/pagination/src/AbstractCursorPaginator.php index c7cc2fe984..5cce12e0ba 100644 --- a/src/pagination/src/AbstractCursorPaginator.php +++ b/src/pagination/src/AbstractCursorPaginator.php @@ -16,6 +16,7 @@ use Hypervel\Database\Eloquent\Relations\Relation; use Hypervel\Http\Resources\Json\JsonResource; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Hypervel\Support\Traits\ForwardsCalls; @@ -502,9 +503,10 @@ public static function resolveCurrentCursor(string $cursorName = 'cursor', ?Curs /** * Set the current cursor resolver callback. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every subsequent cursor-paginator's cursor lookup. + * The resolver persists in a static property for the worker lifetime and + * runs on every subsequent cursor-paginator's cursor lookup. */ + #[BootOnly] public static function currentCursorResolver(Closure $resolver): void { static::$currentCursorResolver = $resolver; diff --git a/src/pagination/src/AbstractPaginator.php b/src/pagination/src/AbstractPaginator.php index e142ceb69c..e45f087912 100644 --- a/src/pagination/src/AbstractPaginator.php +++ b/src/pagination/src/AbstractPaginator.php @@ -12,6 +12,7 @@ use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Relations\Relation; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Traits\ForwardsCalls; use Hypervel\Support\Traits\Tappable; @@ -478,9 +479,10 @@ public static function resolveCurrentPath(string $default = '/'): string /** * Set the current request path resolver callback. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every subsequent paginator's path lookup. + * The resolver persists in a static property for the worker lifetime + * and runs on every subsequent paginator's path lookup. */ + #[BootOnly] public static function currentPathResolver(Closure $resolver): void { static::$currentPathResolver = $resolver; @@ -501,9 +503,10 @@ public static function resolveCurrentPage(string $pageName = 'page', int $defaul /** * Set the current page resolver callback. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every subsequent paginator's page lookup. + * The resolver persists in a static property for the worker lifetime + * and runs on every subsequent paginator's page lookup. */ + #[BootOnly] public static function currentPageResolver(Closure $resolver): void { static::$currentPageResolver = $resolver; @@ -524,9 +527,10 @@ public static function resolveQueryString(string|array|null $default = null): st /** * Set with query string resolver callback. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every subsequent paginator's query-string lookup. + * The resolver persists in a static property for the worker lifetime and + * runs on every subsequent paginator's query-string lookup. */ + #[BootOnly] public static function queryStringResolver(Closure $resolver): void { static::$queryStringResolver = $resolver; @@ -543,9 +547,10 @@ public static function viewFactory(): Factory /** * Set the view factory resolver callback. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every subsequent paginator render. + * The resolver persists in a static property for the worker lifetime + * and runs on every subsequent paginator render. */ + #[BootOnly] public static function viewFactoryResolver(Closure $resolver): void { static::$viewFactoryResolver = $resolver; @@ -554,9 +559,10 @@ public static function viewFactoryResolver(Closure $resolver): void /** * Set the default pagination view. * - * Boot-only. The view name persists in a static property for the worker - * lifetime and applies to every subsequent paginator render. + * The view name persists in a static property for the worker lifetime + * and applies to every subsequent paginator render. */ + #[BootOnly] public static function defaultView(string $view): void { static::$defaultView = $view; @@ -565,9 +571,10 @@ public static function defaultView(string $view): void /** * Set the default "simple" pagination view. * - * Boot-only. The view name persists in a static property for the worker - * lifetime and applies to every subsequent simple-paginator render. + * The view name persists in a static property for the worker lifetime + * and applies to every subsequent simple-paginator render. */ + #[BootOnly] public static function defaultSimpleView(string $view): void { static::$defaultSimpleView = $view; @@ -576,9 +583,10 @@ public static function defaultSimpleView(string $view): void /** * Indicate that Tailwind styling should be used for generated links. * - * Boot-only. Sets the default view names in static properties for the - * worker lifetime; applies to every subsequent paginator render. + * Sets the default view names in static properties for the worker + * lifetime; applies to every subsequent paginator render. */ + #[BootOnly] public static function useTailwind(): void { static::defaultView(self::DEFAULT_VIEW); diff --git a/src/pagination/src/PaginationState.php b/src/pagination/src/PaginationState.php index 5fae1697f5..9c2c623830 100644 --- a/src/pagination/src/PaginationState.php +++ b/src/pagination/src/PaginationState.php @@ -7,15 +7,17 @@ use Hypervel\Context\RequestContext; use Hypervel\Contracts\Container\Container; use Hypervel\Contracts\View\Factory; +use Hypervel\Support\Attributes\BootOnly; class PaginationState { /** * Bind the pagination state resolvers using the given application container as a base. * - * Boot-only. The request resolvers read RequestContext on each invocation, - * while the lazy view resolver captures the worker-lifetime container. + * The request resolvers read RequestContext on each invocation, while the + * lazy view resolver captures the worker-lifetime container. */ + #[BootOnly] public static function resolveUsing(Container $app): void { Paginator::viewFactoryResolver(fn (): Factory => $app->make('view')); diff --git a/src/passkeys/composer.json b/src/passkeys/composer.json index aa4be419cd..dbde4c7d33 100644 --- a/src/passkeys/composer.json +++ b/src/passkeys/composer.json @@ -26,6 +26,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/collections": "^0.4", "hypervel/config": "^0.4", diff --git a/src/passkeys/src/Passkeys.php b/src/passkeys/src/Passkeys.php index d013fea10e..256f024769 100644 --- a/src/passkeys/src/Passkeys.php +++ b/src/passkeys/src/Passkeys.php @@ -12,6 +12,7 @@ use Hypervel\Contracts\Config\Repository as Config; use Hypervel\Http\Request; use Hypervel\Passkeys\Contracts\PasskeyUser; +use Hypervel\Support\Attributes\BootOnly; use RuntimeException; class Passkeys @@ -67,10 +68,11 @@ public static function relyingPartyId(): string /** * Register a callback to resolve the WebAuthn relying party ID for the current request. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent WebAuthn ceremony. + * The callback persists in static state for the worker lifetime and affects every subsequent WebAuthn ceremony. * * @param null|(callable(Request): string) $callback */ + #[BootOnly] public static function resolveRelyingPartyIdUsing(?callable $callback): void { self::$relyingPartyIdResolver = $callback === null @@ -111,10 +113,11 @@ public static function allowedOrigins(): array /** * Register a callback to resolve WebAuthn allowed origins for the current request. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent WebAuthn ceremony. + * The callback persists in static state for the worker lifetime and affects every subsequent WebAuthn ceremony. * * @param null|(callable(Request): list) $callback */ + #[BootOnly] public static function resolveAllowedOriginsUsing(?callable $callback): void { self::$allowedOriginsResolver = $callback === null @@ -160,10 +163,11 @@ public static function passkeyModel(): string /** * Set the passkey model class name. * - * Boot-only. The model class persists in static state for the worker lifetime and affects every subsequent passkey query. + * The model class persists in static state for the worker lifetime and affects every subsequent passkey query. * * @param class-string $model */ + #[BootOnly] public static function usePasskeyModel(string $model): void { self::$passkeyModel = $model; @@ -174,10 +178,11 @@ public static function usePasskeyModel(string $model): void /** * Register a callback to authorize passkey logins before login. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent passkey login. + * The callback persists in static state for the worker lifetime and affects every subsequent passkey login. * * @param null|(callable(Request, PasskeyUser, Passkey): bool) $callback */ + #[BootOnly] public static function authorizeLoginUsing(?callable $callback): void { self::$authorizeLoginUsing = $callback !== null @@ -206,10 +211,11 @@ public static function allowsLogin(Request $request, Passkey $passkey): bool /** * Register a callback to resolve the successful login redirect path. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent successful passkey login response. + * The callback persists in static state for the worker lifetime and affects every subsequent successful passkey login response. * * @param null|(callable(Request): (null|string)) $callback */ + #[BootOnly] public static function redirectUsing(?callable $callback): void { self::$redirectUsingCallback = $callback === null @@ -270,8 +276,9 @@ public static function shouldRegisterRoutes(): bool /** * Configure Passkeys to not register its routes. * - * Boot-only. The route registration flag persists in static state for the worker lifetime and affects route bootstrapping. + * The route registration flag persists in static state for the worker lifetime and affects route bootstrapping. */ + #[BootOnly] public static function ignoreRoutes(): void { self::$registersRoutes = false; diff --git a/src/passkeys/src/Support/WebAuthn.php b/src/passkeys/src/Support/WebAuthn.php index facb342da2..c11658dd6d 100644 --- a/src/passkeys/src/Support/WebAuthn.php +++ b/src/passkeys/src/Support/WebAuthn.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Passkeys\Passkeys; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -119,10 +120,11 @@ public static function assertionValidator(): AuthenticatorAssertionResponseValid /** * Configure the ceremony step manager factory. * - * Boot-only. The callback persists in static state for the worker lifetime and affects every subsequent WebAuthn ceremony. + * The callback persists in static state for the worker lifetime and affects every subsequent WebAuthn ceremony. * * @param null|(callable(CeremonyStepManagerFactory): (null|CeremonyStepManagerFactory)) $callback */ + #[BootOnly] public static function configureCeremonyStepManagerFactoryUsing(?callable $callback): void { self::$configureCeremonyStepManagerFactoryUsing = $callback === null diff --git a/src/passport/composer.json b/src/passport/composer.json index 711fea6b08..3707b2722f 100644 --- a/src/passport/composer.json +++ b/src/passport/composer.json @@ -30,6 +30,7 @@ "defuse/php-encryption": "^2.4", "firebase/php-jwt": "^7.0", "guzzlehttp/psr7": "^3.1", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", diff --git a/src/passport/src/Bridge/DeviceCodeGrant.php b/src/passport/src/Bridge/DeviceCodeGrant.php index 0087a76df9..d8a2f86661 100644 --- a/src/passport/src/Bridge/DeviceCodeGrant.php +++ b/src/passport/src/Bridge/DeviceCodeGrant.php @@ -7,6 +7,7 @@ use DateInterval; use Hypervel\Contracts\Routing\UrlGenerator; use Hypervel\Passport\ClientRepository as ClientModelRepository; +use Hypervel\Support\Attributes\BootOnly; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; @@ -92,9 +93,10 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ /** * Configure whether device responses include the complete verification URI. * - * Boot-only. This grant is shared by the worker and the flag affects subsequent requests. + * On the worker-shared grant, the flag affects subsequent requests. */ #[Override] + #[BootOnly(whenShared: true)] public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void { parent::setIncludeVerificationUriComplete($includeVerificationUriComplete); diff --git a/src/passport/src/Guards/TokenGuard.php b/src/passport/src/Guards/TokenGuard.php index 828a552346..62648955e8 100644 --- a/src/passport/src/Guards/TokenGuard.php +++ b/src/passport/src/Guards/TokenGuard.php @@ -428,7 +428,7 @@ public function getAuthContextKeys(): array */ protected function getExplicitUserContextKey(): string { - return "__auth.guards.{$this->name}.user.explicit"; + return $this->contextKey('user.explicit'); } /** @@ -436,7 +436,7 @@ protected function getExplicitUserContextKey(): string */ protected function getExplicitClientContextKey(): string { - return "__auth.guards.{$this->name}.client.explicit"; + return $this->contextKey('client.explicit'); } /** @@ -445,7 +445,7 @@ protected function getExplicitClientContextKey(): string protected function getContextKey(Request $request, string $type): string { if ($this->hasAccessToken($request)) { - return "__auth.guards.{$this->name}.{$type}." . $this->servers->authenticationFingerprint($request); + return $this->contextKey($type . '.' . $this->servers->authenticationFingerprint($request)); } $credentials = serialize([ @@ -454,7 +454,7 @@ protected function getContextKey(Request $request, string $type): string $request->header('X-XSRF-TOKEN'), ]); - return "__auth.guards.{$this->name}.{$type}." . hash('xxh128', $credentials, options: ['seed' => $this->hashSeed]); + return $this->contextKey($type . '.' . hash('xxh128', $credentials, options: ['seed' => $this->hashSeed])); } /** diff --git a/src/passport/src/Passport.php b/src/passport/src/Passport.php index 6cea839ce2..642746f28b 100644 --- a/src/passport/src/Passport.php +++ b/src/passport/src/Passport.php @@ -21,6 +21,8 @@ use Hypervel\Passport\Contracts\ServerSettingsResolver; use Hypervel\Passport\Guards\TokenGuard; use Hypervel\Passport\Http\Responses\SimpleViewResponse; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Date; use League\OAuth2\Server\ResourceServer; @@ -341,11 +343,12 @@ class Passport /** * Set or get the default scopes. * - * Boot-only when setting. The scopes persist for the worker lifetime and affect every request. + * Given scopes persist for the worker lifetime and affect every request. * * @param null|string|string[] $scopes * @return string[] */ + #[BootOnly(whenNotDefault: 'scopes')] public static function defaultScopes(array|string|null $scopes = null): array { if (! is_null($scopes)) { @@ -427,10 +430,11 @@ protected static function configuredScopes(): array /** * Define the scopes for the application. * - * Boot-only. The catalog persists in a static property and affects every request on the worker. + * The catalog persists in a static property and affects every request on the worker. * * @param array $scopes */ + #[BootOnly] public static function tokensCan(array $scopes): void { static::$scopes = $scopes; @@ -439,10 +443,11 @@ public static function tokensCan(array $scopes): void /** * Define the resources tokens may access. * - * Boot-only. The catalog persists in a static property and affects every request on the worker. + * The catalog persists in a static property and affects every request on the worker. * * @param array $resources */ + #[BootOnly] public static function tokensCanAccess(array $resources): void { ProtectedResource::validateIdentifiers($resources); @@ -492,8 +497,9 @@ protected static function configuredResources(): array /** * Get or set how long user approval is remembered. * - * Boot-only when setting. The duration persists for every subsequent request on the worker. + * A given duration, including null, persists for every subsequent request on the worker. */ + #[BootOnly(whenProvided: 'duration')] public static function rememberConsentFor(DateTimeInterface|DateInterval|null $duration = null): ?DateInterval { if (func_num_args() === 0) { @@ -508,8 +514,9 @@ public static function rememberConsentFor(DateTimeInterface|DateInterval|null $d /** * Get or set when access tokens expire. * - * Boot-only when setting. Cached servers retain this lifetime across requests. + * Cached servers retain a given lifetime across requests. */ + #[BootOnly(whenNotDefault: 'date')] public static function tokensExpireIn(DateTimeInterface|DateInterval|null $date = null): DateInterval { if (is_null($date)) { @@ -524,8 +531,9 @@ public static function tokensExpireIn(DateTimeInterface|DateInterval|null $date /** * Get or set when refresh tokens expire. * - * Boot-only when setting. Cached servers retain this lifetime across requests. + * Cached servers retain a given lifetime across requests. */ + #[BootOnly(whenNotDefault: 'date')] public static function refreshTokensExpireIn(DateTimeInterface|DateInterval|null $date = null): DateInterval { if (is_null($date)) { @@ -540,8 +548,9 @@ public static function refreshTokensExpireIn(DateTimeInterface|DateInterval|null /** * Get or set when personal access tokens expire. * - * Boot-only when setting. Cached servers retain this lifetime across requests. + * Cached servers retain a given lifetime across requests. */ + #[BootOnly(whenNotDefault: 'date')] public static function personalAccessTokensExpireIn(DateTimeInterface|DateInterval|null $date = null): DateInterval { if (is_null($date)) { @@ -556,8 +565,9 @@ public static function personalAccessTokensExpireIn(DateTimeInterface|DateInterv /** * Get or set when client credentials grant tokens expire. * - * Boot-only when setting. Cached servers retain this lifetime across requests. + * Cached servers retain a given lifetime across requests. */ + #[BootOnly(whenNotDefault: 'date')] public static function clientCredentialsTokensExpireIn(DateTimeInterface|DateInterval|null $date = null): DateInterval { if (is_null($date)) { @@ -572,8 +582,9 @@ public static function clientCredentialsTokensExpireIn(DateTimeInterface|DateInt /** * Get or set the maximum lifetime of proof-bound access tokens. * - * Boot-only when setting. The lifetime persists for every subsequent request on the worker. + * A given lifetime persists for every subsequent request on the worker. */ + #[BootOnly(whenNotDefault: 'date')] public static function dpopTokensExpireIn(DateTimeInterface|DateInterval|null $date = null): DateInterval { if (is_null($date)) { @@ -588,8 +599,9 @@ public static function dpopTokensExpireIn(DateTimeInterface|DateInterval|null $d /** * Get or set the name for API token cookies. * - * Boot-only when setting. The name persists in a static property and affects every request on the worker. + * A given name persists in a static property and affects every request on the worker. */ + #[BootOnly(whenNotDefault: 'cookie')] public static function cookie(?string $cookie = null): string { if (is_null($cookie)) { @@ -602,8 +614,9 @@ public static function cookie(?string $cookie = null): string /** * Indicate that Passport should ignore incoming CSRF tokens. * - * Boot-only. This policy persists in a static property and affects every request on the worker. + * This policy persists in a static property and affects every request on the worker. */ + #[BootOnly] public static function ignoreCsrfToken(bool $ignoreCsrfToken = true): void { static::$ignoreCsrfToken = $ignoreCsrfToken; @@ -612,13 +625,14 @@ public static function ignoreCsrfToken(bool $ignoreCsrfToken = true): void /** * Set the current user for the application with the given scopes. * - * Tests only. This replaces the coroutine's authenticated user and default guard with test state. + * This replaces the coroutine's authenticated user and default guard with test state. * * @param OAuthenticatable $user * @param string[] $scopes * @param list $resources * @return OAuthenticatable */ + #[TestsOnly] public static function actingAs( Authenticatable $user, array $scopes = [], @@ -650,11 +664,12 @@ public static function actingAs( /** * Set the current client for the application with the given scopes. * - * Tests only. This installs a resource-server mock and authenticated client in the coroutine's context. + * This installs a resource-server mock and authenticated client in the coroutine's context. * * @param string[] $scopes * @param list $resources */ + #[TestsOnly] public static function actingAsClient(Client $client, array $scopes = [], ?string $guard = self::DEFAULT_GUARD, array $resources = []): Client { $mock = Mockery::mock(ResourceServer::class); @@ -689,8 +704,9 @@ public static function defaultProvider(): ?string /** * Set the storage location of the encryption keys. * - * Boot-only. Cached servers retain their loaded keys for the worker lifetime. + * Cached servers retain their loaded keys for the worker lifetime. */ + #[BootOnly] public static function loadKeysFrom(string $path): void { static::$keyPath = $path; @@ -711,10 +727,11 @@ public static function keyPath(string $file): string /** * Set the access token entity class name. * - * Boot-only. The entity class persists in a static property and affects every issuance on the worker. + * The entity class persists in a static property and affects every issuance on the worker. * * @param class-string $accessTokenEntity */ + #[BootOnly] public static function useAccessTokenEntity(string $accessTokenEntity): void { static::$accessTokenEntity = $accessTokenEntity; @@ -723,10 +740,11 @@ public static function useAccessTokenEntity(string $accessTokenEntity): void /** * Set the consent model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $consentModel */ + #[BootOnly] public static function useConsentModel(string $consentModel): void { static::$consentModel = $consentModel; @@ -753,10 +771,11 @@ public static function consent(): Consent /** * Set the auth code model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $authCodeModel */ + #[BootOnly] public static function useAuthCodeModel(string $authCodeModel): void { static::$authCodeModel = $authCodeModel; @@ -783,10 +802,11 @@ public static function authCode(): AuthCode /** * Set the pushed authorization request model class name. * - * Boot-only. The model class persists for the worker lifetime and affects every request. + * The model class persists for the worker lifetime and affects every request. * * @param class-string $pushedAuthorizationRequestModel */ + #[BootOnly] public static function usePushedAuthorizationRequestModel(string $pushedAuthorizationRequestModel): void { static::$pushedAuthorizationRequestModel = $pushedAuthorizationRequestModel; @@ -813,8 +833,9 @@ public static function pushedAuthorizationRequest(): PushedAuthorizationRequest /** * Set the default ResponseType that should be used by the authorization server. * - * Boot-only. Cached servers retain this response prototype for the worker lifetime. + * Cached servers retain this response prototype for the worker lifetime. */ + #[BootOnly] public static function useAuthorizationServerResponseType(?ResponseTypeInterface $authorizationServerResponseType): void { static::$authorizationServerResponseType = $authorizationServerResponseType; @@ -823,10 +844,11 @@ public static function useAuthorizationServerResponseType(?ResponseTypeInterface /** * Set the device code model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $deviceCodeModel */ + #[BootOnly] public static function useDeviceCodeModel(string $deviceCodeModel): void { static::$deviceCodeModel = $deviceCodeModel; @@ -853,10 +875,11 @@ public static function deviceCode(): DeviceCode /** * Set the client model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $clientModel */ + #[BootOnly] public static function useClientModel(string $clientModel): void { static::$clientModel = $clientModel; @@ -883,10 +906,11 @@ public static function client(): Client /** * Set the token model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $tokenModel */ + #[BootOnly] public static function useTokenModel(string $tokenModel): void { static::$tokenModel = $tokenModel; @@ -913,10 +937,11 @@ public static function token(): Token /** * Set the refresh token model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $refreshTokenModel */ + #[BootOnly] public static function useRefreshTokenModel(string $refreshTokenModel): void { static::$refreshTokenModel = $refreshTokenModel; @@ -943,10 +968,11 @@ public static function refreshToken(): RefreshToken /** * Set the refresh token family model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $refreshTokenFamilyModel */ + #[BootOnly] public static function useRefreshTokenFamilyModel(string $refreshTokenFamilyModel): void { static::$refreshTokenFamilyModel = $refreshTokenFamilyModel; @@ -973,10 +999,11 @@ public static function refreshTokenFamily(): RefreshTokenFamily /** * Set the signing key model class name. * - * Boot-only. The model class persists in a static property and affects every request on the worker. + * The model class persists in a static property and affects every request on the worker. * * @param class-string $signingKeyModel */ + #[BootOnly] public static function useSigningKeyModel(string $signingKeyModel): void { static::$signingKeyModel = $signingKeyModel; @@ -1003,10 +1030,11 @@ public static function signingKey(): SigningKey /** * Specify an additional access token authentication check. * - * Boot-only. The callback persists in a static property and runs for every bearer authentication. + * The callback persists in a static property and runs for every bearer authentication. * * @param Closure(Token, bool): bool $callback */ + #[BootOnly] public static function authenticateAccessTokensUsing(Closure $callback): void { static::$accessTokenAuthenticationCallback = $callback; @@ -1015,10 +1043,11 @@ public static function authenticateAccessTokensUsing(Closure $callback): void /** * Specify an additional client authentication check. * - * Boot-only. The callback persists in a static property and runs for every client authentication. + * The callback persists in a static property and runs for every client authentication. * * @param Closure(Client): bool $callback */ + #[BootOnly] public static function authenticateClientsUsing(Closure $callback): void { static::$clientAuthenticationCallback = $callback; @@ -1027,10 +1056,11 @@ public static function authenticateClientsUsing(Closure $callback): void /** * Authorize resource-server clients to inspect another client's access token. * - * Boot-only. The callback persists in a static property and affects every introspection request. + * The callback persists in a static property and affects every introspection request. * * @param Closure(Client, Token): bool $callback */ + #[BootOnly] public static function authorizeIntrospectionUsing(Closure $callback): void { static::$introspectionAuthorizationCallback = $callback; @@ -1039,10 +1069,11 @@ public static function authorizeIntrospectionUsing(Closure $callback): void /** * Specify the callback resolving protected-resource metadata for authentication challenges. * - * Boot-only. The callback persists in a static property and affects every authentication challenge on the worker. + * The callback persists in a static property and affects every authentication challenge on the worker. * * @param null|(Closure(Request): ?string) $callback */ + #[BootOnly] public static function resolveResourceMetadataUsing(?Closure $callback): void { static::$resourceMetadataCallback = $callback; @@ -1051,10 +1082,11 @@ public static function resolveResourceMetadataUsing(?Closure $callback): void /** * Specify grant attributes to carry into issued tokens. * - * Boot-only. The attribute list persists in a static property and affects every issuance on the worker. + * The attribute list persists in a static property and affects every issuance on the worker. * * @param list $attributes */ + #[BootOnly] public static function inheritGrantAttributes(array $attributes): void { static::$inheritedGrantAttributes = $attributes; @@ -1063,10 +1095,11 @@ public static function inheritGrantAttributes(array $attributes): void /** * Specify the binding included in and checked against API cookies. * - * Boot-only. The callback persists in a static property and affects cookies across all requests. + * The callback persists in a static property and affects cookies across all requests. * * @param Closure(Request): ?string $callback */ + #[BootOnly] public static function bindCookiesUsing(Closure $callback): void { static::$cookieBindingCallback = $callback; @@ -1075,10 +1108,11 @@ public static function bindCookiesUsing(Closure $callback): void /** * Specify additional claims for issued access tokens. * - * Boot-only. The callback persists in a static property and affects every issuance on the worker. + * The callback persists in a static property and affects every issuance on the worker. * * @param Closure(Token): array $callback */ + #[BootOnly] public static function accessTokenClaimsUsing(Closure $callback): void { static::$accessTokenClaimsCallback = $callback; @@ -1087,10 +1121,11 @@ public static function accessTokenClaimsUsing(Closure $callback): void /** * Specify whether an access token may receive a refresh token. * - * Boot-only. The callback persists in a static property and affects every issuance on the worker. + * The callback persists in a static property and affects every issuance on the worker. * * @param Closure(Token): bool $callback */ + #[BootOnly] public static function issueRefreshTokensUsing(Closure $callback): void { static::$issueRefreshTokensCallback = $callback; @@ -1099,10 +1134,11 @@ public static function issueRefreshTokensUsing(Closure $callback): void /** * Specify the callback that should be invoked to generate encryption keys for encrypting JWT tokens. * - * Boot-only. The callback supplies random key material that cached servers retain for the worker lifetime. + * The callback supplies random key material that cached servers retain for the worker lifetime. * * @param null|(Closure(Encrypter): string) $callback */ + #[BootOnly] public static function encryptTokensUsing(?Closure $callback): void { static::$tokenEncryptionKeyCallback = $callback; @@ -1121,8 +1157,9 @@ public static function tokenEncryptionKey(Encrypter $encrypter): string /** * Register the views for Passport using conventional names under the given namespace. * - * Boot-only. The response bindings persist in the worker's container and affect subsequent requests. + * The response bindings persist in the worker's container and affect subsequent requests. */ + #[BootOnly] public static function viewNamespace(string $namespace): void { static::viewPrefix($namespace . '::'); @@ -1131,8 +1168,9 @@ public static function viewNamespace(string $namespace): void /** * Register the views for Passport using conventional names under the given prefix. * - * Boot-only. The response bindings persist in the worker's container and affect subsequent requests. + * The response bindings persist in the worker's container and affect subsequent requests. */ + #[BootOnly] public static function viewPrefix(string $prefix): void { $prefix = str_ends_with($prefix, '::') ? $prefix : rtrim($prefix, '.') . '.'; @@ -1145,10 +1183,11 @@ public static function viewPrefix(string $prefix): void /** * Specify which view should be used as the authorization view. * - * Boot-only. The response binding persists in the worker's container and affects subsequent requests. + * The response binding persists in the worker's container and affects subsequent requests. * * @param (Closure(array): mixed)|string $view */ + #[BootOnly] public static function authorizationView(Closure|string $view): void { // withParameters() stores consent data, so each resolution needs its own response. @@ -1158,10 +1197,11 @@ public static function authorizationView(Closure|string $view): void /** * Specify which view should be used to select the approving account. * - * Boot-only. The response binding persists in the worker's container and affects subsequent requests. + * The response binding persists in the worker's container and affects subsequent requests. * * @param (Closure(array): mixed)|string $view */ + #[BootOnly] public static function accountSelectionView(Closure|string $view): void { app()->bind(AccountSelectionViewResponse::class, fn () => new SimpleViewResponse($view)); @@ -1170,10 +1210,11 @@ public static function accountSelectionView(Closure|string $view): void /** * Specify which view should be used as the device authorization view. * - * Boot-only. The response binding persists in the worker's container and affects subsequent requests. + * The response binding persists in the worker's container and affects subsequent requests. * * @param (Closure(array): mixed)|string $view */ + #[BootOnly] public static function deviceAuthorizationView(Closure|string $view): void { app()->bind(DeviceAuthorizationViewResponse::class, fn () => new SimpleViewResponse($view)); @@ -1182,10 +1223,11 @@ public static function deviceAuthorizationView(Closure|string $view): void /** * Specify which view should be used as the device user code view. * - * Boot-only. The response binding persists in the worker's container and affects subsequent requests. + * The response binding persists in the worker's container and affects subsequent requests. * * @param (Closure(array): mixed)|string $view */ + #[BootOnly] public static function deviceUserCodeView(Closure|string $view): void { app()->bind(DeviceUserCodeViewResponse::class, fn () => new SimpleViewResponse($view)); @@ -1194,8 +1236,9 @@ public static function deviceUserCodeView(Closure|string $view): void /** * Configure Passport to not register its routes. * - * Boot-only. This changes route registration for the worker's application. + * This changes route registration for the worker's application. */ + #[BootOnly] public static function ignoreRoutes(): void { static::$registersRoutes = false; @@ -1204,8 +1247,9 @@ public static function ignoreRoutes(): void /** * Enable the dynamic client registration endpoint. * - * Boot-only. Call during provider registration, before Passport registers routes for the worker. + * Call during provider registration, before Passport registers routes for the worker. */ + #[BootOnly] public static function enableDynamicClientRegistration(): void { static::$dynamicClientRegistrationEnabled = true; @@ -1214,8 +1258,9 @@ public static function enableDynamicClientRegistration(): void /** * Instruct Passport to enable cookie serialization. * - * Boot-only. The policy persists in a static property and affects every request on the worker. + * The policy persists in a static property and affects every request on the worker. */ + #[BootOnly] public static function withCookieSerialization(): void { static::$unserializesCookies = true; @@ -1224,8 +1269,9 @@ public static function withCookieSerialization(): void /** * Instruct Passport to disable cookie serialization. * - * Boot-only. The policy persists in a static property and affects every request on the worker. + * The policy persists in a static property and affects every request on the worker. */ + #[BootOnly] public static function withoutCookieSerialization(): void { static::$unserializesCookies = false; @@ -1234,8 +1280,9 @@ public static function withoutCookieSerialization(): void /** * Instruct Passport to enable cookie encryption. * - * Boot-only. The policy persists in a static property and affects every request on the worker. + * The policy persists in a static property and affects every request on the worker. */ + #[BootOnly] public static function withCookieEncryption(): void { static::$decryptsCookies = true; @@ -1244,8 +1291,9 @@ public static function withCookieEncryption(): void /** * Instruct Passport to disable cookie encryption. * - * Boot-only. The policy persists in a static property and affects every request on the worker. + * The policy persists in a static property and affects every request on the worker. */ + #[BootOnly] public static function withoutCookieEncryption(): void { static::$decryptsCookies = false; diff --git a/src/pennant/composer.json b/src/pennant/composer.json index 76780fb71d..bf1b5bcd7c 100644 --- a/src/pennant/composer.json +++ b/src/pennant/composer.json @@ -28,6 +28,7 @@ "require": { "php": "^8.4", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", diff --git a/src/pennant/src/Contracts/Driver.php b/src/pennant/src/Contracts/Driver.php index 3cd2d69480..f6b211db5c 100644 --- a/src/pennant/src/Contracts/Driver.php +++ b/src/pennant/src/Contracts/Driver.php @@ -4,6 +4,8 @@ namespace Hypervel\Pennant\Contracts; +use Hypervel\Support\Attributes\BootOrTestsOnly; + interface Driver { /** @@ -11,6 +13,7 @@ interface Driver * * @param (callable(mixed $scope): mixed) $resolver */ + #[BootOrTestsOnly(whenShared: true)] public function define(string $feature, callable $resolver): void; /** diff --git a/src/pennant/src/Drivers/ArrayDriver.php b/src/pennant/src/Drivers/ArrayDriver.php index 53418efdf4..70efbc8583 100644 --- a/src/pennant/src/Drivers/ArrayDriver.php +++ b/src/pennant/src/Drivers/ArrayDriver.php @@ -13,6 +13,7 @@ use Hypervel\Pennant\Feature; use Hypervel\Pennant\FeaturePartition; use Hypervel\Pennant\PennantState; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Facades\Event; use stdClass; @@ -63,11 +64,12 @@ public function __construct(string $name, ?Closure $partitionResolver, array $fe /** * Define an initial feature flag state resolver. * - * Boot or tests only. The definition persists on the store for the worker - * lifetime and applies to every subsequent request. + * On a shared store, the definition persists for the worker lifetime and + * applies to every subsequent request. * * @param (callable(mixed $scope): mixed) $resolver */ + #[BootOrTestsOnly(whenShared: true)] public function define(string $feature, callable $resolver): void { $this->featureStateResolvers[$feature] = $resolver; diff --git a/src/pennant/src/Drivers/DatabaseDriver.php b/src/pennant/src/Drivers/DatabaseDriver.php index 4f5551e0a1..8b46d7a477 100644 --- a/src/pennant/src/Drivers/DatabaseDriver.php +++ b/src/pennant/src/Drivers/DatabaseDriver.php @@ -18,6 +18,7 @@ use Hypervel\Pennant\Events\UnknownFeatureResolved; use Hypervel\Pennant\Feature; use Hypervel\Pennant\FeaturePartition; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Event; use InvalidArgumentException; @@ -130,11 +131,12 @@ public function __construct( /** * Define an initial feature flag state resolver. * - * Boot or tests only. The definition persists on the store for the worker - * lifetime and applies to every subsequent request. + * On a shared store, the definition persists for the worker lifetime and + * applies to every subsequent request. * * @param (callable(mixed $scope): mixed) $resolver */ + #[BootOrTestsOnly(whenShared: true)] public function define(string $feature, callable $resolver): void { $this->featureStateResolvers[$feature] = $resolver; diff --git a/src/pennant/src/Drivers/Decorator.php b/src/pennant/src/Drivers/Decorator.php index b1ee267b01..de5cd58406 100644 --- a/src/pennant/src/Drivers/Decorator.php +++ b/src/pennant/src/Drivers/Decorator.php @@ -33,6 +33,8 @@ use Hypervel\Pennant\PendingScopedFeatureInteraction; use Hypervel\Pennant\PennantState; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Enumerable; use Hypervel\Support\Facades\Event; @@ -149,9 +151,10 @@ public function __construct(string $name, Driver $driver, Closure $defaultScopeR /** * Discover and register the application's feature classes. * - * Boot or tests only. The definitions persist on the store for the worker - * lifetime and apply to every subsequent request. + * On a shared store, the definitions persist for the worker lifetime and + * add every discovered class to all() for subsequent requests. */ + #[BootOrTestsOnly(whenShared: true)] public function discover(string $namespace = 'App\Features', ?string $path = null): void { $namespace = Str::finish($namespace, '\\'); @@ -167,12 +170,13 @@ public function discover(string $namespace = 'App\Features', ?string $path = nul /** * Define an initial feature flag state resolver. * - * Boot or tests only. The definition persists on the store for the worker - * lifetime and applies to every subsequent request. + * On a shared store, the definition persists for the worker lifetime and + * applies to every subsequent request. * * @param class-string|string|UnitEnum $feature * @param mixed $resolver a resolver closure, a lottery, or the feature's value */ + #[BootOrTestsOnly(whenShared: true)] public function define(UnitEnum|string $feature, mixed $resolver = null): void { $feature = enum_value($feature); @@ -1048,10 +1052,11 @@ public function getDriver(): Driver /** * Set the container instance used by the decorator. * - * Tests only. The container persists on the store for the worker lifetime. + * On a shared store, the container persists for the worker lifetime. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/pennant/src/FeatureManager.php b/src/pennant/src/FeatureManager.php index d7cde1fbaa..bb55949bfc 100644 --- a/src/pennant/src/FeatureManager.php +++ b/src/pennant/src/FeatureManager.php @@ -15,6 +15,9 @@ use Hypervel\Pennant\Drivers\Decorator; use Hypervel\Pennant\Drivers\StoredValueCache; use Hypervel\Pennant\Exceptions\FeaturePartitionNotResolved; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; use RuntimeException; @@ -300,11 +303,12 @@ public function serializeScope(mixed $scope): string /** * Specify that the Eloquent morph map should be used when serializing. * - * Boot-only. The setting persists on the manager for the worker lifetime and + * On the shared manager, the setting persists for the worker lifetime and * changes how every subsequent request serializes model scopes. * * @return $this */ + #[BootOnly(whenShared: true)] public function useMorphMap(bool $value = true): static { $this->useMorphMap = $value; @@ -343,11 +347,12 @@ protected function defaultScopeResolver(string $driver): Closure /** * Set the default scope resolver. * - * Boot-only. The resolver persists on the manager for the worker lifetime and + * On the shared manager, the resolver persists for the worker lifetime and * applies to every subsequent request. * * @param callable(string): mixed $resolver */ + #[BootOnly(whenShared: true)] public function resolveScopeUsing(callable $resolver): void { $this->defaultScopeResolver = $resolver(...); @@ -359,7 +364,7 @@ public function resolveScopeUsing(callable $resolver): void * The resolver receives the store name and returns the current partition value. Stores * configured with "partitioned" set to false are not partitioned. * - * Boot-only. Register before any store is resolved. The resolver persists on the manager + * Register before any store is resolved. On the shared manager, the resolver persists * for the worker lifetime and applies to every subsequent request. * * @param Closure(string): (null|int|string) $resolver @@ -367,6 +372,7 @@ public function resolveScopeUsing(callable $resolver): void * @throws InvalidArgumentException when the column is not a simple SQL identifier * @throws RuntimeException when a store has already been resolved or partitioning is already configured */ + #[BootOnly(whenShared: true)] public function resolvePartitionUsing(string $column, Closure $resolver): void { if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/D', $column) !== 1) { @@ -389,13 +395,14 @@ public function resolvePartitionUsing(string $column, Closure $resolver): void /** * Guard database writes, including the first resolution of a stored value. * - * Boot-only. Register once, before any store is resolved. The callback persists - * on database stores for the worker lifetime and affects every subsequent write. + * Register once, before any store is resolved. On the shared manager, the callback + * persists on database stores for the worker lifetime and affects every subsequent write. * * @param Closure(string, ?FeaturePartition): void $guard * * @throws RuntimeException when a store has already been resolved or a guard is already registered */ + #[BootOnly(whenShared: true)] public function guardWritesUsing(Closure $guard): void { if ($this->stores !== []) { @@ -456,9 +463,9 @@ public function getDefaultDriver(): string /** * Set the default store name. * - * Boot-only. The default store is process-global config shared by every - * request in the worker. + * The default store is process-global config shared by every request in the worker. */ + #[BootOnly] public function setDefaultDriver(string $name): void { $this->container->make('config')->set('pennant.default', $name); @@ -467,12 +474,13 @@ public function setDefaultDriver(string $name): void /** * Unset the given store instances. * - * Boot or tests only. Stores are shared by every request in the worker, and - * the current coroutine's values for a forgotten store are flushed. + * The shared manager's stores serve every request in the worker, and the + * current coroutine's values for a forgotten store are flushed. * * @param null|array|string $name * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetDriver(array|string|null $name = null): static { $name ??= $this->getDefaultDriver(); @@ -491,11 +499,12 @@ public function forgetDriver(array|string|null $name = null): static /** * Forget all of the resolved store instances. * - * Boot or tests only. Stores are shared by every request in the worker, and - * the current coroutine's values for the forgotten stores are flushed. + * The shared manager's stores serve every request in the worker, and the + * current coroutine's values for the forgotten stores are flushed. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetDrivers(): static { foreach ($this->stores as $store) { @@ -510,10 +519,11 @@ public function forgetDrivers(): static /** * Register a custom driver creator Closure. * - * Boot-only. The creator persists on the manager for the worker lifetime. + * On the shared manager, the creator persists for the worker lifetime. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback): static { $this->customCreators[$driver] = $callback->bindTo($this, $this); @@ -524,11 +534,12 @@ public function extend(string $driver, Closure $callback): static /** * Set the container instance used by the manager. * - * Tests only. The container persists on the manager and its stores for the - * worker lifetime. + * On the shared manager, the container persists on it and its stores for + * the worker lifetime. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/pennant/src/Middleware/EnsureFeaturesAreActive.php b/src/pennant/src/Middleware/EnsureFeaturesAreActive.php index 137b39fa14..94b46e2ea0 100644 --- a/src/pennant/src/Middleware/EnsureFeaturesAreActive.php +++ b/src/pennant/src/Middleware/EnsureFeaturesAreActive.php @@ -8,6 +8,7 @@ use Closure; use Hypervel\Http\Request; use Hypervel\Pennant\Feature; +use Hypervel\Support\Attributes\BootOnly; use UnitEnum; use function Hypervel\Pennant\enum_value; @@ -52,9 +53,10 @@ public static function using(BackedEnum|UnitEnum|string ...$features): string /** * Specify a callback that should be used to generate responses for failed feature checks. * - * Boot-only. The callback persists in a static property for the worker lifetime - * and affects every subsequent request. + * The callback persists in a static property for the worker lifetime and + * affects every subsequent request. */ + #[BootOnly] public static function whenInactive(?Closure $callback): void { static::$respondUsing = $callback; diff --git a/src/permission/composer.json b/src/permission/composer.json index 21691389e7..4dc250786d 100644 --- a/src/permission/composer.json +++ b/src/permission/composer.json @@ -34,6 +34,7 @@ "require": { "php": "^8.4", "composer-runtime-api": "^2.2", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/cache": "^0.4", diff --git a/src/permission/src/PermissionRegistrar.php b/src/permission/src/PermissionRegistrar.php index 178ea40b0b..0796c414cd 100644 --- a/src/permission/src/PermissionRegistrar.php +++ b/src/permission/src/PermissionRegistrar.php @@ -36,6 +36,8 @@ use Hypervel\Permission\Support\PermissionPartition; use Hypervel\Permission\Support\PermissionRelationContext; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\Str; use InvalidArgumentException; @@ -146,11 +148,12 @@ public function __construct( * * Register before resolving the Permission registrar or Gate. * - * Boot-only. The column and callback persist in static properties for the - * worker lifetime and affect every subsequent permission operation. + * The column and callback persist in static properties for the worker + * lifetime and affect every subsequent permission operation. * * @param Closure(): (null|int|string) $resolver */ + #[BootOnly] public static function resolvePartitionUsing(string $column, Closure $resolver): void { if (static::$initialized || static::$partitionResolver !== null) { @@ -262,9 +265,10 @@ public function ensureModelUsesPermissionConnection(Model $model): void /** * Initialize cache and config-backed registrar state. * - * Boot or tests only. The values are stored on the singleton registrar and - * affect every later permission lookup in this worker. + * The values are stored on the singleton registrar and affect every later + * permission lookup in this worker. */ + #[BootOrTestsOnly(whenShared: true)] public function initializeCache(): void { /** @var class-string $permissionClass */ @@ -1768,11 +1772,12 @@ protected function partitionRuntimePrefix(?PermissionPartition $partition): stri /** * Set the permission model class. * - * Boot or tests only. The model class is stored on the singleton registrar - * and affects every later permission lookup in this worker. + * The model class is stored on the singleton registrar and affects every + * later permission lookup in this worker. * * @param class-string $permissionClass */ + #[BootOrTestsOnly] public function setPermissionClass(string $permissionClass): static { $this->validatePermissionClass($permissionClass); @@ -1797,11 +1802,12 @@ public function getRoleClass(): string /** * Set the role model class. * - * Boot or tests only. The model class is stored on the singleton registrar - * and affects every later role lookup in this worker. + * The model class is stored on the singleton registrar and affects every + * later role lookup in this worker. * * @param class-string $roleClass */ + #[BootOrTestsOnly] public function setRoleClass(string $roleClass): static { $this->validateRoleClass($roleClass); @@ -1845,11 +1851,12 @@ public function getAssignmentPivotClass(Model $model, string $relation): string /** * Set the team model class. * - * Boot or tests only. The model class is stored on the singleton registrar - * and affects every later team permission lookup in this worker. + * The model class is stored on the singleton registrar and affects every + * later team permission lookup in this worker. * * @param null|class-string $teamClass */ + #[BootOrTestsOnly] public function setTeamClass(?string $teamClass): static { $this->teamClass = $teamClass; diff --git a/src/phpstan/src/Collectors/WorkerStateClassCollector.php b/src/phpstan/src/Collectors/WorkerStateClassCollector.php index 1f28be6b4c..53ccf00424 100644 --- a/src/phpstan/src/Collectors/WorkerStateClassCollector.php +++ b/src/phpstan/src/Collectors/WorkerStateClassCollector.php @@ -62,8 +62,10 @@ public function processNode(Node $node, Scope $scope): ?array $constructor = $node->getOriginalNode()->getMethod('__construct'); - // A constructor documented as boot-time or test-only keeps its parameters by design. - $parameters = $this->helper->hasLifetimeTag($constructor?->getDocComment()?->getText()) ? [] : $constructor->params ?? []; + // A constructor marked as boot-time or test-only keeps its parameters by design. + $parameters = $constructor === null || $this->helper->hasLifetimeAttribute($class->getNativeMethod('__construct')) + ? [] + : $constructor->params; foreach ($parameters as $parameter) { if ($parameter->flags === 0 || ! $parameter->var instanceof Variable || ! is_string($parameter->var->name)) { diff --git a/src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php b/src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php index 53ba7f563b..50f0c44322 100644 --- a/src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php +++ b/src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php @@ -10,6 +10,9 @@ use Hypervel\Contracts\Queue\ShouldQueue; use Hypervel\Database\Eloquent\Model; use Hypervel\Foundation\Http\FormRequest; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Facades\App; use Hypervel\Support\Facades\Auth; use Hypervel\Support\Facades\Request as RequestFacade; @@ -26,6 +29,9 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ReflectionProvider; @@ -44,7 +50,6 @@ use function array_map; use function in_array; use function is_string; -use function preg_match; use function strtolower; /** @@ -86,9 +91,9 @@ class WorkerStateHelper private const array RESOLVING_METHODS = ['make' => ['abstract', true], 'get' => ['id', false], 'factory' => ['abstract', false]]; /** - * The documented warning tags of boot-time and test-only mutators. + * The attributes that restrict a method to boot time or tests. */ - private const string LIFETIME_TAG_PATTERN = '/^\s*(?:\/\*\*|\*)?\s*(?:Boot-only|Tests only|Boot or tests only)\b/m'; + private const array LIFETIME_ATTRIBUTES = [BootOnly::class, TestsOnly::class, BootOrTestsOnly::class]; /** * Create a new worker state helper instance. @@ -120,7 +125,7 @@ public function isEnabled(): bool /** * Get the facts of the function the scope's code at the node runs in, or null when it is a - * flushState() method or one documented as boot-time or test-only. + * flushState() method or one marked as boot-time or test-only. * * The context is the operation the code always runs in when the class alone decides it: a queued * class's handle(), __invoke() or failed(), or a form request's method. The rule adds booted @@ -133,7 +138,7 @@ public function getFunctionFacts(Scope $scope, Node $node): ?array { $function = $scope->getFunction(); - if ($function !== null && ($function->getName() === 'flushState' || $this->hasLifetimeTag($function->getDocComment()))) { + if ($function !== null && ($function->getName() === 'flushState' || $this->hasLifetimeAttribute($function))) { return null; } @@ -158,11 +163,14 @@ public function getFunctionFacts(Scope $scope, Node $node): ?array } /** - * Determine whether the docblock documents its method as boot-time or test-only. + * Determine whether the function is marked as running only while the application boots or in tests. */ - public function hasLifetimeTag(?string $docComment): bool + public function hasLifetimeAttribute(FunctionReflection|ExtendedMethodReflection $function): bool { - return preg_match(self::LIFETIME_TAG_PATTERN, $docComment ?? '') === 1; + return array_any( + $function->getAttributes(), + static fn (AttributeReflection $attribute): bool => in_array($attribute->getName(), self::LIFETIME_ATTRIBUTES, true), + ); } /** diff --git a/src/phpstan/stubs/common/Config/Repository.stub b/src/phpstan/stubs/common/Config/Repository.stub index 01245dcd48..4c95b3cadf 100644 --- a/src/phpstan/stubs/common/Config/Repository.stub +++ b/src/phpstan/stubs/common/Config/Repository.stub @@ -25,9 +25,8 @@ class Repository /** * Set a configuration option. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. * * @param int|string $offset * @param mixed $value @@ -37,9 +36,8 @@ class Repository /** * Unset a configuration option. * - * Boot or tests only. The config repository is a process-global singleton; - * per-request mutation races across coroutines and affects every concurrent - * request. + * The config repository is a process-global singleton; per-request mutation + * races across coroutines and affects every concurrent request. * * @param int|string $offset */ diff --git a/src/pipeline/composer.json b/src/pipeline/composer.json index 6f050950c7..410509a3d4 100644 --- a/src/pipeline/composer.json +++ b/src/pipeline/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/contracts": "^0.4", "hypervel/database": "^0.4", diff --git a/src/pipeline/src/Hub.php b/src/pipeline/src/Hub.php index a1feccc832..cbbfcb8b27 100644 --- a/src/pipeline/src/Hub.php +++ b/src/pipeline/src/Hub.php @@ -7,6 +7,8 @@ use Closure; use Hypervel\Contracts\Container\Container; use Hypervel\Contracts\Pipeline\Hub as HubContract; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; class Hub implements HubContract @@ -32,11 +34,11 @@ public function __construct(?Container $container = null) /** * Define the default named pipeline. * - * Boot-only. The callback persists on the singleton Hub for the worker - * lifetime and is executed on every subsequent Hub::pipe() call that does - * not specify a pipeline name. Per-request registration races across - * coroutines. + * The callback persists on the singleton Hub for the worker lifetime and + * is executed on every subsequent Hub::pipe() call that does not specify + * a pipeline name. Per-request registration races across coroutines. */ + #[BootOnly(whenShared: true)] public function defaults(Closure $callback): void { $this->pipeline('default', $callback); @@ -45,10 +47,11 @@ public function defaults(Closure $callback): void /** * Define a new named pipeline. * - * Boot-only. The callback persists on the singleton Hub for the worker - * lifetime and is executed on every subsequent Hub::pipe() call for this - * name. Per-request registration races across coroutines. + * The callback persists on the singleton Hub for the worker lifetime and + * is executed on every subsequent Hub::pipe() call for this name. + * Per-request registration races across coroutines. */ + #[BootOnly(whenShared: true)] public function pipeline(string $name, Closure $callback): void { $this->pipelines[$name] = $callback; @@ -83,12 +86,13 @@ public function getContainer(): ?Container /** * Set the container instance used by the hub. * - * Tests only. Swaps the container on the singleton Hub used by every - * coroutine; per-request use races across coroutines and breaks every - * concurrent pipeline dispatch. + * Swaps the container on the singleton Hub used by every coroutine; + * per-request use races across coroutines and breaks every concurrent + * pipeline dispatch. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/process/composer.json b/src/process/composer.json index 6017781333..1aa54933ee 100644 --- a/src/process/composer.json +++ b/src/process/composer.json @@ -24,6 +24,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/context": "^0.4", diff --git a/src/process/src/Factory.php b/src/process/src/Factory.php index dadd33d9c1..89d6ac99ff 100644 --- a/src/process/src/Factory.php +++ b/src/process/src/Factory.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Contracts\Process\ProcessResult as ProcessResultContract; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Traits\Macroable; use PHPUnit\Framework\Assert as PHPUnit; @@ -76,10 +77,11 @@ public function sequence(array $processes = []): FakeProcessSequence /** * Indicate that the process factory should fake processes. * - * Tests only. Fake handlers and recordings persist on the container-resolved factory for the worker lifetime and affect later calls. + * Fake handlers and recordings persist on the container-resolved factory for the worker lifetime and affect later calls. * * @return $this */ + #[TestsOnly(whenShared: true)] public function fake(array|Closure|null $callback = null): static { $this->recording = true; @@ -130,10 +132,11 @@ public function recordIfRecording(PendingProcess $process, ProcessResultContract /** * Record the given process. * - * Tests only. Recorded processes are retained on the worker factory and affect later assertions and memory retention. + * Recorded processes are retained on the worker factory and affect later assertions and memory retention. * * @return $this */ + #[TestsOnly(whenShared: true)] public function record(PendingProcess $process, ProcessResultContract $result): static { $this->recorded[] = [$process, $result]; @@ -144,10 +147,11 @@ public function record(PendingProcess $process, ProcessResultContract $result): /** * Indicate that an exception should be thrown if any process is not faked. * - * Tests only. This setting persists on the worker factory and can reject every later unmatched process. + * This setting persists on the worker factory and can reject every later unmatched process. * * @return $this */ + #[TestsOnly(whenShared: true)] public function preventStrayProcesses(bool $prevent = true): static { $this->preventStrayProcesses = $prevent; diff --git a/src/prompts/composer.json b/src/prompts/composer.json index 558108619e..b16271d00e 100644 --- a/src/prompts/composer.json +++ b/src/prompts/composer.json @@ -34,12 +34,13 @@ "require": { "php": "^8.4", "ext-mbstring": "*", - "symfony/console": "^8.1.10", - "symfony/process": "^8.1", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/context": "^0.4", "hypervel/coroutine": "^0.4", - "hypervel/engine": "^0.4" + "hypervel/engine": "^0.4", + "symfony/console": "^8.1.10", + "symfony/process": "^8.1" }, "require-dev": { "mockery/mockery": "^1.6.15" diff --git a/src/prompts/src/Concerns/Cursor.php b/src/prompts/src/Concerns/Cursor.php index c400b4032a..c7618adf85 100644 --- a/src/prompts/src/Concerns/Cursor.php +++ b/src/prompts/src/Concerns/Cursor.php @@ -4,6 +4,8 @@ namespace Hypervel\Prompts\Concerns; +use Hypervel\Support\Attributes\BootOrTestsOnly; + trait Cursor { /** @@ -106,10 +108,10 @@ public function moveCursorUp(int $lines): void /** * Reset cursor state to defaults. * - * Boot or tests only. Clears worker-wide cursor visibility tracking; - * concurrent prompts may restore cursor state differently depending on - * timing. + * Clears worker-wide cursor visibility tracking; concurrent prompts + * may restore cursor state differently depending on timing. */ + #[BootOrTestsOnly] public static function resetCursor(): void { static::$cursorHidden = false; diff --git a/src/prompts/src/Concerns/FakesInputOutput.php b/src/prompts/src/Concerns/FakesInputOutput.php index 2f4b832fc3..477e47ceb7 100644 --- a/src/prompts/src/Concerns/FakesInputOutput.php +++ b/src/prompts/src/Concerns/FakesInputOutput.php @@ -7,6 +7,7 @@ use Hypervel\Prompts\Output\BufferedConsoleOutput; use Hypervel\Prompts\Support\Utils; use Hypervel\Prompts\Terminal; +use Hypervel\Support\Attributes\TestsOnly; use Mockery; use Mockery\MockInterface; use PHPUnit\Framework\Assert; @@ -17,11 +18,11 @@ trait FakesInputOutput /** * Fake the terminal and queue key presses to be simulated. * - * Tests only. Swaps prompt terminal and output state for the worker - * lifetime. + * Swaps prompt terminal and output state for the worker lifetime. * * @param array $keys */ + #[TestsOnly] public static function fake(array $keys = []): void { // Force interactive mode when testing because we will be mocking the terminal. diff --git a/src/prompts/src/Concerns/Fallback.php b/src/prompts/src/Concerns/Fallback.php index 092f187ae7..01a8cb5f3f 100644 --- a/src/prompts/src/Concerns/Fallback.php +++ b/src/prompts/src/Concerns/Fallback.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Context\CoroutineContext; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOrTestsOnly; use RuntimeException; trait Fallback @@ -100,10 +101,10 @@ public function fallback(): mixed /** * Reset fallback state to defaults. * - * Boot or tests only. Clears worker-wide prompt fallback configuration; - * concurrent prompts may render with different fallback behavior depending - * on timing. + * Clears worker-wide prompt fallback configuration; concurrent prompts may + * render with different fallback behavior depending on timing. */ + #[BootOrTestsOnly] public static function resetFallback(): void { static::$shouldFallback = false; diff --git a/src/prompts/src/Concerns/Interactivity.php b/src/prompts/src/Concerns/Interactivity.php index 26b5f17b3f..bb0ada3d25 100644 --- a/src/prompts/src/Concerns/Interactivity.php +++ b/src/prompts/src/Concerns/Interactivity.php @@ -7,6 +7,7 @@ use Hypervel\Context\CoroutineContext; use Hypervel\Coroutine\Coroutine; use Hypervel\Prompts\Exceptions\NonInteractiveValidationException; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait Interactivity { @@ -49,10 +50,11 @@ public static function isInteractive(): ?bool /** * Reset interactivity state to defaults. * - * Boot or tests only. Clears worker-wide prompt interactivity - * configuration; concurrent prompts may render with different - * interactivity depending on timing. + * Clears worker-wide prompt interactivity configuration; + * concurrent prompts may render with different interactivity + * depending on timing. */ + #[BootOrTestsOnly] public static function resetInteractivity(): void { static::$interactive = null; diff --git a/src/prompts/src/Concerns/Themes.php b/src/prompts/src/Concerns/Themes.php index 6a3e75c6b9..2c634fbb74 100644 --- a/src/prompts/src/Concerns/Themes.php +++ b/src/prompts/src/Concerns/Themes.php @@ -51,6 +51,8 @@ use Hypervel\Prompts\Themes\Default\TextPromptRenderer; use Hypervel\Prompts\Themes\Default\TitleRenderer; use Hypervel\Prompts\Title; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; trait Themes @@ -108,11 +110,12 @@ trait Themes /** * Get or set the active theme. * - * Boot-only when setting. The active theme persists in a static property - * for the worker lifetime and applies to every subsequent prompt render. + * A given theme persists in a static property for the worker lifetime and + * applies to every subsequent prompt render. * * @throws InvalidArgumentException */ + #[BootOnly(whenNotDefault: 'name')] public static function theme(?string $name = null): string { if ($name === null) { @@ -129,11 +132,12 @@ public static function theme(?string $name = null): string /** * Add a new theme. * - * Boot-only. Themes persist in a static property for the worker lifetime - * and are available to every subsequent prompt render. + * Themes persist in a static property for the worker lifetime and are + * available to every subsequent prompt render. * * @param array, class-string> $renderers */ + #[BootOnly] public static function addTheme(string $name, array $renderers): void { if ($name === self::DEFAULT_THEME) { @@ -171,9 +175,10 @@ protected function renderTheme(): string /** * Reset theme state to defaults. * - * Boot or tests only. Clears worker-wide prompt theme configuration; - * concurrent prompts may render with different themes depending on timing. + * Clears worker-wide prompt theme configuration; concurrent prompts may + * render with different themes depending on timing. */ + #[BootOrTestsOnly] public static function resetTheme(): void { static::$theme = self::DEFAULT_THEME; diff --git a/src/prompts/src/Progress.php b/src/prompts/src/Progress.php index 0153cbbe53..2a2bb95412 100644 --- a/src/prompts/src/Progress.php +++ b/src/prompts/src/Progress.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\Unsupported; use InvalidArgumentException; use RuntimeException; use Throwable; @@ -275,6 +276,7 @@ public function percentage(): float|int * * @throws RuntimeException */ + #[Unsupported('Use map(), or start(), advance() and finish().')] public function prompt(): never { throw new RuntimeException('Progress Bar cannot be prompted.'); diff --git a/src/prompts/src/Prompt.php b/src/prompts/src/Prompt.php index ffc39ef0ff..0c1c0387c5 100644 --- a/src/prompts/src/Prompt.php +++ b/src/prompts/src/Prompt.php @@ -11,6 +11,7 @@ use Hypervel\Prompts\Output\ConsoleOutput; use Hypervel\Prompts\Support\Result; use Hypervel\Prompts\Support\Utils; +use Hypervel\Support\Attributes\BootOnly; use RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Output\OutputInterface; @@ -257,9 +258,10 @@ public function runLoop(callable $callable): mixed /** * Register a callback to be invoked when a user cancels a prompt. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every subsequent prompt cancellation. + * The callback persists in a static property for the worker lifetime + * and runs on every subsequent prompt cancellation. */ + #[BootOnly] public static function cancelUsing(?Closure $callback): void { static::$cancelUsing = $callback; diff --git a/src/prompts/src/Spinner.php b/src/prompts/src/Spinner.php index 5fded63724..d5505f2422 100644 --- a/src/prompts/src/Spinner.php +++ b/src/prompts/src/Spinner.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Coroutine\Coroutine; use Hypervel\Prompts\Support\PromptAnimation; +use Hypervel\Support\Attributes\Unsupported; use RuntimeException; use Throwable; @@ -156,6 +157,7 @@ private function settleOperation(): ?Throwable * * @throws RuntimeException */ + #[Unsupported('Use spin() instead.')] public function prompt(): never { throw new RuntimeException('Spinner cannot be prompted.'); diff --git a/src/prompts/src/Stream.php b/src/prompts/src/Stream.php index 389b703a4b..4fd7f48e75 100644 --- a/src/prompts/src/Stream.php +++ b/src/prompts/src/Stream.php @@ -7,6 +7,7 @@ use Closure; use Hypervel\Coroutine\Coroutine; use Hypervel\Prompts\Themes\Default\Concerns\InteractsWithStrings; +use Hypervel\Support\Attributes\Unsupported; use RuntimeException; use Symfony\Component\Console\Output\OutputInterface; use Throwable; @@ -139,6 +140,7 @@ public function lines(): array * * @throws RuntimeException */ + #[Unsupported('Use append() and close() instead.')] public function prompt(): mixed { throw new RuntimeException('Stream cannot be prompted'); diff --git a/src/prompts/src/Task.php b/src/prompts/src/Task.php index 370105b1ff..e11e915f8e 100644 --- a/src/prompts/src/Task.php +++ b/src/prompts/src/Task.php @@ -11,6 +11,7 @@ use Hypervel\Prompts\Support\PromptAnimation; use Hypervel\Prompts\Support\TaskFrame; use Hypervel\Prompts\Support\Utils; +use Hypervel\Support\Attributes\Unsupported; use InvalidArgumentException; use RuntimeException; use Throwable; @@ -825,6 +826,7 @@ private function settleOperation(bool $renderFinalFrame = false, bool $success = * * @throws RuntimeException */ + #[Unsupported('Use run() instead.')] public function prompt(): never { throw new RuntimeException('Task cannot be prompted.'); diff --git a/src/queue/composer.json b/src/queue/composer.json index 81f16f0751..650dd74d46 100644 --- a/src/queue/composer.json +++ b/src/queue/composer.json @@ -24,6 +24,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/bus": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", diff --git a/src/queue/src/CoroutineQueue.php b/src/queue/src/CoroutineQueue.php index 150984d74c..af2d7f34c7 100644 --- a/src/queue/src/CoroutineQueue.php +++ b/src/queue/src/CoroutineQueue.php @@ -10,6 +10,7 @@ use Hypervel\Bus\DispatchLockContext; use Hypervel\Coordinator\Timer; use Hypervel\Database\DatabaseTransactionsManager; +use Hypervel\Support\Attributes\BootOnly; use Swoole\Coroutine\CanceledException; use Throwable; use UnitEnum; @@ -72,9 +73,10 @@ static function (Queue $owner) use ($delay, $job, $payload, $queue): int { /** * Set the exception callback for the coroutine queue. * - * Boot-only. The callback persists on the cached queue connection for the - * worker lifetime and handles every subsequent coroutine queue exception. + * The callback persists on the cached queue connection for the worker + * lifetime and handles every subsequent coroutine queue exception. */ + #[BootOnly(whenShared: true)] public function setExceptionCallback(?callable $callback): static { $this->exceptionCallback = $callback; diff --git a/src/queue/src/Listener.php b/src/queue/src/Listener.php index 047c4a6b63..b8c92ed756 100644 --- a/src/queue/src/Listener.php +++ b/src/queue/src/Listener.php @@ -5,6 +5,7 @@ namespace Hypervel\Queue; use Closure; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Sleep; use Hypervel\Support\Traits\GuardsProcessOutput; use Symfony\Component\Process\Process; @@ -182,9 +183,10 @@ public function stop() /** * Set the output handler callback. * - * Boot-only. The callback persists on the singleton listener for the - * command lifetime and handles every subsequent worker output line. + * The callback persists on the singleton listener for the command + * lifetime and handles every subsequent worker output line. */ + #[BootOnly(whenShared: true)] public function setOutputHandler(Closure $outputHandler): void { $this->outputHandler = $outputHandler; diff --git a/src/queue/src/Queue.php b/src/queue/src/Queue.php index 1685ca46ee..8af92a2518 100644 --- a/src/queue/src/Queue.php +++ b/src/queue/src/Queue.php @@ -34,6 +34,8 @@ use Hypervel\Queue\Events\JobQueueing; use Hypervel\Queue\Events\JobQueueingCanceled; use Hypervel\Queue\Events\JobQueueingFailed; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Context; @@ -397,10 +399,11 @@ protected function createStringPayload(array|string $job, ?string $queue, mixed /** * Register a callback to be executed when creating job payloads. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every subsequent payload creation across all - * coroutines. Passing null clears the registry. + * The callback persists in a static property for the worker lifetime + * and runs on every subsequent payload creation across all coroutines. + * Passing null clears the registry. */ + #[BootOnly] public static function createPayloadUsing(?callable $callback): void { if (is_null($callback)) { @@ -427,9 +430,14 @@ protected function withCreatePayloadHooks(?string $queue, array $payload): array /** * Set the dispatcher for after-commit queue operations. * + * Queue connection instances are cached on QueueManager; replacing the + * dispatcher on a shared connection changes every subsequent after-commit + * enqueue through it. + * * @param null|Closure(Closure(Queue): mixed): mixed $dispatcher * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setAfterCommitDispatcher(?Closure $dispatcher): static { $this->afterCommitDispatcher = $dispatcher; @@ -999,12 +1007,13 @@ public function getConnectionName(): string /** * Set the connection name for the queue. * - * Boot or tests only. Queue connection instances are cached on - * QueueManager; runtime use races across coroutines and changes every - * concurrent dispatch through this connection. + * Queue connection instances are cached on QueueManager; runtime use + * races across coroutines and changes every concurrent dispatch + * through this connection. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setConnectionName(string $name): static { $this->connectionName = $name; @@ -1023,12 +1032,13 @@ public function getConfig(): array /** * Set the queue configuration array. * - * Boot or tests only. Queue connection instances are cached on - * QueueManager; runtime use races across coroutines and changes every - * concurrent dispatch through this connection. + * Queue connection instances are cached on QueueManager; runtime use + * races across coroutines and changes every concurrent dispatch + * through this connection. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setConfig(array $config): static { $this->config = $config; @@ -1048,10 +1058,11 @@ public function getContainer(): Container /** * Set the IoC container instance. * - * Boot or tests only. Queue connection instances are cached on - * QueueManager; runtime use races across coroutines and changes every - * concurrent dispatch through this connection. + * Queue connection instances are cached on QueueManager; runtime use + * races across coroutines and changes every concurrent dispatch + * through this connection. */ + #[BootOrTestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/queue/src/QueueManager.php b/src/queue/src/QueueManager.php index be4e89d2dc..de5d7eda2b 100644 --- a/src/queue/src/QueueManager.php +++ b/src/queue/src/QueueManager.php @@ -30,6 +30,8 @@ use Hypervel\Queue\Events\WorkerStarting; use Hypervel\Queue\Events\WorkerStopping; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\InteractsWithTime; use Hypervel\Support\Queue\Concerns\ResolvesQueueRoutes; use InvalidArgumentException; @@ -87,9 +89,10 @@ public function __construct( /** * Register an event listener for the before job event. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function before(mixed $callback): void { $this->app->make('events') @@ -99,9 +102,10 @@ public function before(mixed $callback): void /** * Register an event listener for the after job event. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function after(mixed $callback): void { $this->app->make('events') @@ -111,9 +115,10 @@ public function after(mixed $callback): void /** * Register an event listener for the exception occurred job event. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function exceptionOccurred(mixed $callback): void { $this->app->make('events') @@ -123,9 +128,10 @@ public function exceptionOccurred(mixed $callback): void /** * Register an event listener for the daemon queue loop. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function looping(mixed $callback): void { $this->app->make('events') @@ -135,9 +141,10 @@ public function looping(mixed $callback): void /** * Register an event listener for the failed job event. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function failing(mixed $callback): void { $this->app->make('events') @@ -147,9 +154,10 @@ public function failing(mixed $callback): void /** * Register an event listener for the daemon queue starting. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function starting(mixed $callback): void { $this->app->make('events') @@ -159,9 +167,10 @@ public function starting(mixed $callback): void /** * Register an event listener for the daemon queue stopping. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function stopping(mixed $callback): void { $this->app->make('events') @@ -171,9 +180,10 @@ public function stopping(mixed $callback): void /** * Register a callback to be invoked when a job runs for longer than a given amount of time. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent matching queue event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent matching queue event. */ + #[BootOnly] public function whenJobRunsLongerThan(DateTimeInterface|CarbonInterval|float|int $threshold, callable $handler): void { $threshold = $threshold instanceof DateTimeInterface @@ -196,11 +206,12 @@ public function whenJobRunsLongerThan(DateTimeInterface|CarbonInterval|float|int /** * Register the default queue route for a given class. * - * Boot-only. The route persists on the singleton QueueRoutes registry for - * the worker lifetime and affects every subsequent dispatch of that class. + * The route persists on the singleton QueueRoutes registry for the worker + * lifetime and affects every subsequent dispatch of that class. * * @param array|class-string $class */ + #[BootOnly] public function route(array|string $class, UnitEnum|string|null $queue = null, UnitEnum|string|null $connection = null): void { $this->queueRoutes()->set($class, $queue, $connection); @@ -209,11 +220,12 @@ public function route(array|string $class, UnitEnum|string|null $queue = null, U /** * Forward the given queue to another queue and/or connection. * - * Boot-only. Forwards persist on the singleton QueueRoutes registry for - * the worker lifetime and affect every subsequent dispatch and queue operation. + * Forwards persist on the singleton QueueRoutes registry for the worker + * lifetime and affect every subsequent dispatch and queue operation. * * @param array|string|UnitEnum $queue */ + #[BootOnly] public function forward(array|string|UnitEnum $queue, UnitEnum|string|null $to = null, UnitEnum|string|null $connection = null): void { $this->queueRoutes()->forward($queue, $to, $connection); @@ -380,9 +392,10 @@ public function getPausedQueues(array $queues, ?string $connection = null): arra * * This prevents the workers from hitting the application cache to determine if they need to pause or restart. * - * Boot-only. Mutates process-global worker flags; runtime use races across - * coroutines and changes every concurrent worker pause or restart check. + * Mutates process-global worker flags; runtime use races across coroutines + * and changes every concurrent worker pause or restart check. */ + #[BootOnly] public function withoutInterruptionPolling(): void { Worker::$restartable = false; @@ -519,9 +532,10 @@ protected function getConnector(string $driver): ConnectorInterface /** * Add a queue connection resolver. * - * Boot-only. The resolver persists in the singleton's connectors array for - * the worker lifetime and applies to every subsequent connection. + * The resolver persists in the singleton's connectors array for the worker + * lifetime and applies to every subsequent connection. */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $resolver): void { $this->addConnector($driver, $resolver); @@ -530,9 +544,10 @@ public function extend(string $driver, Closure $resolver): void /** * Add a queue connection resolver. * - * Boot-only. The resolver persists in the singleton's connectors array for - * the worker lifetime and applies to every subsequent connection. + * The resolver persists in the singleton's connectors array for the worker + * lifetime and applies to every subsequent connection. */ + #[BootOnly(whenShared: true)] public function addConnector(string $driver, Closure $resolver): void { $this->connectors[$driver] = $resolver; @@ -569,8 +584,9 @@ public function getDefaultDriver(): string /** * Set the name of the default queue connection. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultDriver(UnitEnum|string $name): void { $name = $name instanceof UnitEnum ? (string) enum_value($name) : $name; @@ -591,9 +607,8 @@ public function getName(?string $connection = null): string /** * Disconnect a queue connection and close its shared resource pool. * - * Boot or tests only, plus operational recovery of broken pooled - * resources. Other connections sharing the pool transparently acquire a - * fresh pool on their next operation. + * Use this to recover broken pooled resources. Other connections sharing + * the pool transparently acquire a fresh pool on their next operation. */ public function purge(?string $name = null): void { @@ -639,12 +654,13 @@ public function getApplication(): Container /** * Set the application instance used by the manager. * - * Tests only. Swaps the container reference on the singleton and on every - * cached queue connection; per-request use races across coroutines and - * breaks every concurrent dispatch. + * Swaps the container reference on the singleton and on every cached + * queue connection; per-request use races across coroutines and breaks + * every concurrent dispatch. * * @return $this */ + #[TestsOnly] public function setApplication(Container $app): static { $this->app = $app; @@ -666,10 +682,11 @@ public function setApplication(Container $app): static /** * Register a callback to be executed when creating job payloads. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every subsequent payload creation across all - * coroutines. Passing null clears the registry. + * The callback persists in a static property for the worker lifetime + * and runs on every subsequent payload creation across all coroutines. + * Passing null clears the registry. */ + #[BootOnly] public function createPayloadUsing(?callable $callback): void { Queue::createPayloadUsing($callback); diff --git a/src/queue/src/QueuePoolProxy.php b/src/queue/src/QueuePoolProxy.php index d2de1d46aa..2699e82d67 100644 --- a/src/queue/src/QueuePoolProxy.php +++ b/src/queue/src/QueuePoolProxy.php @@ -22,6 +22,7 @@ use Hypervel\Queue\Outbox\Concerns\PublishesOutboxMessagesSequentially; use Hypervel\Queue\Outbox\OutboxBatchResult; use Hypervel\Queue\Outbox\OutboxMessage; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use RuntimeException; use Swoole\Coroutine\CanceledException; @@ -334,7 +335,14 @@ public function getConnectionName(): string /** * Set the connection name for the queue. + * + * Queue connection proxies are cached on QueueManager; runtime use races + * across coroutines and changes every concurrent dispatch through this + * connection. + * + * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setConnectionName(string $name): static { $this->connectionName = $name; diff --git a/src/queue/src/QueueRoutes.php b/src/queue/src/QueueRoutes.php index 78886b1054..2e38ffc53c 100644 --- a/src/queue/src/QueueRoutes.php +++ b/src/queue/src/QueueRoutes.php @@ -5,6 +5,7 @@ namespace Hypervel\Queue; use Hypervel\Queue\Attributes\Queue as QueueAttribute; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Traits\ReadsClassAttributes; use UnitEnum; @@ -135,11 +136,12 @@ class_uses_recursive($queueable) /** * Register the queue route for the given class. * - * Boot-only. The route persists on the singleton registry for the worker - * lifetime and affects every subsequent dispatch of that class. + * The route persists on the singleton registry for the worker lifetime + * and affects every subsequent dispatch of that class. * * @param array|class-string $class */ + #[BootOnly(whenShared: true)] public function set(array|string $class, UnitEnum|string|null $queue = null, UnitEnum|string|null $connection = null): void { $routes = is_array($class) ? $class : [$class => [$connection, $queue]]; @@ -157,11 +159,12 @@ public function set(array|string $class, UnitEnum|string|null $queue = null, Uni /** * Register a forward for the given queue. * - * Boot-only. Forwards persist on the singleton registry for the worker - * lifetime and affect every subsequent dispatch and queue operation. + * Forwards persist on the singleton registry for the worker lifetime + * and affect every subsequent dispatch and queue operation. * * @param array|string|UnitEnum $queue */ + #[BootOnly(whenShared: true)] public function forward(UnitEnum|array|string $queue, UnitEnum|string|null $to = null, UnitEnum|string|null $connection = null): void { $forwards = is_array($queue) ? $queue : [enum_value($queue) => $to]; diff --git a/src/queue/src/QueueServiceProvider.php b/src/queue/src/QueueServiceProvider.php index 5ddf5e9c46..53fdb2a694 100644 --- a/src/queue/src/QueueServiceProvider.php +++ b/src/queue/src/QueueServiceProvider.php @@ -186,8 +186,7 @@ protected function registerConnection(): void /** * Register the connectors on the queue manager. * - * Boot-only. Connectors persist on the supplied manager for the worker - * lifetime and affect every subsequent connection it resolves. + * This changes the given manager, which is usually the application's worker-shared queue manager. */ public function registerConnectors(QueueManager $manager): void { diff --git a/src/queue/src/Worker.php b/src/queue/src/Worker.php index 0017154d69..877c52a35c 100644 --- a/src/queue/src/Worker.php +++ b/src/queue/src/Worker.php @@ -43,6 +43,8 @@ use Hypervel\Queue\Events\WorkerStarting; use Hypervel\Queue\Events\WorkerStopping; use Hypervel\Queue\QueueManager as ConcreteQueueManager; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\FailureReporter; use Hypervel\Support\Sleep; @@ -221,7 +223,7 @@ class Worker /** * The custom exit code to be used when memory is exceeded. * - * Boot-only. Mutates process-global worker configuration; runtime use + * Setting this mutates process-global worker configuration; runtime use * races across coroutines and changes every concurrent worker stop check. */ public static ?int $memoryExceededExitCode = null; @@ -229,7 +231,7 @@ class Worker /** * The custom exit code to be used when a job times out. * - * Boot-only. Mutates process-global worker configuration; runtime use + * Setting this mutates process-global worker configuration; runtime use * races across coroutines and changes every concurrent timeout exit. */ public static ?int $timedOutExitCode = null; @@ -237,7 +239,7 @@ class Worker /** * Indicates if the worker should be killed when a job exceeds its timeout. * - * Boot-only. Mutates the timeout policy shared by every job in the worker. + * Setting this mutates the timeout policy for every job in the worker. */ public static bool $killOnTimeout = true; @@ -251,7 +253,7 @@ class Worker /** * Indicates if the worker should report job exceptions. * - * Boot-only. Mutates process-global worker configuration; runtime use + * Setting this mutates process-global worker configuration; runtime use * races across coroutines and changes exception reporting for every job. */ public static bool $reportJobExceptions = true; @@ -259,7 +261,7 @@ class Worker /** * Indicates if the worker should stop when a lost connection is detected. * - * Boot-only. Mutates process-global worker configuration; runtime use + * Setting this mutates process-global worker configuration; runtime use * races across coroutines and changes every concurrent worker. */ public static bool $stopOnLostConnection = true; @@ -267,7 +269,7 @@ class Worker /** * Indicates if the worker should check for the restart signal in the cache. * - * Boot-only. Mutates process-global worker configuration; runtime use + * Setting this mutates process-global worker configuration; runtime use * races across coroutines and changes every concurrent restart check. */ public static bool $restartable = true; @@ -275,7 +277,7 @@ class Worker /** * Indicates if the worker should check for the paused signal in the cache. * - * Boot-only. Mutates process-global worker configuration; runtime use + * Setting this mutates process-global worker configuration; runtime use * races across coroutines and changes every concurrent pause check. */ public static bool $pausable = true; @@ -2016,11 +2018,12 @@ public function sleep(float|int $seconds): void /** * Set the cache repository implementation. * - * Boot-only. The repository persists on the singleton worker for the - * worker lifetime and is read by every subsequent pause or restart check. + * The repository persists on the singleton worker for the worker lifetime + * and is read by every subsequent pause or restart check. * * @return $this */ + #[BootOnly(whenShared: true)] public function setCache(CacheContract $cache): static { $this->cache = $cache; @@ -2031,11 +2034,12 @@ public function setCache(CacheContract $cache): static /** * Set the name of the worker. * - * Boot-only. The name persists on the singleton worker for the worker - * lifetime and selects the static pop callback used by every job pop. + * The name persists on the singleton worker for the worker lifetime + * and selects the static pop callback used by every job pop. * * @return $this */ + #[BootOnly(whenShared: true)] public function setName(string $name): static { $this->name = $name; @@ -2046,10 +2050,11 @@ public function setName(string $name): static /** * Register a callback to be executed to pick jobs. * - * Boot-only. The callback persists in a static property for the worker - * process lifetime and runs on every job pop for the named worker. Passing - * null removes the callback. + * The callback persists in a static property for the worker process + * lifetime and runs on every job pop for the named worker. Passing null + * removes the callback. */ + #[BootOnly] public static function popUsing(string $workerName, ?callable $callback): void { if (is_null($callback)) { @@ -2062,11 +2067,12 @@ public static function popUsing(string $workerName, ?callable $callback): void /** * Register a callback to be used to kill the worker process. * - * Boot-only. The callback persists for the worker lifetime and runs before - * forced process termination. Passing null clears it. + * The callback persists for the worker lifetime and runs before forced + * process termination. Passing null clears it. * * @param null|(callable(int): mixed) $callback */ + #[BootOnly] public static function killUsing(?callable $callback): void { static::$killCallback = $callback; @@ -2083,9 +2089,10 @@ public function getManager(): QueueManager /** * Set the queue manager instance. * - * Tests only. Swaps the manager reference on the singleton worker; runtime - * use races across coroutines and changes every concurrent queue lookup. + * Swaps the manager reference on the singleton worker; runtime use races + * across coroutines and changes every concurrent queue lookup. */ + #[TestsOnly(whenShared: true)] public function setManager(QueueManager $manager): void { $this->manager = $manager; diff --git a/src/rate-limiter/composer.json b/src/rate-limiter/composer.json index 2bb1fb04c6..5c74d4d430 100644 --- a/src/rate-limiter/composer.json +++ b/src/rate-limiter/composer.json @@ -31,6 +31,7 @@ "require": { "php": "^8.4", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", "hypervel/container": "^0.4", diff --git a/src/rate-limiter/src/RateLimiter.php b/src/rate-limiter/src/RateLimiter.php index c4abc7e4c3..f43411f35e 100644 --- a/src/rate-limiter/src/RateLimiter.php +++ b/src/rate-limiter/src/RateLimiter.php @@ -9,6 +9,7 @@ use Hypervel\Database\ConnectionResolverInterface; use Hypervel\RateLimiter\Contracts\Store; use Hypervel\RateLimiter\Swoole\TableManager; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\MultipleInstanceManager; use Hypervel\Support\Traits\Macroable; use InvalidArgumentException; @@ -63,11 +64,12 @@ public function store(UnitEnum|string|null $name = null): Limiter /** * Register a named rate limiter configuration. * - * Boot-only. The callback and store selection persist on the singleton - * manager for the worker lifetime and affect every subsequent request. + * On the shared manager, the callback and store selection persist for + * the worker lifetime and affect every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function for( UnitEnum|string $name, Closure $callback, @@ -105,13 +107,14 @@ public function limiterStore(UnitEnum|string $name): ?string /** * Register a named limiter key scope resolver. * - * Boot-only. Each non-null callback is appended in registration order, - * while passing null clears every registered callback. The callbacks - * persist on the singleton manager for the worker lifetime and affect - * every subsequent named limiter operation. + * Each non-null callback is appended in registration order, while + * passing null clears every registered callback. On the shared manager, + * the callbacks persist for the worker lifetime and affect every + * subsequent named limiter operation. * * @param null|(Closure(string): ?string) $resolver */ + #[BootOnly(whenShared: true)] public function resolveKeyScopeUsing(?Closure $resolver): void { if ($resolver === null) { @@ -134,8 +137,9 @@ public function getDefaultInstance(): string /** * Set the default rate limiter store name. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultInstance(string $name): void { $this->config->set('rate-limiter.default', $name); diff --git a/src/rate-limiter/src/Swoole/TableManager.php b/src/rate-limiter/src/Swoole/TableManager.php index fbe1d8f8d8..8d5e5f3633 100644 --- a/src/rate-limiter/src/Swoole/TableManager.php +++ b/src/rate-limiter/src/Swoole/TableManager.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Config\Repository; use Hypervel\Core\Swoole\StripedLock; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use LogicException; use RuntimeException; @@ -50,9 +51,10 @@ public function get(string $name): TableState /** * Prevent tables from being created after the server initialization phase. * - * Boot-only. Creating a table after the server forks would give each worker - * private state instead of one shared rate limiter. + * Creating a table after the server forks would give each worker private + * state instead of one shared rate limiter. */ + #[BootOnly(whenShared: true)] public function seal(): void { $this->sealed = true; diff --git a/src/redis/composer.json b/src/redis/composer.json index 6d55bd4337..ca4aa16dea 100644 --- a/src/redis/composer.json +++ b/src/redis/composer.json @@ -27,6 +27,7 @@ "php": "^8.4", "ext-filter": "*", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/config": "^0.4", "hypervel/connection-pool": "^0.4", diff --git a/src/redis/src/Pool/PoolManager.php b/src/redis/src/Pool/PoolManager.php index 7e4f9ced9c..2c48253e1b 100644 --- a/src/redis/src/Pool/PoolManager.php +++ b/src/redis/src/Pool/PoolManager.php @@ -6,10 +6,26 @@ use Hypervel\Contracts\Container\Container as ContainerContract; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Throwable; class PoolManager { + /** + * The sequence used to build unique per-manager context key prefixes. + */ + // Do not reset this; live context entries may still use earlier prefixes. + private static int $contextKeySequence = 0; + + /** + * The prefix of this manager's coroutine context keys. + * + * Proxies pin connections from this manager's pools in coroutine context. + * Another manager can own a pool with the same connection name, so the + * keys include this manager's own prefix. + */ + private ?string $contextKeyPrefix = null; + /** * @var array */ @@ -26,9 +42,10 @@ public function __construct( /** * Remove all pools and close their connections. * - * Boot or tests only. Closes worker-shared pools; connections already - * checked out by concurrent coroutines are destroyed on release. + * Closes worker-shared pools; connections already checked out by + * concurrent coroutines are destroyed on release. */ + #[BootOrTestsOnly] public function purgeAll(): void { $pools = $this->pools; @@ -51,9 +68,10 @@ public function purgeAll(): void /** * Remove a pool and close its connections. * - * Boot or tests only. Closes a worker-shared pool; connections already - * checked out by concurrent coroutines are destroyed on release. + * Closes a worker-shared pool; connections already checked out by + * concurrent coroutines are destroyed on release. */ + #[BootOrTestsOnly] public function purge(string $name): void { $pool = $this->pools[$name] ?? null; @@ -116,4 +134,14 @@ public function getPools(): array { return $this->pools; } + + /** + * Get this manager's coroutine context key for the given entry. + * + * @internal + */ + public function contextKey(string $name): string + { + return ($this->contextKeyPrefix ??= '__redis.' . ++self::$contextKeySequence . '.') . $name; + } } diff --git a/src/redis/src/RedisConfig.php b/src/redis/src/RedisConfig.php index 19ed585429..26ea6b22d4 100644 --- a/src/redis/src/RedisConfig.php +++ b/src/redis/src/RedisConfig.php @@ -5,6 +5,7 @@ namespace Hypervel\Redis; use Hypervel\Config\Repository; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\ConfigurationUrlParser; use InvalidArgumentException; @@ -106,8 +107,10 @@ public function connectionConfig(string $name): array /** * Enable Redis command events. * - * Boot-only. The worker-wide override affects every subsequently assembled connection config. + * On the shared config, the override affects every subsequently assembled + * connection config. */ + #[BootOnly(whenShared: true)] public function enableEvents(): void { $this->eventsOverride = true; @@ -116,8 +119,10 @@ public function enableEvents(): void /** * Disable Redis command events. * - * Boot-only. The worker-wide override affects every subsequently assembled connection config. + * On the shared config, the override affects every subsequently assembled + * connection config. */ + #[BootOnly(whenShared: true)] public function disableEvents(): void { $this->eventsOverride = false; diff --git a/src/redis/src/RedisManager.php b/src/redis/src/RedisManager.php index caa54713d8..93300ae2ee 100644 --- a/src/redis/src/RedisManager.php +++ b/src/redis/src/RedisManager.php @@ -14,6 +14,8 @@ use Hypervel\Redis\Limiters\ConcurrencyLimiterBuilder; use Hypervel\Redis\Limiters\DurationLimiterBuilder; use Hypervel\Redis\Pool\PoolManager; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Throwable; use UnitEnum; @@ -74,10 +76,11 @@ public function connection(UnitEnum|string|null $name = null): RedisProxy * Discards any context-pinned connection and flushes the underlying pool * so all connections are closed and re-created on next use. * - * Boot or tests only. Flushes the shared pool; concurrent coroutines - * checked out before this call may complete against the destroyed pool - * and other coroutines lose their cached proxy reference. + * Flushes the shared pool; concurrent coroutines checked out before + * this call may complete against the destroyed pool and other + * coroutines lose their cached proxy reference. */ + #[BootOrTestsOnly] public function purge(UnitEnum|string|null $name = null): void { if ($name instanceof UnitEnum) { @@ -124,9 +127,10 @@ public function connections(): array /** * Enable Redis command events. * - * Boot-only. Existing pools with a different snapshotted setting are - * replaced; borrowed connections from the old generation are destroyed on release. + * Existing pools with a different snapshotted setting are replaced; borrowed + * connections from the old generation are destroyed on release. */ + #[BootOnly] public function enableEvents(): void { $this->config->enableEvents(); @@ -136,9 +140,10 @@ public function enableEvents(): void /** * Disable Redis command events. * - * Boot-only. Existing pools with a different snapshotted setting are - * replaced; borrowed connections from the old generation are destroyed on release. + * Existing pools with a different snapshotted setting are replaced; borrowed + * connections from the old generation are destroyed on release. */ + #[BootOnly] public function disableEvents(): void { $this->config->disableEvents(); @@ -147,7 +152,10 @@ public function disableEvents(): void /** * Refresh pools whose command-event setting differs from the worker override. + * + * Flushes shared pools that concurrent coroutines may be using. */ + #[BootOnly] protected function refreshEventPools(bool $eventsEnabled): void { foreach ($this->poolManager->getPools() as $name => $pool) { @@ -214,9 +222,10 @@ protected function terminateConnections(Closure $terminate): void /** * Register a Redis command listener with the connection. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent Redis command event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent Redis command event. */ + #[BootOnly] public function listen(Closure $callback): void { if ($this->app->bound('events')) { @@ -227,9 +236,10 @@ public function listen(Closure $callback): void /** * Register a Redis command failure listener with the connection. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent Redis failure event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent Redis failure event. */ + #[BootOnly] public function listenForFailures(Closure $callback): void { if ($this->app->bound('events')) { diff --git a/src/redis/src/RedisProxy.php b/src/redis/src/RedisProxy.php index 41212c1fbf..3ac0b5c3c6 100644 --- a/src/redis/src/RedisProxy.php +++ b/src/redis/src/RedisProxy.php @@ -20,6 +20,8 @@ use Hypervel\Redis\Subscriber\Subscriber; use Hypervel\Redis\Traits\MultiExec; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Redis; use RedisException; use Swoole\Coroutine\CanceledException; @@ -40,16 +42,6 @@ class RedisProxy implements ConnectionContract { use MultiExec; - /** - * Context key prefix for per-connection pool state. - */ - public const string CONNECTION_CONTEXT_PREFIX = '__redis.connection.'; - - /** - * Context key prefix for the coroutine owning deferred pool cleanup. - */ - private const string DEFERRED_RELEASE_OWNER_CONTEXT_KEY_PREFIX = '__redis.deferred_release_owner.'; - /** * Methods that must be called while explicitly holding a pool connection. * @@ -83,6 +75,16 @@ class RedisProxy implements ConnectionContract 'withoutscanprefix', ]; + /** + * The coroutine context key for this connection's pinned pool connection. + */ + private ?string $contextKey = null; + + /** + * The coroutine context key for the coroutine owning deferred pool cleanup. + */ + private ?string $deferredReleaseOwnerContextKey = null; + /** * Create a new Redis proxy instance. */ @@ -114,8 +116,9 @@ public function isCluster(): bool /** * Register a custom Redis connection macro. * - * Boot-only. Macros persist for the worker lifetime and affect every Redis connection. + * Macros persist for the worker lifetime and affect every Redis connection. */ + #[BootOnly] public function macro(string $name, callable|object $macro): void { RedisConnection::macro($name, $macro); @@ -124,8 +127,9 @@ public function macro(string $name, callable|object $macro): void /** * Mix another object into the Redis connection. * - * Boot-only. Registered macros persist for the worker lifetime and affect every Redis connection. + * Registered macros persist for the worker lifetime and affect every Redis connection. */ + #[BootOnly] public function mixin(object $mixin, bool $replace = true): void { RedisConnection::mixin($mixin, $replace); @@ -142,8 +146,9 @@ public function hasMacro(string $name): bool /** * Flush the registered Redis connection macros. * - * Boot or tests only. Concurrent coroutines may resolve different methods during a flush. + * Concurrent coroutines may resolve different methods during a flush. */ + #[BootOrTestsOnly] public function flushMacros(): void { RedisConnection::flushMacros(); @@ -471,7 +476,7 @@ protected function getConnection(bool $hasContextConnection, bool $transform = t */ protected function getContextKey(): string { - return self::CONNECTION_CONTEXT_PREFIX . $this->poolName; + return $this->contextKey ??= $this->poolManager->contextKey('connection.' . $this->poolName); } /** @@ -479,7 +484,7 @@ protected function getContextKey(): string */ private function getDeferredReleaseOwnerContextKey(): string { - return self::DEFERRED_RELEASE_OWNER_CONTEXT_KEY_PREFIX . $this->poolName; + return $this->deferredReleaseOwnerContextKey ??= $this->poolManager->contextKey('deferred_release_owner.' . $this->poolName); } /** @@ -761,9 +766,10 @@ private function createSubscriber( /** * Register a Redis command listener with the connection. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent Redis command event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent Redis command event. */ + #[BootOnly] public function listen(Closure $callback): void { $container = Container::getInstance(); @@ -776,9 +782,10 @@ public function listen(Closure $callback): void /** * Register a Redis command failure listener with the connection. * - * Boot-only. The listener persists on the singleton event dispatcher for - * the worker lifetime and runs for every subsequent Redis failure event. + * The listener persists on the singleton event dispatcher for the worker + * lifetime and runs for every subsequent Redis failure event. */ + #[BootOnly] public function listenForFailures(Closure $callback): void { $container = Container::getInstance(); diff --git a/src/reverb/composer.json b/src/reverb/composer.json index d8ba16a591..1f13b3d229 100644 --- a/src/reverb/composer.json +++ b/src/reverb/composer.json @@ -32,8 +32,9 @@ } }, "require": { - "ext-swoole": "^6.3.0RC1", "php": "^8.4", + "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/bus": "^0.4", "hypervel/collections": "^0.4", "hypervel/console": "^0.4", diff --git a/src/reverb/src/Servers/Hypervel/HypervelServerProvider.php b/src/reverb/src/Servers/Hypervel/HypervelServerProvider.php index dd9a3d559a..f550e256e7 100644 --- a/src/reverb/src/Servers/Hypervel/HypervelServerProvider.php +++ b/src/reverb/src/Servers/Hypervel/HypervelServerProvider.php @@ -18,6 +18,7 @@ use Hypervel\Reverb\Servers\Hypervel\Scaling\RedisPubSubProvider; use Hypervel\Reverb\Servers\Hypervel\Scaling\RedisSharedState; use Hypervel\Reverb\Servers\Hypervel\Scaling\SwooleTableSharedState; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Facades\Redis; use InvalidArgumentException; use Swoole\Table; @@ -106,9 +107,10 @@ public function boot(): void /** * Enable publishing of events. * - * Tests only. The flag persists on the cached server provider for the - * worker lifetime and cannot be turned back off. + * The flag persists on the cached server provider for the worker + * lifetime and cannot be turned back off. */ + #[TestsOnly(whenShared: true)] public function withPublishing(): void { $this->publishesEvents = true; diff --git a/src/routing/composer.json b/src/routing/composer.json index 3dc278aab9..fc26133230 100644 --- a/src/routing/composer.json +++ b/src/routing/composer.json @@ -26,6 +26,7 @@ "require": { "php": "^8.4", "ext-filter": "*", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", diff --git a/src/routing/src/CallableDispatcher.php b/src/routing/src/CallableDispatcher.php index d97f624e88..04693c606d 100644 --- a/src/routing/src/CallableDispatcher.php +++ b/src/routing/src/CallableDispatcher.php @@ -97,8 +97,8 @@ protected function resolveParameters(Route $route, callable $callable): array /** * Flush the static reflection cache. * - * Boot or tests only. Clears the process-wide reflection cache shared by - * every coroutine; next dispatch re-reflects. + * Clears the process-wide reflection cache shared by every coroutine; + * next dispatch re-reflects. */ public static function flushCache(): void { diff --git a/src/routing/src/CompiledRouteCollection.php b/src/routing/src/CompiledRouteCollection.php index 37e25dbc15..ec535dd8e0 100644 --- a/src/routing/src/CompiledRouteCollection.php +++ b/src/routing/src/CompiledRouteCollection.php @@ -6,6 +6,7 @@ use Hypervel\Container\Container; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use LogicException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; @@ -632,11 +633,12 @@ public function setRouter(Router $router): static /** * Set the container instance on the route. * - * Tests only. Swaps the container reference on the route collection used - * by the singleton Router; per-request use races across coroutines. + * Swaps the container reference on the route collection used by the + * singleton Router; per-request use races across coroutines. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/routing/src/ControllerDispatcher.php b/src/routing/src/ControllerDispatcher.php index 41881aa527..9b32d0d2fb 100644 --- a/src/routing/src/ControllerDispatcher.php +++ b/src/routing/src/ControllerDispatcher.php @@ -102,8 +102,8 @@ protected function resolveClassMethodDependencies(array $parameters, object $ins /** * Pre-warm the reflection parameter cache for a controller action. * - * Boot-only. Called during server boot to populate reflection data in a - * process-wide cache before fork so workers inherit it copy-on-write. + * Called during server boot to populate reflection data in a process-wide + * cache before fork so workers inherit it copy-on-write. */ public static function warmReflection(string $class, string $method): void { @@ -114,8 +114,8 @@ public static function warmReflection(string $class, string $method): void /** * Flush the static reflection cache. * - * Boot or tests only. Clears the process-wide reflection cache shared by - * every coroutine; next dispatch re-reflects. + * Clears the process-wide reflection cache shared by every coroutine; + * next dispatch re-reflects. */ public static function flushCache(): void { diff --git a/src/routing/src/ImplicitRouteBinding.php b/src/routing/src/ImplicitRouteBinding.php index c03b383081..cd40f21a8c 100644 --- a/src/routing/src/ImplicitRouteBinding.php +++ b/src/routing/src/ImplicitRouteBinding.php @@ -37,8 +37,8 @@ class ImplicitRouteBinding /** * Flush the static signature cache. * - * Boot or tests only. Clears the process-wide signature caches shared by - * every coroutine; next resolution re-reflects. + * Clears the process-wide signature caches shared by every coroutine; + * next resolution re-reflects. */ public static function flushCache(): void { diff --git a/src/routing/src/Middleware/ValidateSignature.php b/src/routing/src/Middleware/ValidateSignature.php index 0b3f20528e..abae8d0596 100644 --- a/src/routing/src/Middleware/ValidateSignature.php +++ b/src/routing/src/Middleware/ValidateSignature.php @@ -8,6 +8,7 @@ use Hypervel\Http\Request; use Hypervel\Routing\Exceptions\InvalidSignatureException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Symfony\Component\HttpFoundation\Response; class ValidateSignature @@ -84,9 +85,10 @@ protected function parseArguments(array $args): array /** * Indicate that the given parameters should be ignored during signature validation. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every subsequent request. + * The list persists in a static property for the worker lifetime and + * applies to every subsequent request. */ + #[BootOnly] public static function except(array|string $parameters): void { static::$neverValidate = array_values(array_unique( diff --git a/src/routing/src/Redirector.php b/src/routing/src/Redirector.php index dc2a4a7391..b98ed0357a 100755 --- a/src/routing/src/Redirector.php +++ b/src/routing/src/Redirector.php @@ -9,6 +9,7 @@ use DateTimeInterface; use Hypervel\Http\RedirectResponse; use Hypervel\Session\Store as SessionStore; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Traits\Macroable; class Redirector @@ -158,9 +159,10 @@ public function getUrlGenerator(): UrlGenerator /** * Set the active session store. * - * Boot-only. Mutates the worker-shared Redirector while concurrent - * coroutines may still hold or use the previous session store. + * Mutates the worker-shared Redirector while concurrent coroutines + * may still hold or use the previous session store. */ + #[BootOnly(whenShared: true)] public function setSession(SessionStore $session): void { $this->session = $session; diff --git a/src/routing/src/ResolvesRouteDependencies.php b/src/routing/src/ResolvesRouteDependencies.php index 3b1756dbdd..f5458f7a66 100644 --- a/src/routing/src/ResolvesRouteDependencies.php +++ b/src/routing/src/ResolvesRouteDependencies.php @@ -27,8 +27,8 @@ trait ResolvesRouteDependencies /** * Flush the static enum cache. * - * Boot or tests only. Clears the process-wide enum lookup cache shared by - * every coroutine; next resolution re-resolves. + * Clears the process-wide enum lookup cache shared by every coroutine; + * next resolution re-resolves. */ public static function flushEnumCache(): void { diff --git a/src/routing/src/ResourceRegistrar.php b/src/routing/src/ResourceRegistrar.php index 7e162ec979..dfe8c843f0 100644 --- a/src/routing/src/ResourceRegistrar.php +++ b/src/routing/src/ResourceRegistrar.php @@ -4,6 +4,7 @@ namespace Hypervel\Routing; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; class ResourceRegistrar @@ -576,9 +577,10 @@ protected function getResourceRouteName(string $resource, string $method, array /** * Set or unset the unmapped global parameters to singular. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every subsequent resource-route registration. + * The flag persists in a static property for the worker lifetime and + * applies to every subsequent resource-route registration. */ + #[BootOnly] public static function singularParameters(bool $singular = true): void { static::$singularParameters = $singular; @@ -595,9 +597,10 @@ public static function getParameters(): array /** * Set the global parameter mapping. * - * Boot-only. The map persists in a static property for the worker lifetime - * and applies to every subsequent resource-route registration. + * The map persists in a static property for the worker lifetime and + * applies to every subsequent resource-route registration. */ + #[BootOnly] public static function setParameters(array $parameters = []): void { static::$parameterMap = $parameters; @@ -606,9 +609,10 @@ public static function setParameters(array $parameters = []): void /** * Get or set the action verbs used in the resource URIs. * - * Boot-only when setting. Verbs persist in a static property for the - * worker lifetime and apply to every subsequent resource-route URI. + * Given verbs persist in a static property for the worker lifetime and + * apply to every subsequent resource-route URI. */ + #[BootOnly(whenNotDefault: 'verbs')] public static function verbs(array $verbs = []): ?array { if ($verbs === []) { diff --git a/src/routing/src/Route.php b/src/routing/src/Route.php index 8dd022bd0f..4bfb660cb1 100755 --- a/src/routing/src/Route.php +++ b/src/routing/src/Route.php @@ -25,6 +25,7 @@ use Hypervel\Routing\Matching\UriValidator; use Hypervel\Routing\Matching\ValidatorInterface; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Hypervel\Support\Traits\Conditionable; @@ -424,13 +425,14 @@ protected function parseControllerCallback(): array /** * Flush the cached controller state on the route. * - * Boot or tests only. Clears caches on the worker-shared route and - * evicts its controller class from the container. + * Clears caches on the worker-shared route and evicts its + * controller class from the container. * * Clears both the route-level property cache (for worker-shared controllers) * and the coroutine Context entry (for scoped/bound controllers), plus the * container's auto-singleton cache so make() creates a fresh instance. */ + #[BootOrTestsOnly] public function flushController(): void { $this->computedMiddleware = null; diff --git a/src/routing/src/RouteSignatureParameters.php b/src/routing/src/RouteSignatureParameters.php index afc858f320..3931ac18f5 100644 --- a/src/routing/src/RouteSignatureParameters.php +++ b/src/routing/src/RouteSignatureParameters.php @@ -80,8 +80,8 @@ public static function fromAction(array $action, array $conditions = []): array /** * Flush the static parameter cache. * - * Boot or tests only. Clears the process-wide signature caches shared by - * every coroutine; next signature lookup re-reflects. + * Clears the process-wide signature caches shared by every coroutine; + * next signature lookup re-reflects. */ public static function flushCache(): void { diff --git a/src/routing/src/Router.php b/src/routing/src/Router.php index 265a8e2864..3dc6355326 100644 --- a/src/routing/src/Router.php +++ b/src/routing/src/Router.php @@ -27,6 +27,9 @@ use Hypervel\Routing\Events\Routing; use Hypervel\Routing\Exceptions\BackedEnumCaseNotFoundException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Hypervel\Support\Stringable; @@ -961,11 +964,12 @@ public function substituteImplicitBindings(Route $route): mixed /** * Register a callback to run after implicit bindings are substituted. * - * Boot-only. The callback persists on the singleton Router and affects - * every subsequent route binding. + * The callback persists on the singleton Router and affects every + * subsequent route binding. * * @return $this */ + #[BootOnly(whenShared: true)] public function substituteImplicitBindingsUsing(callable $callback): static { $this->implicitBindingCallback = $callback(...); @@ -986,8 +990,9 @@ protected function performBinding(string $key, string $value, Route $route): mix /** * Register a route matched event listener. * - * Boot-only. The listener persists on the worker-lifetime event dispatcher - * and runs for every subsequent route match. + * When the dispatcher is the application's worker-shared dispatcher, the + * listener persists for the worker lifetime and runs for every subsequent + * route match. */ public function matched(string|callable $callback): void { @@ -1007,11 +1012,12 @@ public function getMiddleware(): array /** * Register a short-hand name for a middleware. * - * Boot-only. The alias persists in the singleton Router for the worker - * lifetime and applies to every subsequent route resolution. + * The alias persists in the singleton Router for the worker lifetime + * and applies to every subsequent route resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function aliasMiddleware(string $name, string|Closure $class): static { $this->middleware[$name] = $class; @@ -1040,11 +1046,12 @@ public function getMiddlewareGroups(): array /** * Register a group of middleware. * - * Boot-only. The middleware group persists in the singleton Router for the - * worker lifetime and applies to every subsequent route resolution. + * The middleware group persists in the singleton Router for the worker + * lifetime and applies to every subsequent route resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function middlewareGroup(string $name, array $middleware): static { $this->middlewareGroups[$name] = $middleware; @@ -1057,11 +1064,12 @@ public function middlewareGroup(string $name, array $middleware): static * * If the middleware is already in the group, it will not be added again. * - * Boot-only. The middleware group persists in the singleton Router for the - * worker lifetime and applies to every subsequent route resolution. + * The middleware group persists in the singleton Router for the worker + * lifetime and applies to every subsequent route resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function prependMiddlewareToGroup(string $group, string $middleware): static { if (isset($this->middlewareGroups[$group]) && ! in_array($middleware, $this->middlewareGroups[$group], true)) { @@ -1076,11 +1084,12 @@ public function prependMiddlewareToGroup(string $group, string $middleware): sta * * If the middleware is already in the group, it will not be added again. * - * Boot-only. The middleware group persists in the singleton Router for the - * worker lifetime and applies to every subsequent route resolution. + * The middleware group persists in the singleton Router for the worker + * lifetime and applies to every subsequent route resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function pushMiddlewareToGroup(string $group, string $middleware): static { if (! array_key_exists($group, $this->middlewareGroups)) { @@ -1097,11 +1106,12 @@ public function pushMiddlewareToGroup(string $group, string $middleware): static /** * Remove the given middleware from the specified group. * - * Boot-only. The group mutation persists on the singleton Router and does - * not invalidate middleware already resolved on routes. + * The group mutation persists on the singleton Router and does not + * invalidate middleware already resolved on routes. * * @return $this */ + #[BootOnly(whenShared: true)] public function removeMiddlewareFromGroup(string $group, string|array $middleware): static { if (! $this->hasMiddlewareGroup($group)) { @@ -1126,12 +1136,12 @@ public function removeMiddlewareFromGroup(string $group, string|array $middlewar /** * Flush the router's middleware groups. * - * Boot or tests only. Clears the singleton Router's middleware-group - * registry shared by every coroutine; concurrent requests lose middleware - * mid-dispatch. + * Clears the singleton Router's middleware-group registry shared by every + * coroutine; concurrent requests lose middleware mid-dispatch. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function flushMiddlewareGroups(): static { $this->middlewareGroups = []; @@ -1142,9 +1152,10 @@ public function flushMiddlewareGroups(): static /** * Add a new route parameter binder. * - * Boot-only. The binder persists in the singleton Router for the worker - * lifetime and runs on every subsequent route parameter resolution. + * The binder persists in the singleton Router for the worker lifetime + * and runs on every subsequent route parameter resolution. */ + #[BootOnly(whenShared: true)] public function bind(string $key, string|callable $binder): void { $this->binders[str_replace('-', '_', $key)] = RouteBinding::forCallback( @@ -1156,9 +1167,10 @@ public function bind(string $key, string|callable $binder): void /** * Register a model binder for a wildcard. * - * Boot-only. The model binder persists in the singleton Router for the - * worker lifetime and runs on every subsequent route parameter resolution. + * The model binder persists in the singleton Router for the worker + * lifetime and runs on every subsequent route parameter resolution. */ + #[BootOnly(whenShared: true)] public function model(string $key, string $class, ?Closure $callback = null): void { $this->bind($key, RouteBinding::forModel($this->container, $class, $callback)); @@ -1189,9 +1201,10 @@ public function getPatterns(): array /** * Set a global where pattern on all routes. * - * Boot-only. The pattern persists in the singleton Router for the worker - * lifetime and applies to every subsequent route definition. + * The pattern persists in the singleton Router for the worker lifetime + * and applies to every subsequent route definition. */ + #[BootOnly(whenShared: true)] public function pattern(string $key, string $pattern): void { $this->patterns[$key] = $pattern; @@ -1200,9 +1213,10 @@ public function pattern(string $key, string $pattern): void /** * Set a group of global where patterns on all routes. * - * Boot-only. The patterns persist in the singleton Router for the worker - * lifetime and apply to every subsequent route definition. + * The patterns persist in the singleton Router for the worker lifetime + * and apply to every subsequent route definition. */ + #[BootOnly(whenShared: true)] public function patterns(array $patterns): void { foreach ($patterns as $key => $pattern) { @@ -1337,9 +1351,10 @@ public function currentRouteUses(string $action): bool /** * Set the unmapped global resource parameters to singular. * - * Boot-only. Delegates to ResourceRegistrar::singularParameters() which - * persists in a static property for the worker lifetime. + * Delegates to ResourceRegistrar::singularParameters() which persists + * in a static property for the worker lifetime. */ + #[BootOnly] public function singularResourceParameters(bool $singular = true): void { ResourceRegistrar::singularParameters($singular); @@ -1348,8 +1363,9 @@ public function singularResourceParameters(bool $singular = true): void /** * Set the global resource parameter mapping. * - * Boot-only. The mapping persists in static state for the worker lifetime. + * The mapping persists in static state for the worker lifetime. */ + #[BootOnly] public function resourceParameters(array $parameters = []): void { ResourceRegistrar::setParameters($parameters); @@ -1358,9 +1374,9 @@ public function resourceParameters(array $parameters = []): void /** * Get or set the verbs used in the resource URIs. * - * Boot-only when setting. Custom verbs persist in static state for the - * worker lifetime. + * Given verbs persist in static state for the worker lifetime. */ + #[BootOnly(whenNotDefault: 'verbs')] public function resourceVerbs(array $verbs = []): ?array { return ResourceRegistrar::verbs($verbs); @@ -1377,9 +1393,10 @@ public function getRoutes(): RouteCollectionInterface /** * Set the route collection instance. * - * Boot or tests only. Replacing the collection mutates the singleton Router - * and its shared route binding for every subsequent request. + * Replacing the collection mutates the singleton Router and its shared + * route binding for every subsequent request. */ + #[BootOrTestsOnly] public function setRoutes(RouteCollection $routes): void { if ($this->ownsGlobalRouteState()) { @@ -1400,10 +1417,11 @@ public function setRoutes(RouteCollection $routes): void /** * Set the compiled route collection instance. * - * Boot-only. Replaces the singleton Router's route collection and rebinds - * the container's 'routes' instance; runtime use breaks every concurrent + * Replaces the singleton Router's route collection and rebinds the + * container's 'routes' instance; runtime use breaks every concurrent * request mid-dispatch. */ + #[BootOnly] public function setCompiledRoutes(array $routes): void { if ($this->ownsGlobalRouteState()) { @@ -1454,9 +1472,10 @@ protected function flushRoutingCaches(): void * Kernel::bootstrap() returns, before $server->start(). Idempotent — * safe to call from both servers in combined setups. * - * Boot-only. Replaces the route collection and populates process-wide - * caches before fork. + * Replaces the route collection and populates process-wide caches + * before fork. */ + #[BootOnly] public function compileAndWarm(): void { $routes = $this->getRoutes(); @@ -1540,11 +1559,12 @@ public static function uniqueMiddleware(array $middleware): array /** * Set the container instance used by the router. * - * Tests only. Swaps the singleton Router's container reference; per-request - * use races across coroutines and breaks every concurrent route dispatch. + * Swaps the singleton Router's container reference; per-request use races + * across coroutines and breaks every concurrent route dispatch. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/routing/src/SortedMiddleware.php b/src/routing/src/SortedMiddleware.php index c43fa6a2bf..5108e15819 100644 --- a/src/routing/src/SortedMiddleware.php +++ b/src/routing/src/SortedMiddleware.php @@ -20,8 +20,8 @@ class SortedMiddleware extends Collection /** * Flush the static middleware names cache. * - * Boot or tests only. Clears the process-wide middleware-sort cache shared - * by every coroutine; next dispatch re-sorts. + * Clears the process-wide middleware-sort cache shared by every coroutine; + * next dispatch re-sorts. */ public static function flushCache(): void { diff --git a/src/routing/src/UrlGenerator.php b/src/routing/src/UrlGenerator.php index a55599b29b..8a8272661d 100755 --- a/src/routing/src/UrlGenerator.php +++ b/src/routing/src/UrlGenerator.php @@ -15,6 +15,9 @@ use Hypervel\Http\Request; use Hypervel\Routing\Exceptions\UrlGenerationException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\InteractsWithTime; @@ -661,9 +664,10 @@ protected function routeUrl(): RouteUrlGenerator /** * Register the default route parameter resolver. * - * Boot-only. The callback persists on the worker-shared URL generator and - * is evaluated for subsequent route generations across all coroutines. + * The callback persists on the worker-shared URL generator and is + * evaluated for subsequent route generations across all coroutines. */ + #[BootOnly(whenShared: true)] public function resolveDefaultsUsing(?Closure $resolver): void { $this->defaultsResolver = $resolver; @@ -737,10 +741,11 @@ public function getDefaultParameterOverrides(): array /** * Force the scheme for URLs. * - * Boot-only. The scheme is stored on the worker-shared URL generator - * singleton and affects subsequent requests across coroutines; not safe - * to call per-request. + * The scheme is stored on the worker-shared URL generator singleton and + * affects subsequent requests across coroutines; not safe to call + * per-request. */ + #[BootOnly(whenShared: true)] public function forceScheme(?string $scheme): void { $this->forceScheme = $scheme ? $scheme . '://' : null; @@ -757,10 +762,11 @@ public function getForcedScheme(): ?string /** * Force the use of the HTTPS scheme for all generated URLs. * - * Boot-only. The scheme is stored on the worker-shared URL generator - * singleton and affects subsequent requests across coroutines; not safe - * to call per-request. + * The scheme is stored on the worker-shared URL generator singleton and + * affects subsequent requests across coroutines; not safe to call + * per-request. */ + #[BootOnly(whenShared: true)] public function forceHttps(bool $force = true): void { $this->forceScheme($force ? 'https' : null); @@ -769,9 +775,10 @@ public function forceHttps(bool $force = true): void /** * Register the URL origin resolver. * - * Boot-only. The callback persists on the worker-shared URL generator and - * is evaluated for subsequent URL generations across all coroutines. + * The callback persists on the worker-shared URL generator and is + * evaluated for subsequent URL generations across all coroutines. */ + #[BootOnly(whenShared: true)] public function resolveOriginUsing(?Closure $resolver): void { $this->originResolver = $resolver; @@ -829,11 +836,12 @@ public function getForcedAssetOrigin(): ?string /** * Set a callback to be used to format the host of generated URLs. * - * Boot-only. The callback persists on the singleton URL generator and - * affects every subsequent request. + * The callback persists on the singleton URL generator and affects + * every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function formatHostUsing(Closure $callback): static { $this->formatHostUsing = $callback; @@ -844,11 +852,12 @@ public function formatHostUsing(Closure $callback): static /** * Set a callback to be used to format the path of generated URLs. * - * Boot-only. The callback persists on the singleton URL generator and - * affects every subsequent request. + * The callback persists on the singleton URL generator and affects + * every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function formatPathUsing(Closure $callback): static { $this->formatPathUsing = $callback; @@ -881,11 +890,12 @@ public function getRequest(): Request /** * Set the current request instance. * - * Tests only. Per-request code should rely on RequestContext (read first by + * Per-request code should rely on RequestContext (read first by * getRequest()); this setter writes the singleton UrlGenerator's fallback * request used outside coroutine contexts, plus mutates the shared * routeGenerator — runtime use races across coroutines. */ + #[TestsOnly(whenShared: true)] public function setRequest(Request $request): void { $this->request = $request; @@ -905,11 +915,12 @@ public function setRequest(Request $request): void /** * Set the route collection. * - * Boot or tests only. Replacing the collection mutates the singleton URL - * generator for every subsequent request. + * Replacing the collection mutates the singleton URL generator for every + * subsequent request. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setRoutes(RouteCollectionInterface $routes): static { $this->routes = $routes; @@ -932,11 +943,12 @@ protected function getSession(): mixed /** * Set the session resolver for the generator. * - * Boot-only. The resolver persists on the singleton URL generator and - * affects every subsequent request. + * The resolver persists on the singleton URL generator and affects + * every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function setSessionResolver(callable $sessionResolver): static { $this->sessionResolver = $sessionResolver; @@ -947,11 +959,12 @@ public function setSessionResolver(callable $sessionResolver): static /** * Set the encryption key resolver. * - * Boot-only. The resolver persists on the singleton URL generator and - * affects every subsequent request. + * The resolver persists on the singleton URL generator and affects + * every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function setKeyResolver(callable $keyResolver): static { $this->keyResolver = $keyResolver; @@ -970,11 +983,12 @@ public function withKeyResolver(callable $keyResolver): static /** * Set the callback that should be used to attempt to resolve missing named routes. * - * Boot-only. The callback persists on the singleton URL generator and - * affects every subsequent request. + * The callback persists on the singleton URL generator and affects + * every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function resolveMissingNamedRoutesUsing(callable $missingNamedRouteResolver): static { $this->missingNamedRouteResolver = $missingNamedRouteResolver; @@ -993,11 +1007,12 @@ public function getRootControllerNamespace(): ?string /** * Set the root controller namespace. * - * Boot-only. The namespace persists on the singleton URL generator and - * affects every subsequent request. + * The namespace persists on the singleton URL generator and affects + * every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function setRootControllerNamespace(string $rootNamespace): static { $this->rootNamespace = $rootNamespace; diff --git a/src/saloon/composer.json b/src/saloon/composer.json index ce23d7da60..80ed04ca02 100644 --- a/src/saloon/composer.json +++ b/src/saloon/composer.json @@ -38,6 +38,7 @@ "ext-simplexml": "*", "guzzlehttp/psr7": "^3.1", "guzzlehttp/uri-template": "^1.0 || ^2.0", + "hypervel/attributes": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", diff --git a/src/saloon/src/Http/Faking/MockClient.php b/src/saloon/src/Http/Faking/MockClient.php index b5c0084ac8..45240f3da7 100644 --- a/src/saloon/src/Http/Faking/MockClient.php +++ b/src/saloon/src/Http/Faking/MockClient.php @@ -11,6 +11,7 @@ use Hypervel\Saloon\Http\Request; use Hypervel\Saloon\Http\Response; use Hypervel\Saloon\SaloonManager; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use PHPUnit\Framework\Assert as PHPUnit; @@ -333,11 +334,12 @@ public function shouldBypassResponseCache(): bool /** * Register or retrieve the manager-backed global mock client. * - * Tests only. The client persists until the test application is destroyed - * or `destroyGlobal()` is called. + * The client persists until the test application is destroyed or + * `destroyGlobal()` is called. * * @param array $mockData */ + #[TestsOnly] public static function global(array $mockData = []): MockClient { $manager = static::manager(); @@ -355,7 +357,10 @@ public static function getGlobal(): ?MockClient /** * Destroy the manager-backed global mock client. + * + * This removes the recorder for every subsequent Saloon request in the worker. */ + #[TestsOnly] public static function destroyGlobal(): void { static::manager()->clearFake(); diff --git a/src/saloon/src/OAuthAccounts.php b/src/saloon/src/OAuthAccounts.php index a420c289c0..85b496ae2b 100644 --- a/src/saloon/src/OAuthAccounts.php +++ b/src/saloon/src/OAuthAccounts.php @@ -27,6 +27,7 @@ use Hypervel\Saloon\Http\Auth\TokenAcquisition; use Hypervel\Saloon\Http\Auth\TokenExpiry; use Hypervel\Saloon\Http\RetryAfter; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Date; use Hypervel\Support\Sleep; use Hypervel\Support\Str; @@ -80,10 +81,11 @@ public function __construct( /** * Select the account model. * - * Boot-only. The model persists for the worker lifetime and affects every account operation. + * The model persists for the worker lifetime and affects every account operation. * * @param class-string $model */ + #[BootOnly] public static function useAccountModel(string $model): void { if (static::$initialized) { @@ -110,10 +112,11 @@ public static function accountModel(): string /** * Configure the account partition resolver. * - * Boot-only. The column and callback persist for the worker lifetime and affect every account operation. + * The column and callback persist for the worker lifetime and affect every account operation. * * @param Closure(): (null|int|string) $resolver */ + #[BootOnly] public static function resolvePartitionUsing(string $column, Closure $resolver): void { if (static::$initialized || static::$partitionResolver !== null) { diff --git a/src/saloon/src/Pagination/CursorPaginator.php b/src/saloon/src/Pagination/CursorPaginator.php index cf5c1c01f5..38dbc8557e 100644 --- a/src/saloon/src/Pagination/CursorPaginator.php +++ b/src/saloon/src/Pagination/CursorPaginator.php @@ -6,6 +6,7 @@ use Hypervel\Saloon\Http\Request; use Hypervel\Saloon\Http\Response; +use Hypervel\Support\Attributes\Unsupported; use LogicException; use Throwable; @@ -82,6 +83,7 @@ abstract protected function getNextCursor(Response $response): bool|float|int|st * @param null|callable(Throwable, int): void $exceptionHandler * @return array> */ + #[Unsupported('Iterate over the paginator sequentially; each cursor depends on the previous response.')] public function pool( int $concurrency = 5, ?callable $responseHandler = null, diff --git a/src/saloon/src/SaloonManager.php b/src/saloon/src/SaloonManager.php index 11d51f1396..302a49e18d 100644 --- a/src/saloon/src/SaloonManager.php +++ b/src/saloon/src/SaloonManager.php @@ -38,6 +38,9 @@ use Hypervel\Saloon\Http\Response; use Hypervel\Saloon\Http\Sender; use Hypervel\Saloon\RateLimit\Exceptions\RateLimitReachedException; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Sleep; use InvalidArgumentException; use UnitEnum; @@ -294,9 +297,10 @@ public function inspectRateLimit(Connector|Request $resource, AdmissionPolicy|Co /** * Set the event dispatcher. * - * Boot or tests only. The dispatcher persists on the manager for the - * worker lifetime and receives every subsequent Saloon request event. + * On the shared manager, the dispatcher persists for the worker lifetime + * and receives every subsequent Saloon request event. */ + #[BootOrTestsOnly(whenShared: true)] public function setEventDispatcher(Dispatcher $events): void { $this->events = $events; @@ -305,7 +309,7 @@ public function setEventDispatcher(Dispatcher $events): void /** * Get the global middleware pipeline. * - * Boot-only. Middleware added to this pipeline persists on the manager for + * On the shared manager, middleware added to this pipeline persists for * the worker lifetime and affects every subsequent Saloon request. */ public function middleware(): MiddlewarePipeline @@ -316,11 +320,12 @@ public function middleware(): MiddlewarePipeline /** * Add responses to the global mock client, or replace it with the given client. * - * Tests only. The client persists on the manager until the test - * application is destroyed or `clearFake()` is called. + * The client persists on the manager until the test application + * is destroyed or `clearFake()` is called. * * @param array|MockClient $responses */ + #[TestsOnly(whenShared: true)] public function fake(array|MockClient $responses = []): MockClient { if ($responses instanceof MockClient) { @@ -343,11 +348,12 @@ public function mockClient(): ?MockClient /** * Clear the global mock client. * - * Tests only. This immediately removes the recorder for every subsequent - * Saloon request in the worker. + * On the shared manager, this immediately removes the recorder for every + * subsequent Saloon request in the worker. * * @return $this */ + #[TestsOnly(whenShared: true)] public function clearFake(): static { $this->mockClient = null; @@ -406,11 +412,12 @@ public function assertSentCount(int $count, ?string $requestClass = null): void /** * Register the cache scope resolver. * - * Boot-only. The callback persists on the manager for the worker lifetime + * On the shared manager, the callback persists for the worker lifetime * and affects every subsequently cached Saloon request. * * @param null|Closure(PendingRequest): ?string $resolver */ + #[BootOnly(whenShared: true)] public function resolveCacheScopeUsing(?Closure $resolver): static { $this->cacheScopeResolver = $resolver; @@ -449,9 +456,10 @@ public function clearCache(Connector $connector, Request $request): void /** * Set the fixture storage path. * - * Tests only. The override persists on the manager until the test - * application is destroyed. + * The override persists on the manager until the test application + * is destroyed. */ + #[TestsOnly(whenShared: true)] public function fixturePath(string $path): static { $this->fixturePath = $path; @@ -474,9 +482,10 @@ public function getFixturePath(): string /** * Configure missing fixture behavior. * - * Tests only. The override persists on the manager until the test - * application is destroyed. + * The override persists on the manager until the test application + * is destroyed. */ + #[TestsOnly(whenShared: true)] public function throwOnMissingFixtures(bool $throw = true): static { $this->throwOnMissingFixtures = $throw; diff --git a/src/sanctum/composer.json b/src/sanctum/composer.json index 2bcf6c2acd..5280cb6e39 100644 --- a/src/sanctum/composer.json +++ b/src/sanctum/composer.json @@ -26,7 +26,7 @@ "php": "^8.4", "ext-ctype": "*", "ext-filter": "*", - "nesbot/carbon": "^3.13.1", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", @@ -43,6 +43,7 @@ "hypervel/routing": "^0.4", "hypervel/session": "^0.4", "hypervel/support": "^0.4", + "nesbot/carbon": "^3.13.1", "symfony/console": "^8.1.10", "symfony/http-foundation": "^8.1.11" }, diff --git a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php index 1ac9f08ecf..ef203c586f 100644 --- a/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php +++ b/src/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php @@ -12,6 +12,7 @@ use Hypervel\Routing\Pipeline; use Hypervel\Sanctum\Sanctum; use Hypervel\Session\Middleware\StartSession; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Symfony\Component\HttpFoundation\Response; @@ -147,11 +148,12 @@ private static function filterDomainList(mixed $domains): array /** * Register a closure that determines whether the current request is stateful. * - * Boot-only. The closure receives the current request and returns the final - * stateful decision. Persists for the worker lifetime. + * The closure receives the current request and returns the final stateful + * decision. Persists for the worker lifetime. * * @param null|Closure(Request): bool $callback */ + #[BootOnly] public static function resolveStatefulRequestsUsing(?Closure $callback): void { static::$statefulRequestResolver = $callback; @@ -160,12 +162,13 @@ public static function resolveStatefulRequestsUsing(?Closure $callback): void /** * Register a closure that resolves stateful domains for the current request. * - * Boot-only. The closure receives the current request and returns the - * stateful domain list; useful when the list varies by host or other - * request data. Persists for the worker lifetime. + * The closure receives the current request and returns the stateful + * domain list; useful when the list varies by host or other request + * data. Persists for the worker lifetime. * * @param null|Closure(Request): array $callback */ + #[BootOnly] public static function resolveStatefulDomainsUsing(?Closure $callback): void { static::$statefulDomainsResolver = $callback; diff --git a/src/sanctum/src/Sanctum.php b/src/sanctum/src/Sanctum.php index 8e8d2a0905..fb2cbd7e6a 100644 --- a/src/sanctum/src/Sanctum.php +++ b/src/sanctum/src/Sanctum.php @@ -7,6 +7,8 @@ use Hypervel\Container\Container; use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Sanctum\Contracts\HasAbilities; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Mockery; use Mockery\MockInterface; use UnitEnum; @@ -80,8 +82,8 @@ public static function supportsTokens(?Authenticatable $tokenable): bool /** * Set the current user for the application with the given abilities. * - * Tests only. This installs a Mockery token double and replaces the current - * coroutine's authenticated user and default guard with test state. + * This installs a Mockery token double and replaces the current coroutine's + * authenticated user and default guard with test state. * * @template TUser of Authenticatable * @@ -89,6 +91,7 @@ public static function supportsTokens(?Authenticatable $tokenable): bool * @param array $abilities * @return TUser */ + #[TestsOnly] public static function actingAs(Authenticatable $user, array $abilities = [], ?string $guard = 'sanctum'): Authenticatable { $abilities = array_map(enum_value(...), $abilities); @@ -122,12 +125,12 @@ public static function actingAs(Authenticatable $user, array $abilities = [], ?s /** * Set the personal access token model name. * - * Boot-only. The model class name persists in a static property for the - * worker lifetime and is used for every token resolution across all - * coroutines. + * The model class name persists in a static property for the worker + * lifetime and is used for every token resolution across all coroutines. * * @param class-string $model */ + #[BootOnly] public static function usePersonalAccessTokenModel(string $model): void { static::$personalAccessTokenModel = $model; @@ -136,9 +139,10 @@ public static function usePersonalAccessTokenModel(string $model): void /** * Specify a callback that should be used to fetch the access token from the request. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every token retrieval across all coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every token retrieval across all coroutines. */ + #[BootOnly] public static function getAccessTokenFromRequestUsing(?callable $callback): void { static::$accessTokenRetrievalCallback = $callback; @@ -147,9 +151,10 @@ public static function getAccessTokenFromRequestUsing(?callable $callback): void /** * Specify a callback that should be used to authenticate access tokens. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every token authentication across all coroutines. + * The callback persists in a static property for the worker lifetime and + * runs on every token authentication across all coroutines. */ + #[BootOnly] public static function authenticateAccessTokensUsing(callable $callback): void { static::$accessTokenAuthenticationCallback = $callback; diff --git a/src/sanctum/src/SanctumGuard.php b/src/sanctum/src/SanctumGuard.php index b471804ff7..d25b05f9ad 100644 --- a/src/sanctum/src/SanctumGuard.php +++ b/src/sanctum/src/SanctumGuard.php @@ -282,7 +282,7 @@ public function validate(array $credentials = []): bool */ protected function getExplicitUserContextKey(): string { - return "__auth.guards.{$this->name}.user.explicit"; + return $this->contextKey('user.explicit'); } /** @@ -291,10 +291,10 @@ protected function getExplicitUserContextKey(): string protected function getContextKeyForToken(?string $token): string { if ($token === null || $token === '') { - return "__auth.guards.{$this->name}.user.default"; + return $this->contextKey('user.default'); } - return "__auth.guards.{$this->name}.user." . hash('xxh128', $token); + return $this->contextKey('user.' . hash('xxh128', $token)); } /** diff --git a/src/scout/composer.json b/src/scout/composer.json index 1bbe85a00d..61a42c1a5c 100644 --- a/src/scout/composer.json +++ b/src/scout/composer.json @@ -37,6 +37,7 @@ "require": { "php": "^8.4", "ext-filter": "*", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/config": "^0.4", diff --git a/src/scout/src/EngineManager.php b/src/scout/src/EngineManager.php index c9027a3443..d1fbf18802 100644 --- a/src/scout/src/EngineManager.php +++ b/src/scout/src/EngineManager.php @@ -18,6 +18,8 @@ use Hypervel\Scout\Engines\TurbopufferEngine; use Hypervel\Scout\Engines\TypesenseEngine; use Hypervel\Scout\Services\Turbopuffer\TurbopufferClient; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use Meilisearch\Client as MeilisearchClient; use Meilisearch\Meilisearch; @@ -239,11 +241,12 @@ public function createNullDriver(): NullEngine /** * Register a custom driver creator. * - * Boot-only. The callback persists in the singleton's customCreators array - * for the worker lifetime and applies to every subsequent engine resolution. + * The callback persists in the singleton's customCreators array for the + * worker lifetime and applies to every subsequent engine resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback): static { $this->customCreators[$driver] = $callback; @@ -254,12 +257,12 @@ public function extend(string $driver, Closure $callback): static /** * Forget all of the resolved engine instances. * - * Boot or tests only. Clears the manager's engine cache; concurrent - * coroutines may already hold references that next resolution will not - * share. + * Clears the manager's engine cache; concurrent coroutines may already + * hold references that next resolution will not share. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetEngines(): static { $this->engines = []; @@ -270,12 +273,12 @@ public function forgetEngines(): static /** * Forget a specific resolved engine instance. * - * Boot or tests only. Mutates the manager's engine cache; concurrent - * coroutines may already hold a reference to the engine and next resolution - * will rebuild. + * Mutates the manager's engine cache; concurrent coroutines may already + * hold a reference to the engine and next resolution will rebuild. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetEngine(string $name): static { unset($this->engines[$name]); diff --git a/src/scout/src/EngineOperationRunner.php b/src/scout/src/EngineOperationRunner.php index 1b3b1ede07..e15ec42676 100644 --- a/src/scout/src/EngineOperationRunner.php +++ b/src/scout/src/EngineOperationRunner.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Scout\Contracts\EngineOperationObserver; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\FailureReporter; use Throwable; @@ -21,9 +22,10 @@ class EngineOperationRunner /** * Register an engine operation observer. * - * Boot-only. Observers persist on this singleton for the worker lifetime - * and apply to every subsequent engine operation. + * Observers persist on this singleton for the worker lifetime and apply + * to every subsequent engine operation. */ + #[BootOnly(whenShared: true)] public function observe(EngineOperationObserver $observer): void { $this->observers[] = $observer; diff --git a/src/scout/src/Engines/AlgoliaEngine.php b/src/scout/src/Engines/AlgoliaEngine.php index 4b74655178..031da6f8b9 100644 --- a/src/scout/src/Engines/AlgoliaEngine.php +++ b/src/scout/src/Engines/AlgoliaEngine.php @@ -22,6 +22,7 @@ use Hypervel\Scout\Exceptions\ScoutException; use Hypervel\Scout\Jobs\RemoveableScoutCollection; use Hypervel\Scout\Scout; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Collection; use Hypervel\Support\LazyCollection; use InvalidArgumentException; @@ -486,6 +487,7 @@ function () use ($filters, $index): void { * * @throws NotSupportedException */ + #[Unsupported('Indexes are created automatically when objects are added.')] public function createIndex(string $name, array $options = []): mixed { throw new NotSupportedException('Algolia indexes are created automatically upon adding objects.'); diff --git a/src/scout/src/Engines/Engine.php b/src/scout/src/Engines/Engine.php index b924b6f335..d7e4d11a47 100644 --- a/src/scout/src/Engines/Engine.php +++ b/src/scout/src/Engines/Engine.php @@ -11,6 +11,7 @@ use Hypervel\Scout\Contracts\SearchableInterface; use Hypervel\Scout\EngineOperation; use Hypervel\Scout\EngineOperationRunner; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\LazyCollection; @@ -102,11 +103,12 @@ public function importConcurrency(Model $model, int $configured): int /** * Assign the operation runner to this engine. * - * Boot-only. The runner persists on this engine for the worker lifetime. - * Engines without observable operations retain no runner. + * The runner persists on a cached engine for the worker lifetime. Engines + * without observable operations retain no runner. * * @return $this */ + #[BootOnly(whenShared: true)] public function setOperationRunner(EngineOperationRunner $runner, string $engineName): static { if (! $this->hasObservableOperations()) { diff --git a/src/scout/src/Engines/TurbopufferEngine.php b/src/scout/src/Engines/TurbopufferEngine.php index f904d65a7b..b51305d839 100644 --- a/src/scout/src/Engines/TurbopufferEngine.php +++ b/src/scout/src/Engines/TurbopufferEngine.php @@ -21,6 +21,7 @@ use Hypervel\Scout\Scout; use Hypervel\Scout\Services\Turbopuffer\TurbopufferClient; use Hypervel\Scout\Services\Turbopuffer\TurbopufferNamespace; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Collection; use Hypervel\Support\LazyCollection; use InvalidArgumentException; @@ -759,6 +760,7 @@ public function flush(Model $model): void * * @throws NotSupportedException */ + #[Unsupported('Namespaces are created automatically when documents are added.')] public function createIndex(string $name, array $options = []): mixed { throw new NotSupportedException('Turbopuffer namespaces are created automatically upon adding documents.'); diff --git a/src/scout/src/Engines/TypesenseEngine.php b/src/scout/src/Engines/TypesenseEngine.php index b314e362c5..ac49d9e1d0 100644 --- a/src/scout/src/Engines/TypesenseEngine.php +++ b/src/scout/src/Engines/TypesenseEngine.php @@ -18,6 +18,7 @@ use Hypervel\Scout\Exceptions\ScoutException; use Hypervel\Scout\Jobs\RemoveableScoutCollection; use Hypervel\Scout\Scout; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Collection; use Hypervel\Support\LazyCollection; use InvalidArgumentException; @@ -928,6 +929,7 @@ function () use ($filters, $index): void { * * @throws NotSupportedException */ + #[Unsupported('Indexes are created automatically when objects are added.')] public function createIndex(string $name, array $options = []): mixed { throw new NotSupportedException('Typesense indexes are created automatically upon adding objects.'); diff --git a/src/scout/src/Scout.php b/src/scout/src/Scout.php index a9ddf55dbb..79ea635595 100644 --- a/src/scout/src/Scout.php +++ b/src/scout/src/Scout.php @@ -11,6 +11,7 @@ use Hypervel\Scout\Engines\Engine; use Hypervel\Scout\Jobs\MakeSearchable; use Hypervel\Scout\Jobs\RemoveFromSearch; +use Hypervel\Support\Attributes\BootOnly; /** * Scout utility class for lifecycle customization and engine access. @@ -107,11 +108,12 @@ public static function engine(?string $name = null): Engine /** * Specify the callback that prepares a Builder before terminal execution. * - * Boot-only. The callback persists for the worker lifetime, so registering - * it per request would leak captured request state into later requests. + * The callback persists for the worker lifetime, so registering it per + * request would leak captured request state into later requests. * * @param callable(Builder, Engine): void $callback */ + #[BootOnly] public static function prepareBuilderUsing(callable $callback): void { static::$prepareBuilderCallback = Closure::fromCallable($callback); @@ -130,11 +132,12 @@ public static function prepareBuilder(Builder $builder, Engine $engine): void /** * Specify the callback that prepares a final searchable document. * - * Boot-only. The callback persists for the worker lifetime, so registering - * it per request would leak captured request state into later requests. + * The callback persists for the worker lifetime, so registering it per + * request would leak captured request state into later requests. * * @param callable(array, Model, Engine): array $callback */ + #[BootOnly] public static function prepareSearchableDocumentUsing(callable $callback): void { static::$prepareSearchableDocumentCallback = Closure::fromCallable($callback); @@ -157,11 +160,12 @@ public static function prepareSearchableDocument(array $document, Model $model, /** * Specify the callback that prepares final index settings. * - * Boot-only. The callback persists for the worker lifetime, so registering - * it per request would leak captured request state into later requests. + * The callback persists for the worker lifetime, so registering it per + * request would leak captured request state into later requests. * * @param callable(array, null|Model, Engine, string): array $callback */ + #[BootOnly] public static function prepareIndexSettingsUsing(callable $callback): void { static::$prepareIndexSettingsCallback = Closure::fromCallable($callback); @@ -184,11 +188,12 @@ public static function prepareIndexSettings(array $settings, ?Model $model, Engi /** * Specify the callback that guards a whole-model index flush. * - * Boot-only. The callback persists for the worker lifetime, so registering - * it per request would leak captured request state into later requests. + * The callback persists for the worker lifetime, so registering it per + * request would leak captured request state into later requests. * * @param callable(Model, Engine, bool): void $callback */ + #[BootOnly] public static function guardModelFlushUsing(callable $callback): void { static::$guardModelFlushCallback = Closure::fromCallable($callback); @@ -207,11 +212,12 @@ public static function guardModelFlush(Model $model, Engine $engine, bool $force /** * Specify the job class that should make models searchable. * - * Boot-only. The job class persists in a static property for the worker - * lifetime and is used by every subsequent searchable import. + * The job class persists in a static property for the worker lifetime + * and is used by every subsequent searchable import. * * @param class-string $class */ + #[BootOnly] public static function makeSearchableUsing(string $class): void { static::$makeSearchableJob = $class; @@ -220,11 +226,12 @@ public static function makeSearchableUsing(string $class): void /** * Specify the job class that should remove models from the search index. * - * Boot-only. The job class persists in a static property for the worker - * lifetime and is used by every subsequent searchable removal. + * The job class persists in a static property for the worker lifetime + * and is used by every subsequent searchable removal. * * @param class-string $class */ + #[BootOnly] public static function removeFromSearchUsing(string $class): void { static::$removeFromSearchJob = $class; diff --git a/src/scout/src/Searchable.php b/src/scout/src/Searchable.php index 3a574ecf49..31edf878df 100644 --- a/src/scout/src/Searchable.php +++ b/src/scout/src/Searchable.php @@ -20,6 +20,7 @@ use Hypervel\Scout\Engines\Engine; use Hypervel\Scout\Events\ModelsFlushed; use Hypervel\Scout\Events\ModelsImported; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection as BaseCollection; use LogicException; use Throwable; @@ -68,8 +69,9 @@ public static function bootSearchable(): void /** * Register the searchable macros on collections. * - * Boot-only. These macros persist for the worker lifetime and affect every subsequent request. + * These macros persist for the worker lifetime and affect every subsequent request. */ + #[BootOnly] public function registerSearchableMacros(): void { BaseCollection::macro('searchable', function () { diff --git a/src/sentry/composer.json b/src/sentry/composer.json index 6e68c3c2fd..9bed882d7d 100644 --- a/src/sentry/composer.json +++ b/src/sentry/composer.json @@ -32,6 +32,7 @@ "php": "^8.4", "ext-filter": "*", "guzzlehttp/psr7": "^3.1", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", diff --git a/src/sentry/src/Features/CacheIntegration.php b/src/sentry/src/Features/CacheIntegration.php index 5b58999925..171f08fcdd 100644 --- a/src/sentry/src/Features/CacheIntegration.php +++ b/src/sentry/src/Features/CacheIntegration.php @@ -56,7 +56,7 @@ class CacheIntegration extends Feature /** * Indicates whether to attempt to detect the session key when running in the console. * - * Tests only. This mutates the feature singleton and should not be used at runtime. + * Setting this mutates the shared feature instance for the worker lifetime. * * @internal this is mainly intended for testing purposes */ diff --git a/src/sentry/src/Features/HttpClientIntegration.php b/src/sentry/src/Features/HttpClientIntegration.php index 5091216d95..7857d2358e 100644 --- a/src/sentry/src/Features/HttpClientIntegration.php +++ b/src/sentry/src/Features/HttpClientIntegration.php @@ -11,6 +11,7 @@ use GuzzleHttp\TransferStats; use Hypervel\Sentry\Features\Concerns\ResolvesEventOrigin; use Hypervel\Sentry\Integration; +use Hypervel\Support\Attributes\BootOnly; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; @@ -62,8 +63,9 @@ public function isApplicable(): bool /** * Instrument outgoing Guzzle requests. * - * Boot-only. The middleware persists for the worker lifetime and affects every Guzzle client. + * The middleware persists for the worker lifetime and affects every Guzzle client. */ + #[BootOnly] public function onBoot(): void { $userConfig = $this->getUserConfig(); diff --git a/src/sentry/src/Features/Storage/FilesystemAdapterDecorator.php b/src/sentry/src/Features/Storage/FilesystemAdapterDecorator.php index 75359f7beb..caae66640d 100644 --- a/src/sentry/src/Features/Storage/FilesystemAdapterDecorator.php +++ b/src/sentry/src/Features/Storage/FilesystemAdapterDecorator.php @@ -99,9 +99,9 @@ public function temporaryUploadUrl(string $path, DateTimeInterface $expiration, /** * Define a custom temporary URL builder callback. * - * Boot-only. The callback persists on the cached disk adapter for the - * worker lifetime and runs on every subsequent temporary URL generation for - * that disk. + * This changes the wrapped adapter. On a cached disk, the callback persists + * for the worker lifetime and runs on every subsequent temporary URL + * generation for that disk. */ public function buildTemporaryUrlsUsing(?Closure $callback): void { @@ -111,8 +111,8 @@ public function buildTemporaryUrlsUsing(?Closure $callback): void /** * Define a custom temporary upload URL builder callback. * - * Boot-only. The callback persists on the cached disk adapter for the - * worker lifetime and runs on every subsequent temporary upload URL + * This changes the wrapped adapter. On a cached disk, the callback persists + * for the worker lifetime and runs on every subsequent temporary upload URL * generation for that disk. */ public function buildTemporaryUploadUrlsUsing(?Closure $callback): void diff --git a/src/sentry/src/Hub.php b/src/sentry/src/Hub.php index d0b89bbae4..18e14aff62 100644 --- a/src/sentry/src/Hub.php +++ b/src/sentry/src/Hub.php @@ -8,6 +8,7 @@ use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Foundation\Application; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOnly; use Psr\Log\NullLogger; use Sentry\Attachment\Attachment; use Sentry\Breadcrumb; @@ -64,10 +65,10 @@ public function getClient(): ?ClientInterface /** * Bind a new client to the hub. * - * Boot-only. Mutates the shared client on the singleton Hub; per-request use - * races across coroutines, and existing coroutine layer stacks may already - * hold the previous client. + * On the shared Hub, per-request use races across coroutines, and existing + * coroutine layer stacks may already hold the previous client. */ + #[BootOnly(whenShared: true)] public function bindClient(ClientInterface $client): void { $this->client = $client; diff --git a/src/sentry/src/Integration.php b/src/sentry/src/Integration.php index ebc6045ff2..ecfe4605c9 100644 --- a/src/sentry/src/Integration.php +++ b/src/sentry/src/Integration.php @@ -62,8 +62,8 @@ public function setupOnce(): void /** * Register the exception handler with the Hypervel exception configuration. * - * Boot-only. Registers a reportable callback on the singleton exception - * handler that persists for the worker lifetime. + * When the configuration wraps the application's worker-shared handler, + * the reportable callback persists for the worker lifetime. */ public static function handles(Exceptions $exceptions): void { diff --git a/src/sentry/src/SentryHandler.php b/src/sentry/src/SentryHandler.php index 107800e4fb..01a08eb0f0 100644 --- a/src/sentry/src/SentryHandler.php +++ b/src/sentry/src/SentryHandler.php @@ -4,6 +4,7 @@ namespace Hypervel\Sentry; +use Hypervel\Support\Attributes\BootOnly; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Monolog\Handler\AbstractProcessingHandler; @@ -219,9 +220,10 @@ protected function getDefaultBatchFormatter(): FormatterInterface /** * Set the release. * - * Boot-only. Mutates the release on the worker-lifetime log handler; per-request - * use races across coroutines and affects every subsequent log event. + * On a cached log channel's handler, per-request use races across coroutines + * and affects every subsequent log event. */ + #[BootOnly(whenShared: true)] public function setRelease(string $value): self { $this->release = $value; @@ -232,9 +234,10 @@ public function setRelease(string $value): self /** * Set the current application environment. * - * Boot-only. Mutates the environment on the worker-lifetime log handler; - * per-request use races across coroutines and affects every subsequent log event. + * On a cached log channel's handler, per-request use races across coroutines + * and affects every subsequent log event. */ + #[BootOnly(whenShared: true)] public function setEnvironment(string $value): self { $this->environment = $value; diff --git a/src/sentry/src/SentryServiceProvider.php b/src/sentry/src/SentryServiceProvider.php index 0c5d72c04b..64ad981fae 100644 --- a/src/sentry/src/SentryServiceProvider.php +++ b/src/sentry/src/SentryServiceProvider.php @@ -95,8 +95,8 @@ class SentryServiceProvider extends ServiceProvider /** * The abstract type to bind Sentry as in the service container. * - * Boot-only. This value is read while registering container aliases and - * config paths; runtime changes split later config reads from the + * This value is read while registering container aliases and config + * paths; runtime changes split later config reads from the * already-registered binding. */ public static string $abstract = 'sentry'; diff --git a/src/server-process/composer.json b/src/server-process/composer.json index 2b29736ca5..3e1fcd661e 100644 --- a/src/server-process/composer.json +++ b/src/server-process/composer.json @@ -31,11 +31,12 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/contracts": "^0.4", "hypervel/coordinator": "^0.4", + "hypervel/core": "^0.4", "hypervel/coroutine": "^0.4", "hypervel/engine": "^0.4", - "hypervel/core": "^0.4", "hypervel/support": "^0.4" }, "extra": { diff --git a/src/server-process/src/AbstractProcess.php b/src/server-process/src/AbstractProcess.php index ce85cc73d8..e6da7601f1 100644 --- a/src/server-process/src/AbstractProcess.php +++ b/src/server-process/src/AbstractProcess.php @@ -17,6 +17,7 @@ use Hypervel\ServerProcess\Events\BeforeProcessHandle; use Hypervel\ServerProcess\Events\PipeMessage; use Hypervel\ServerProcess\Exceptions\SocketAcceptException; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Sleep; use RuntimeException; use Swoole\Coroutine\Socket; @@ -64,7 +65,11 @@ public function isEnabled(Server $server): bool /** * Create process objects and bind them to the server. + * + * Swoole only accepts processes added before the server starts, and + * coroutine-enabled processes persist in the process-wide collector. */ + #[BootOnly] public function bind(Server $server): void { for ($i = 0; $i < $this->processCount; ++$i) { diff --git a/src/server-process/src/ProcessCollector.php b/src/server-process/src/ProcessCollector.php index 2d3edb42fd..f716c3b88c 100644 --- a/src/server-process/src/ProcessCollector.php +++ b/src/server-process/src/ProcessCollector.php @@ -4,6 +4,7 @@ namespace Hypervel\ServerProcess; +use Hypervel\Support\Attributes\BootOnly; use Swoole\Process; /** @@ -19,10 +20,11 @@ class ProcessCollector /** * Add a process to the collector under the given name. * - * Boot-only. The collector is process-wide static state populated during - * server boot; runtime use mutates state shared across every coroutine in - * the worker. + * The collector is process-wide static state populated during server + * boot; runtime use mutates state shared across every coroutine in the + * worker. */ + #[BootOnly] public static function add(string $name, Process $process): void { static::$processes[$name][] = $process; diff --git a/src/server-process/src/ProcessManager.php b/src/server-process/src/ProcessManager.php index af3c269765..2fd8b4b889 100644 --- a/src/server-process/src/ProcessManager.php +++ b/src/server-process/src/ProcessManager.php @@ -5,6 +5,7 @@ namespace Hypervel\ServerProcess; use Hypervel\Contracts\ServerProcess\ProcessInterface; +use Hypervel\Support\Attributes\BootOnly; use RuntimeException; class ProcessManager @@ -17,9 +18,10 @@ class ProcessManager /** * Register a server process. * - * Boot-only. Process registrations persist in static state and must be - * completed before the server process manager starts running. + * Process registrations persist in static state and must be completed + * before the server process manager starts running. */ + #[BootOnly] public static function register(ProcessInterface $process): void { if (static::$running) { @@ -50,9 +52,9 @@ public static function isRunning(): bool /** * Set the running state. * - * Boot-only. Part of the server lifecycle; also called by the SIGTERM - * handler at shutdown. Mutates worker-lifetime static state; per-request - * use races across coroutines and disables the register() guard. + * Part of the server lifecycle; also called by the SIGTERM handler at + * shutdown. Mutates worker-lifetime static state; per-request use races + * across coroutines and disables the register() guard. */ public static function setRunning(bool $running): void { diff --git a/src/server/composer.json b/src/server/composer.json index a6a984089d..b0190c8161 100644 --- a/src/server/composer.json +++ b/src/server/composer.json @@ -33,8 +33,7 @@ "ext-filter": "*", "ext-posix": "*", "ext-swoole": "^6.3.0RC1", - "psr/log": "^3.0", - "symfony/console": "^8.1.10", + "hypervel/attributes": "^0.4", "hypervel/console": "^0.4", "hypervel/contracts": "^0.4", "hypervel/core": "^0.4", @@ -43,7 +42,9 @@ "hypervel/events": "^0.4", "hypervel/filesystem": "^0.4", "hypervel/server-process": "^0.4", - "hypervel/support": "^0.4" + "hypervel/support": "^0.4", + "psr/log": "^3.0", + "symfony/console": "^8.1.10" }, "config": { "sort-packages": true diff --git a/src/server/src/Server.php b/src/server/src/Server.php index 7fdb519013..85ab77b1c2 100644 --- a/src/server/src/Server.php +++ b/src/server/src/Server.php @@ -15,6 +15,7 @@ use Hypervel\Core\Events\BeforeServerStart; use Hypervel\Server\Exceptions\InvalidArgumentException; use Hypervel\Server\Exceptions\ServerException; +use Hypervel\Support\Attributes\BootOnly; use Psr\Log\LoggerInterface; use Swoole\Coroutine\CanceledException; use Swoole\Http\Request as SwooleRequest; @@ -41,9 +42,10 @@ public function __construct(protected Container $container, protected LoggerInte /** * Initialize the server with the given configuration. * - * Boot-only. Mutates the underlying Swoole server and registered callbacks; - * runtime use races with active workers and request handling. + * Mutates the underlying Swoole server and registered callbacks; runtime + * use races with active workers and request handling. */ + #[BootOnly] public function init(ServerConfig $config): ServerInterface { $this->initServers($config); @@ -77,7 +79,11 @@ public function getServer(): SwooleServer /** * Initialize all server ports from the configuration. + * + * Mutates the underlying Swoole server and the process-wide server + * registry; runtime use races with active workers and request handling. */ + #[BootOnly] protected function initServers(ServerConfig $config): void { $servers = $this->sortServers($config->getServers()); diff --git a/src/server/src/ServerFactory.php b/src/server/src/ServerFactory.php index 41f0e57031..10f40a551f 100644 --- a/src/server/src/ServerFactory.php +++ b/src/server/src/ServerFactory.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Events\Dispatcher as DispatcherContract; use Hypervel\Server\Entry\EventDispatcher; use Hypervel\Server\Entry\Logger; +use Hypervel\Support\Attributes\BootOnly; use Psr\Log\LoggerInterface; class ServerFactory @@ -61,9 +62,10 @@ public function getServer(): ServerInterface /** * Set the server instance. * - * Boot-only. Mutates the shared ServerFactory before startup; runtime use - * races across coroutines and changes the server used by later calls. + * Mutates the shared ServerFactory before startup; runtime use races + * across coroutines and changes the server used by later calls. */ + #[BootOnly(whenShared: true)] public function setServer(ServerInterface $server): static { $this->server = $server; @@ -84,9 +86,10 @@ public function getEventDispatcher(): DispatcherContract /** * Set the event dispatcher instance. * - * Boot-only. Set before configure(); the constructed Server captures this - * dispatcher and does not pick up later changes. + * Set before configure(); the constructed Server captures this dispatcher + * and does not pick up later changes. */ + #[BootOnly(whenShared: true)] public function setEventDispatcher(DispatcherContract $eventDispatcher): static { $this->eventDispatcher = $eventDispatcher; @@ -107,9 +110,10 @@ public function getLogger(): LoggerInterface /** * Set the logger instance. * - * Boot-only. Set before configure(); the constructed Server captures this - * logger and does not pick up later changes. + * Set before configure(); the constructed Server captures this logger and + * does not pick up later changes. */ + #[BootOnly(whenShared: true)] public function setLogger(LoggerInterface $logger): static { $this->logger = $logger; diff --git a/src/server/src/ServerInterface.php b/src/server/src/ServerInterface.php index f8dd0ada59..de444aed41 100644 --- a/src/server/src/ServerInterface.php +++ b/src/server/src/ServerInterface.php @@ -4,6 +4,7 @@ namespace Hypervel\Server; +use Hypervel\Support\Attributes\BootOnly; use Swoole\Server as SwooleServer; interface ServerInterface @@ -17,6 +18,7 @@ interface ServerInterface /** * Initialize the server with the given configuration. */ + #[BootOnly] public function init(ServerConfig $config): ServerInterface; /** diff --git a/src/server/src/ServerManager.php b/src/server/src/ServerManager.php index 25c71746c2..1a7cba875c 100644 --- a/src/server/src/ServerManager.php +++ b/src/server/src/ServerManager.php @@ -4,6 +4,8 @@ namespace Hypervel\Server; +use Hypervel\Support\Attributes\BootOnly; + class ServerManager { protected static array $container = []; @@ -11,10 +13,11 @@ class ServerManager /** * Add a server entry to the registry. * - * Boot-only. The registry is process-wide static state populated during - * server boot (before fork); runtime use mutates state shared across every - * coroutine in the worker. + * The registry is process-wide static state populated during server boot + * (before fork); runtime use mutates state shared across every coroutine + * in the worker. */ + #[BootOnly] public static function add(string $name, array $value): void { static::$container[$name] = $value; diff --git a/src/session/composer.json b/src/session/composer.json index 86236e8d82..5befef2ae7 100644 --- a/src/session/composer.json +++ b/src/session/composer.json @@ -28,6 +28,7 @@ "ext-ctype": "*", "ext-mbstring": "*", "ext-session": "*", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/cache": "^0.4", "hypervel/collections": "^0.4", diff --git a/src/session/src/DatabaseSessionHandler.php b/src/session/src/DatabaseSessionHandler.php index b43c6d8703..da57ebb8fe 100644 --- a/src/session/src/DatabaseSessionHandler.php +++ b/src/session/src/DatabaseSessionHandler.php @@ -13,6 +13,7 @@ use Hypervel\Database\UniqueConstraintViolationException; use Hypervel\Session\Concerns\ValidatesUserSessionArguments; use Hypervel\Session\Contracts\CanManageUserSessions; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection; use Hypervel\Support\InteractsWithTime; @@ -354,12 +355,13 @@ public function connection(): ConnectionInterface /** * Set the application instance used by the handler. * - * Boot or tests only. Mutating the container on a shared handler during - * request handling can expose the wrong request or authentication state - * to concurrent coroutines. + * Mutating the container on a shared handler during request handling + * can expose the wrong request or authentication state to concurrent + * coroutines. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/session/src/Middleware/AuthenticateSession.php b/src/session/src/Middleware/AuthenticateSession.php index 866aeae4d8..cb6c58c5cb 100644 --- a/src/session/src/Middleware/AuthenticateSession.php +++ b/src/session/src/Middleware/AuthenticateSession.php @@ -10,6 +10,7 @@ use Hypervel\Contracts\Auth\Guard; use Hypervel\Contracts\Session\Middleware\AuthenticatesSessions; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; class AuthenticateSession implements AuthenticatesSessions { @@ -135,9 +136,10 @@ protected function redirectTo(Request $request): ?string /** * Specify the callback that should be used to generate the redirect path. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every subsequent session-mismatch. + * The callback persists in a static property for the worker lifetime + * and runs on every subsequent session-mismatch. */ + #[BootOnly] public static function redirectUsing(callable $redirectToCallback): void { static::$redirectToCallback = $redirectToCallback; diff --git a/src/session/src/Middleware/StartSession.php b/src/session/src/Middleware/StartSession.php index c66ce4acd7..1858830b3b 100644 --- a/src/session/src/Middleware/StartSession.php +++ b/src/session/src/Middleware/StartSession.php @@ -16,6 +16,7 @@ use Hypervel\Routing\Route; use Hypervel\Session\SessionManager; use Hypervel\Session\Store; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\CarbonImmutable; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; @@ -248,13 +249,14 @@ public static function resolveCookieConfig(Request $request, array $config): arr /** * Register a callback to configure the session cookie attributes. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every session cookie write across all coroutines. - * Callbacks run in registration order; later callbacks receive the values - * returned by earlier callbacks and may overwrite them. + * The callback persists in a static property for the worker lifetime and + * runs on every session cookie write across all coroutines. Callbacks run + * in registration order; later callbacks receive the values returned by + * earlier callbacks and may overwrite them. * * @param Closure(Request, array): array $callback */ + #[BootOnly] public static function configureSessionCookieUsing(Closure $callback): void { static::$sessionCookieCallbacks[] = $callback; diff --git a/src/session/src/SessionManager.php b/src/session/src/SessionManager.php index 5dd9a13434..ee59ee7a8c 100644 --- a/src/session/src/SessionManager.php +++ b/src/session/src/SessionManager.php @@ -10,6 +10,7 @@ use Hypervel\Contracts\Encryption\Encrypter; use Hypervel\Contracts\Redis\Factory as RedisFactory; use Hypervel\Session\Contracts\CanManageUserSessions; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Manager; use InvalidArgumentException; use SessionHandlerInterface; @@ -290,8 +291,9 @@ public function getDefaultDriver(): string /** * Set the default session driver name. * - * Boot-only. Mutates process-global config; per-request use races across coroutines. + * Mutates process-global config; per-request use races across coroutines. */ + #[BootOnly] public function setDefaultDriver(UnitEnum|string $name): void { if ($name instanceof UnitEnum) { diff --git a/src/session/src/Store.php b/src/session/src/Store.php index 72efb15960..41af18a4a7 100644 --- a/src/session/src/Store.php +++ b/src/session/src/Store.php @@ -15,6 +15,7 @@ use Hypervel\Contracts\Session\Session; use Hypervel\Http\Request; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Cache; use Hypervel\Support\Facades\Date; @@ -686,9 +687,10 @@ public function getName(): string /** * Set the name of the session. * - * Boot-only. Mutates the shared session store name; per-request use races - * across coroutines and changes the cookie name for concurrent requests. + * Mutates the shared session store name; per-request use races across + * coroutines and changes the cookie name for concurrent requests. */ + #[BootOnly(whenShared: true)] public function setName(string $name): void { $this->name = $name; @@ -860,10 +862,11 @@ public function getHandler(): SessionHandlerInterface /** * Set the underlying session handler implementation. * - * Boot-only. Replaces the handler on the shared session store; per-request - * use races across coroutines and can route session reads or writes through - * the wrong handler. + * Replaces the handler on the shared session store; per-request use races + * across coroutines and can route session reads or writes through the wrong + * handler. */ + #[BootOnly(whenShared: true)] public function setHandler(SessionHandlerInterface $handler): SessionHandlerInterface { return $this->handler = $handler; diff --git a/src/session/src/SymfonySessionDecorator.php b/src/session/src/SymfonySessionDecorator.php index 4422c8fd1a..96c3f10d94 100644 --- a/src/session/src/SymfonySessionDecorator.php +++ b/src/session/src/SymfonySessionDecorator.php @@ -6,6 +6,7 @@ use BadMethodCallException; use Hypervel\Contracts\Session\Session; +use Hypervel\Support\Attributes\Unsupported; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; @@ -43,8 +44,9 @@ public function getName(): string /** * Set the session name. * - * Boot-only. Delegates to the shared session store name; per-request use - * races across coroutines and changes the cookie name for concurrent requests. + * This changes the wrapped store's name. That store is usually the + * worker-shared session store, where per-request use races across + * coroutines and changes the cookie name for concurrent requests. */ public function setName(string $name): void { @@ -113,6 +115,7 @@ public function isStarted(): bool /** * @throws BadMethodCallException */ + #[Unsupported] public function registerBag(SessionBagInterface $bag): void { throw new BadMethodCallException('Method not implemented by Hypervel.'); @@ -121,6 +124,7 @@ public function registerBag(SessionBagInterface $bag): void /** * @throws BadMethodCallException */ + #[Unsupported] public function getBag(string $name): SessionBagInterface { throw new BadMethodCallException('Method not implemented by Hypervel.'); @@ -129,6 +133,7 @@ public function getBag(string $name): SessionBagInterface /** * @throws BadMethodCallException */ + #[Unsupported] public function getMetadataBag(): MetadataBag { throw new BadMethodCallException('Method not implemented by Hypervel.'); diff --git a/src/signal/composer.json b/src/signal/composer.json index 5d05449903..6fbcd816c0 100644 --- a/src/signal/composer.json +++ b/src/signal/composer.json @@ -30,10 +30,11 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/contracts": "^0.4", + "hypervel/core": "^0.4", "hypervel/coroutine": "^0.4", "hypervel/engine": "^0.4", - "hypervel/core": "^0.4", "hypervel/server-process": "^0.4", "hypervel/support": "^0.4" }, diff --git a/src/signal/src/SignalManager.php b/src/signal/src/SignalManager.php index 6f2c58836f..aaaaeb647b 100644 --- a/src/signal/src/SignalManager.php +++ b/src/signal/src/SignalManager.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Signal\SignalHandler; use Hypervel\Coroutine\Coroutine; use Hypervel\Coroutine\SignalRegistry; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\SafeCaller; use Hypervel\Support\SplPriorityQueue; use InvalidArgumentException; @@ -37,9 +38,10 @@ public function __construct(protected Container $container) /** * Start listening for signals for the given process type. * - * Boot-only. Call once for each process incarnation to avoid registering - * the configured handlers more than once. + * Call once for each process incarnation to avoid registering the + * configured handlers more than once. */ + #[BootOnly] public function listen(string $process): void { if (! in_array($process, [SignalHandler::WORKER, SignalHandler::SERVER_PROCESS], true)) { diff --git a/src/socialite/composer.json b/src/socialite/composer.json index a772072845..45a3a9c380 100644 --- a/src/socialite/composer.json +++ b/src/socialite/composer.json @@ -32,14 +32,15 @@ "php": "^8.4", "ext-filter": "*", "firebase/php-jwt": "^7.0", - "psr/http-message": "^2.0", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", "hypervel/coroutine": "^0.4", "hypervel/guzzle": "^8.3", "hypervel/http": "^0.4", - "hypervel/support": "^0.4" + "hypervel/support": "^0.4", + "psr/http-message": "^2.0" }, "config": { "sort-packages": true diff --git a/src/socialite/src/AbstractProvider.php b/src/socialite/src/AbstractProvider.php index e7a219445b..0de6d6385c 100644 --- a/src/socialite/src/AbstractProvider.php +++ b/src/socialite/src/AbstractProvider.php @@ -10,6 +10,7 @@ use Hypervel\Http\Request; use Hypervel\Socialite\Concerns\HasProviderContext; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; use LogicException; use SensitiveParameter; @@ -48,11 +49,12 @@ public function __construct(Request $request, protected array $guzzle = []) /** * Set the baseline provider configuration. * - * Boot-only. The configuration persists for the worker lifetime and affects - * every subsequent request. Use setConfig() for per-request overrides. + * On a worker-cached provider, the configuration persists and affects every + * subsequent request. Use setConfig() for per-request overrides. * * @return $this */ + #[BootOnly(whenShared: true)] public function withConfig(#[SensitiveParameter] array $config): static { $this->additionalConfig = $config; diff --git a/src/socialite/src/Socialite.php b/src/socialite/src/Socialite.php index 98e52c0fac..3f5f515b0c 100644 --- a/src/socialite/src/Socialite.php +++ b/src/socialite/src/Socialite.php @@ -8,6 +8,7 @@ use Hypervel\Socialite\Contracts\Factory; use Hypervel\Socialite\Contracts\User as UserContract; use Hypervel\Socialite\Testing\SocialiteFake; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Facades\Facade; /** @@ -25,6 +26,9 @@ */ class Socialite extends Facade { + /** + * Get the registered name of the component. + */ protected static function getFacadeAccessor(): string { return Factory::class; @@ -32,7 +36,10 @@ protected static function getFacadeAccessor(): string /** * Register a fake Socialite instance. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fake(string $driver, UserContract|Closure|null $user = null): SocialiteFake { $root = static::getFacadeRoot(); diff --git a/src/socialite/src/Two/AbstractProvider.php b/src/socialite/src/Two/AbstractProvider.php index acd6d8e791..87d5bb08fe 100644 --- a/src/socialite/src/Two/AbstractProvider.php +++ b/src/socialite/src/Two/AbstractProvider.php @@ -13,6 +13,7 @@ use Hypervel\Socialite\Two\Exceptions\InvalidAudienceException; use Hypervel\Socialite\Two\Exceptions\InvalidCodeException; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; use SensitiveParameter; @@ -421,12 +422,13 @@ public function getScopes(): array /** * Set the callback that formats redirect URLs for the provider. * - * Boot-only. The callback persists on the worker-cached provider and affects - * every subsequent request. Use setConfig() for per-request redirect values. + * On a worker-cached provider, the callback persists and affects every + * subsequent request. Use setConfig() for per-request redirect values. * * @param Closure(array): string $formatter * @return $this */ + #[BootOnly(whenShared: true)] public function withRedirectFormatter(Closure $formatter): static { $this->redirectFormatter = $formatter; diff --git a/src/support/composer.json b/src/support/composer.json index c541a18035..65d005841a 100644 --- a/src/support/composer.json +++ b/src/support/composer.json @@ -26,6 +26,7 @@ "php": "^8.4", "ext-filter": "*", "doctrine/inflector": "^2.0.5", + "hypervel/attributes": "^0.4", "hypervel/bus": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", diff --git a/src/support/src/BinaryCodec.php b/src/support/src/BinaryCodec.php index 77cb85cca5..2c01d7f962 100644 --- a/src/support/src/BinaryCodec.php +++ b/src/support/src/BinaryCodec.php @@ -4,6 +4,7 @@ namespace Hypervel\Support; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Uuid; @@ -20,9 +21,10 @@ class BinaryCodec /** * Register a custom codec. * - * Boot-only. Codecs persist in a static property for the worker lifetime - * and apply to every subsequent encode/decode call. + * Codecs persist in a static property for the worker lifetime and apply + * to every subsequent encode/decode call. */ + #[BootOnly] public static function register(string $name, callable $encode, callable $decode): void { self::$customCodecs[$name] = [ diff --git a/src/support/src/ClearStatCache.php b/src/support/src/ClearStatCache.php index 4f446a1270..c56fe94a95 100644 --- a/src/support/src/ClearStatCache.php +++ b/src/support/src/ClearStatCache.php @@ -4,6 +4,8 @@ namespace Hypervel\Support; +use Hypervel\Support\Attributes\BootOnly; + class ClearStatCache { /** @@ -56,9 +58,10 @@ public static function getInterval(): int /** * Set the clear interval in seconds. * - * Boot-only. The interval persists in a static property for the worker - * lifetime and affects every subsequent stat-cache clear check. + * The interval persists in a static property for the worker lifetime + * and affects every subsequent stat-cache clear check. */ + #[BootOnly] public static function setInterval(int $interval): void { self::$interval = $interval; diff --git a/src/support/src/Composer.php b/src/support/src/Composer.php index f57fdd8d53..caa73fb1ea 100644 --- a/src/support/src/Composer.php +++ b/src/support/src/Composer.php @@ -8,6 +8,7 @@ use Composer\Autoload\ClassLoader; use Composer\InstalledVersions; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Traits\GuardsProcessOutput; use JsonException; use RuntimeException; @@ -278,9 +279,10 @@ public static function getLoader(): ClassLoader /** * Set the Composer autoloader instance. * - * Tests only. Swaps the cached Composer loader for the worker lifetime and - * affects every subsequent class-map registration. + * Swaps the cached Composer loader for the worker lifetime and affects + * every subsequent class-map registration. */ + #[TestsOnly] public static function setLoader(ClassLoader $classLoader): ClassLoader { return static::$classLoader = $classLoader; diff --git a/src/support/src/ConfigurationUrlParser.php b/src/support/src/ConfigurationUrlParser.php index 871d657703..6a85e9d2d9 100644 --- a/src/support/src/ConfigurationUrlParser.php +++ b/src/support/src/ConfigurationUrlParser.php @@ -4,6 +4,7 @@ namespace Hypervel\Support; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; class ConfigurationUrlParser @@ -189,9 +190,10 @@ public static function getDriverAliases(): array /** * Add the given driver alias to the driver aliases array. * - * Boot-only. Driver aliases persist in a static property for the worker - * lifetime and apply to every subsequent configuration URL parse. + * Driver aliases persist in a static property for the worker lifetime + * and apply to every subsequent configuration URL parse. */ + #[BootOnly] public static function addDriverAlias(string $alias, string $driver): void { static::$driverAliases[$alias] = $driver; diff --git a/src/support/src/DateFactory.php b/src/support/src/DateFactory.php index 84b698bf5e..ab2e4198f3 100644 --- a/src/support/src/DateFactory.php +++ b/src/support/src/DateFactory.php @@ -11,6 +11,7 @@ use Closure; use DateTimeInterface; use DateTimeZone; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; use ReflectionClass; use Symfony\Contracts\Translation\TranslatorInterface; @@ -137,11 +138,12 @@ class DateFactory /** * Use the given handler when generating dates (class name, callable, or factory). * - * Boot-only. The selected handler persists in static state for the worker - * lifetime and affects date creation in every subsequent request. + * The selected handler persists in static state for the worker lifetime + * and affects date creation in every subsequent request. * * @throws InvalidArgumentException */ + #[BootOnly] public static function use(mixed $handler): void { if ($handler instanceof Factory) { @@ -167,9 +169,10 @@ public static function use(mixed $handler): void /** * Use the default date class when generating dates. * - * Boot-only. Clearing the worker-wide handler affects date creation in - * every subsequent request. + * Clearing the worker-wide handler affects date creation in every + * subsequent request. */ + #[BootOnly] public static function useDefault(): void { static::$dateClass = null; @@ -180,9 +183,10 @@ public static function useDefault(): void /** * Execute the given callable on each date creation. * - * Boot-only. The callable persists in a static property for the worker - * lifetime and runs on every date creation across all coroutines. + * The callable persists in a static property for the worker lifetime + * and runs on every date creation across all coroutines. */ + #[BootOnly] public static function useCallable(callable $callable): void { static::$callable = Closure::fromCallable($callable); @@ -194,11 +198,12 @@ public static function useCallable(callable $callable): void /** * Use the given date type (class) when generating dates. * - * Boot-only. The class name persists in a static property for the worker - * lifetime and is used for every date creation across all coroutines. + * The class name persists in a static property for the worker lifetime + * and is used for every date creation across all coroutines. * * @param class-string $dateClass */ + #[BootOnly] public static function useClass(string $dateClass): void { if ( @@ -219,9 +224,10 @@ public static function useClass(string $dateClass): void /** * Use the given Carbon factory when generating dates. * - * Boot-only. The factory persists in a static property for the worker - * lifetime and is used for every date creation across all coroutines. + * The factory persists in a static property for the worker lifetime + * and is used for every date creation across all coroutines. */ + #[BootOnly] public static function useFactory(Factory $factory): void { static::$factory = $factory; diff --git a/src/support/src/DotenvManager.php b/src/support/src/DotenvManager.php index 82628aa852..4b6468feef 100644 --- a/src/support/src/DotenvManager.php +++ b/src/support/src/DotenvManager.php @@ -5,6 +5,7 @@ namespace Hypervel\Support; use Dotenv\Dotenv; +use Hypervel\Support\Attributes\BootOnly; class DotenvManager { @@ -31,9 +32,10 @@ public static function getLoadedVariableNames(): array * This is a one-shot method — subsequent calls return early if values * have already been loaded. Use reload() to re-read the env file. * - * Boot-only. Loaded values and the one-shot cache persist for the worker - * lifetime and affect every subsequent request. + * Loaded values and the one-shot cache persist for the worker lifetime + * and affect every subsequent request. */ + #[BootOnly] public static function load(array $paths, ?string $name = null): void { if (static::$cachedValues !== null) { @@ -50,9 +52,10 @@ public static function load(array $paths, ?string $name = null): void * which returns an empty array if the file doesn't exist instead * of throwing. InvalidFileException is still thrown for malformed files. * - * Boot-only. Loaded values and the one-shot cache persist for the worker - * lifetime and affect every subsequent request. + * Loaded values and the one-shot cache persist for the worker lifetime + * and affect every subsequent request. */ + #[BootOnly] public static function safeLoad(array $paths, ?string $name = null): void { if (static::$cachedValues !== null) { @@ -70,9 +73,10 @@ public static function safeLoad(array $paths, ?string $name = null): void * then safely re-reads the env file. A missing file publishes an empty * environment instead of retaining values from the previous load. * - * Boot-only. Reloading mutates process-global environment values observed - * by every concurrent and subsequent request in the worker. + * Reloading mutates process-global environment values observed by every + * concurrent and subsequent request in the worker. */ + #[BootOnly] public static function reload(array $paths, ?string $name = null): void { if (static::$cachedValues !== null) { diff --git a/src/support/src/EncodedHtmlString.php b/src/support/src/EncodedHtmlString.php index d93d43bfb3..299456a518 100644 --- a/src/support/src/EncodedHtmlString.php +++ b/src/support/src/EncodedHtmlString.php @@ -9,6 +9,7 @@ use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Support\DeferringDisplayableValue; use Hypervel\Contracts\Support\Htmlable; +use Hypervel\Support\Attributes\BootOnly; use Override; class EncodedHtmlString extends HtmlString @@ -73,9 +74,10 @@ public function toHtml(): string /** * Set the callable that will be used to encode the HTML strings. * - * Boot-only. The factory persists in a static property for the worker - * lifetime and applies to every encoded HTML string. + * The factory persists in a static property for the worker lifetime + * and applies to every encoded HTML string. */ + #[BootOnly] public static function encodeUsing(?callable $factory = null): void { static::$encodeUsingFactory = $factory === null ? null : Closure::fromCallable($factory); diff --git a/src/support/src/Env.php b/src/support/src/Env.php index a64d9c9b2f..027de7ba1e 100644 --- a/src/support/src/Env.php +++ b/src/support/src/Env.php @@ -14,6 +14,8 @@ use Dotenv\Repository\RepositoryInterface; use Hypervel\Contracts\Filesystem\FileNotFoundException; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; use PhpOption\Option; use RuntimeException; @@ -50,9 +52,10 @@ class Env /** * Enable the putenv adapter. * - * Boot-only. The setting and cached repository persist in static properties - * for the worker lifetime and affect every subsequent env lookup. + * The setting and cached repository persist in static properties for the + * worker lifetime and affect every subsequent env lookup. */ + #[BootOnly] public static function enablePutenv(): void { static::$putenv = true; @@ -62,8 +65,8 @@ public static function enablePutenv(): void /** * Flush the environment repository. * - * Boot or tests only. Clears the worker-wide repository cache; concurrent - * env lookups may rebuild with different adapter state. + * Clears the worker-wide repository cache; concurrent env lookups may + * rebuild with different adapter state. * * Clears the cached repository so the next getRepository() call creates * a fresh instance with a new ImmutableWriter. This is required before @@ -71,6 +74,7 @@ public static function enablePutenv(): void * and refuses to overwrite "externally defined" keys. Flushing creates * a clean writer that treats all keys as writable. */ + #[BootOrTestsOnly] public static function flushRepository(): void { static::$repository = null; @@ -105,9 +109,10 @@ public static function deleteMany(array $keys): void /** * Disable the putenv adapter. * - * Boot-only. The setting and cached repository persist in static properties - * for the worker lifetime and affect every subsequent env lookup. + * The setting and cached repository persist in static properties for the + * worker lifetime and affect every subsequent env lookup. */ + #[BootOnly] public static function disablePutenv(): void { static::$putenv = false; @@ -117,9 +122,10 @@ public static function disablePutenv(): void /** * Register a custom adapter creator Closure. * - * Boot-only. Custom adapters persist in a static property for the worker - * lifetime and affect every subsequent env repository build. + * Custom adapters persist in a static property for the worker lifetime + * and affect every subsequent env repository build. */ + #[BootOnly] public static function extend(Closure $callback, ?string $name = null): void { if (! is_null($name)) { diff --git a/src/support/src/Facades/Bus.php b/src/support/src/Facades/Bus.php index a7a0fafed3..af0428f2de 100644 --- a/src/support/src/Facades/Bus.php +++ b/src/support/src/Facades/Bus.php @@ -7,6 +7,7 @@ use Hypervel\Bus\BatchRepository; use Hypervel\Contracts\Bus\Dispatcher as BusDispatcherContract; use Hypervel\Foundation\Bus\PendingChain; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Testing\Fakes\BusFake; /** @@ -66,7 +67,10 @@ class Bus extends Facade { /** * Replace the bound instance with a fake. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fake(array|string $jobsToFake = [], ?BatchRepository $batchRepository = null): BusFake { $actualDispatcher = static::isFake() diff --git a/src/support/src/Facades/Cache.php b/src/support/src/Facades/Cache.php index 8812171234..d292cd3fc2 100644 --- a/src/support/src/Facades/Cache.php +++ b/src/support/src/Facades/Cache.php @@ -4,6 +4,7 @@ namespace Hypervel\Support\Facades; +use Hypervel\Support\Attributes\TestsOnly; use Mockery; use Mockery\MockInterface; @@ -92,8 +93,9 @@ class Cache extends Facade * Uses a partial spy on the real instance so that methods like `memo()` * execute their real implementation while still recording calls. * - * Tests only. Swaps the resolved facade instance for the worker lifetime. + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function spy(): ?MockInterface { if (! static::isMock()) { diff --git a/src/support/src/Facades/DB.php b/src/support/src/Facades/DB.php index 9eee15680a..0f173a126c 100644 --- a/src/support/src/Facades/DB.php +++ b/src/support/src/Facades/DB.php @@ -9,6 +9,7 @@ use Hypervel\Database\Console\Migrations\ResetCommand; use Hypervel\Database\Console\Migrations\RollbackCommand; use Hypervel\Database\Console\WipeCommand; +use Hypervel\Support\Attributes\BootOnly; /** * @method static void allowQueryDurationHandlersToRunAgain() @@ -152,9 +153,10 @@ class DB extends Facade * * Prohibits: db:wipe, migrate:fresh, migrate:refresh, migrate:reset, and migrate:rollback * - * Boot-only. The prohibition flags persist in static command state for the - * worker lifetime and affect every subsequent destructive database command. + * The prohibition flags persist in static command state for the worker + * lifetime and affect every subsequent destructive database command. */ + #[BootOnly] public static function prohibitDestructiveCommands(bool $prohibit = true): void { FreshCommand::prohibit($prohibit); diff --git a/src/support/src/Facades/Event.php b/src/support/src/Facades/Event.php index 1fb3d39a74..7e9b6b4a0a 100644 --- a/src/support/src/Facades/Event.php +++ b/src/support/src/Facades/Event.php @@ -5,6 +5,7 @@ namespace Hypervel\Support\Facades; use Hypervel\Database\Eloquent\Model; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Testing\Fakes\EventFake; /** @@ -52,7 +53,10 @@ class Event extends Facade { /** * Replace the bound instance with a fake. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fake(array|string $eventsToFake = []): EventFake { $actualDispatcher = static::isFake() @@ -69,7 +73,10 @@ public static function fake(array|string $eventsToFake = []): EventFake /** * Replace the bound instance with a fake that fakes all events except the given events. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fakeExcept(array|string $eventsToAllow): EventFake { return static::fake([ @@ -81,7 +88,10 @@ function ($eventName) use ($eventsToAllow) { /** * Replace the bound instance with a fake during the given callable's execution. + * + * Swaps the resolved facade instance worker-wide until the callable returns. */ + #[TestsOnly] public static function fakeFor(callable $callable, array $eventsToFake = []): mixed { $originalDispatcher = static::getFacadeRoot(); @@ -100,7 +110,10 @@ public static function fakeFor(callable $callable, array $eventsToFake = []): mi /** * Replace the bound instance with a fake during the given callable's execution. + * + * Swaps the resolved facade instance worker-wide until the callable returns. */ + #[TestsOnly] public static function fakeExceptFor(callable $callable, array $eventsToAllow = []): mixed { $originalDispatcher = static::getFacadeRoot(); diff --git a/src/support/src/Facades/Exceptions.php b/src/support/src/Facades/Exceptions.php index 436a4b659c..c259ed607a 100644 --- a/src/support/src/Facades/Exceptions.php +++ b/src/support/src/Facades/Exceptions.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Debug\ExceptionHandler; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Testing\Fakes\ExceptionHandlerFake; use Throwable; @@ -55,8 +56,11 @@ class Exceptions extends Facade /** * Replace the bound instance with a fake. * + * Swaps the resolved facade instance for the worker lifetime. + * * @param array>|class-string $exceptions */ + #[TestsOnly] public static function fake(array|string $exceptions = []): ExceptionHandlerFake { $exceptionHandler = static::isFake() diff --git a/src/support/src/Facades/Facade.php b/src/support/src/Facades/Facade.php index d7aa1047b8..c93385b593 100644 --- a/src/support/src/Facades/Facade.php +++ b/src/support/src/Facades/Facade.php @@ -8,6 +8,9 @@ use Hypervel\Contracts\Container\Container as ContainerContract; use Hypervel\Database\Eloquent\Model; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Benchmark; use Hypervel\Support\Collection; use Hypervel\Support\Js; @@ -62,9 +65,10 @@ abstract class Facade /** * Run a Closure when the facade has been resolved. * - * Boot-only. The callback is registered on the worker-global container - * and runs for every subsequent resolution of the facade accessor. + * The callback is registered on the worker-global container and runs + * for every subsequent resolution of the facade accessor. */ + #[BootOnly] public static function resolved(Closure $callback): void { $accessor = static::getFacadeAccessor(); @@ -83,8 +87,9 @@ public static function resolved(Closure $callback): void /** * Convert the facade into a Mockery spy. * - * Tests only. Swaps the resolved facade instance for the worker lifetime. + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function spy(): ?MockInterface { if (static::isMock()) { @@ -101,8 +106,9 @@ public static function spy(): ?MockInterface /** * Initiate a partial mock on the facade. * - * Tests only. Swaps the resolved facade instance for the worker lifetime. + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function partialMock(): MockInterface { $name = static::getFacadeAccessor(); @@ -117,8 +123,9 @@ public static function partialMock(): MockInterface /** * Initiate a mock expectation on the facade. * - * Tests only. Swaps the resolved facade instance for the worker lifetime. + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function shouldReceive(): CompositeExpectation|Expectation|HigherOrderMessage { $name = static::getFacadeAccessor(); @@ -133,8 +140,9 @@ public static function shouldReceive(): CompositeExpectation|Expectation|HigherO /** * Initiate a mock expectation on the facade. * - * Tests only. Swaps the resolved facade instance for the worker lifetime. + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function expects(): CompositeExpectation|Expectation|ExpectsHigherOrderMessage { $name = static::getFacadeAccessor(); @@ -148,7 +156,10 @@ public static function expects(): CompositeExpectation|Expectation|ExpectsHigher /** * Create a fresh mock instance for the given class. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] protected static function createFreshMockInstance(): MockInterface { return tap(static::createMock(), function ($mock) { @@ -194,10 +205,11 @@ protected static function getMockableClass(): ?string /** * Hotswap the underlying instance behind the facade. * - * Tests only. Replaces the worker-wide resolved facade instance and - * container binding; runtime use races across coroutines and changes every - * subsequent facade call. + * Replaces the worker-wide resolved facade instance and container binding; + * runtime use races across coroutines and changes every subsequent facade + * call. */ + #[TestsOnly] public static function swap(mixed $instance): void { static::$resolvedInstance[static::getFacadeAccessor()] = $instance; @@ -267,9 +279,10 @@ protected static function resolveFacadeInstance(string $name): mixed /** * Clear a resolved facade instance. * - * Boot or tests only. Clears a worker-wide facade cache entry; concurrent - * coroutines may still hold the previous resolved instance. + * Clears a worker-wide facade cache entry; concurrent coroutines may + * still hold the previous resolved instance. */ + #[BootOrTestsOnly] public static function clearResolvedInstance(?string $name = null): void { $name ??= static::getFacadeAccessor(); @@ -280,9 +293,10 @@ public static function clearResolvedInstance(?string $name = null): void /** * Clear all of the resolved instances. * - * Boot or tests only. Clears the worker-wide facade cache; concurrent - * coroutines may still hold previously resolved instances. + * Clears the worker-wide facade cache; concurrent coroutines may + * still hold previously resolved instances. */ + #[BootOrTestsOnly] public static function clearResolvedInstances(): void { static::$resolvedInstance = []; @@ -360,9 +374,10 @@ public static function getFacadeApplication(): ?ContainerContract /** * Set the application instance. * - * Boot or tests only. Replaces the worker-wide facade application reference; - * runtime use races across coroutines and breaks every facade lookup. + * Replaces the worker-wide facade application reference; runtime use races + * across coroutines and breaks every facade lookup. */ + #[BootOrTestsOnly] public static function setFacadeApplication(?ContainerContract $app): void { static::$app = $app; diff --git a/src/support/src/Facades/Http.php b/src/support/src/Facades/Http.php index 8c7ea7f9f7..da2613be90 100644 --- a/src/support/src/Facades/Http.php +++ b/src/support/src/Facades/Http.php @@ -8,6 +8,8 @@ use Hypervel\Http\Client\Factory; use Hypervel\Http\Client\Response; use Hypervel\Http\Client\ResponseSequence; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; /** * @method static \Hypervel\Http\Client\Factory allowStrayRequests(array|null $only = null) @@ -136,8 +138,9 @@ class Http extends Facade /** * Register a stub callable that will intercept requests and be able to return stub responses. * - * Tests only. Stubs persist on the HTTP factory for the worker lifetime. + * Stubs persist on the HTTP factory for the worker lifetime. */ + #[TestsOnly] public static function fake(array|callable|null $callback = null): Factory { return tap(static::getFacadeRoot(), function ($fake) use ($callback) { @@ -148,8 +151,9 @@ public static function fake(array|callable|null $callback = null): Factory /** * Register a response sequence for the given URL pattern. * - * Tests only. The sequence and stub persist on the HTTP factory for the worker lifetime. + * The sequence and stub persist on the HTTP factory for the worker lifetime. */ + #[TestsOnly] public static function fakeSequence(string $urlPattern = '*'): ResponseSequence { $fake = tap(static::getFacadeRoot(), function ($fake) { @@ -162,8 +166,9 @@ public static function fakeSequence(string $urlPattern = '*'): ResponseSequence /** * Indicate that an exception should be thrown if any request is not faked. * - * Boot or tests only. The policy persists on the HTTP factory for the worker lifetime. + * The policy persists on the HTTP factory for the worker lifetime. */ + #[BootOrTestsOnly] public static function preventStrayRequests(bool $prevent = true): Factory { return tap(static::getFacadeRoot(), function ($fake) use ($prevent) { @@ -174,8 +179,9 @@ public static function preventStrayRequests(bool $prevent = true): Factory /** * Stub the given URL using the given callback. * - * Tests only. The stub persists on the HTTP factory for the worker lifetime. + * The stub persists on the HTTP factory for the worker lifetime. */ + #[TestsOnly] public static function stubUrl(string $url, array|callable|int|PromiseInterface|Response|string $callback): Factory { return tap(static::getFacadeRoot(), function ($fake) use ($url, $callback) { diff --git a/src/support/src/Facades/Mail.php b/src/support/src/Facades/Mail.php index fe08b17352..a11d9eee63 100644 --- a/src/support/src/Facades/Mail.php +++ b/src/support/src/Facades/Mail.php @@ -4,6 +4,7 @@ namespace Hypervel\Support\Facades; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Testing\Fakes\MailFake; /** @@ -78,7 +79,10 @@ class Mail extends Facade { /** * Replace the bound instance with a fake. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fake(): MailFake { $actualMailManager = static::isFake() diff --git a/src/support/src/Facades/Notification.php b/src/support/src/Facades/Notification.php index 6b328c7654..c1f77976f2 100644 --- a/src/support/src/Facades/Notification.php +++ b/src/support/src/Facades/Notification.php @@ -6,6 +6,7 @@ use Hypervel\Notifications\AnonymousNotifiable; use Hypervel\Notifications\ChannelManager; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Testing\Fakes\NotificationFake; /** @@ -53,7 +54,10 @@ class Notification extends Facade { /** * Replace the bound instance with a fake. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fake(): NotificationFake { return tap(new NotificationFake, function ($fake) { diff --git a/src/support/src/Facades/Process.php b/src/support/src/Facades/Process.php index 5844c80ef2..d00313eb04 100644 --- a/src/support/src/Facades/Process.php +++ b/src/support/src/Facades/Process.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Process\Factory; +use Hypervel\Support\Attributes\TestsOnly; /** * @method static \Hypervel\Process\PendingProcess command(array|string $command) @@ -65,8 +66,9 @@ protected static function getFacadeAccessor(): string /** * Indicate that the process factory should fake processes. * - * Tests only. Fake handlers and recordings persist on the container-resolved factory for the worker lifetime and affect later calls. + * Fake handlers and recordings persist on the container-resolved factory for the worker lifetime and affect later calls. */ + #[TestsOnly] public static function fake(array|Closure|null $callback = null): Factory { return tap(static::getFacadeRoot(), function ($fake) use ($callback) { diff --git a/src/support/src/Facades/Queue.php b/src/support/src/Facades/Queue.php index 5ac2497a60..84f89ad096 100644 --- a/src/support/src/Facades/Queue.php +++ b/src/support/src/Facades/Queue.php @@ -6,6 +6,8 @@ use Hypervel\Contracts\Container\Container as ContainerContract; use Hypervel\Queue\Worker; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Testing\Fakes\QueueFake; /** @@ -113,15 +115,22 @@ class Queue extends Facade { /** * Register a callback to be executed to pick jobs. + * + * The callback persists for the worker lifetime and runs on every job pop + * for the named worker. Passing null removes the callback. */ - public static function popUsing(string $workerName, callable $callback): void + #[BootOnly] + public static function popUsing(string $workerName, ?callable $callback): void { Worker::popUsing($workerName, $callback); } /** * Replace the bound instance with a fake. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fake(array|string $jobsToFake = []): QueueFake { $actualQueueManager = static::isFake() @@ -141,7 +150,10 @@ public static function fake(array|string $jobsToFake = []): QueueFake /** * Replace the bound instance with a fake that fakes all jobs except the given jobs. + * + * Swaps the resolved facade instance for the worker lifetime. */ + #[TestsOnly] public static function fakeExcept(array|string $jobsToAllow): QueueFake { return static::fake()->except($jobsToAllow); @@ -149,7 +161,10 @@ public static function fakeExcept(array|string $jobsToAllow): QueueFake /** * Replace the bound instance with a fake during the given callable's execution. + * + * Swaps the resolved facade instance worker-wide until the callable returns. */ + #[TestsOnly] public static function fakeFor(callable $callable, array $jobsToFake = []): mixed { $originalQueueManager = static::getFacadeRoot(); @@ -169,7 +184,10 @@ public static function fakeFor(callable $callable, array $jobsToFake = []): mixe /** * Replace the bound instance with a fake during the given callable's execution. + * + * Swaps the resolved facade instance worker-wide until the callable returns. */ + #[TestsOnly] public static function fakeExceptFor(callable $callable, array $jobsToAllow = []): mixed { $originalQueueManager = static::getFacadeRoot(); diff --git a/src/support/src/Facades/Storage.php b/src/support/src/Facades/Storage.php index d0a84111f2..1d25756854 100644 --- a/src/support/src/Facades/Storage.php +++ b/src/support/src/Facades/Storage.php @@ -7,6 +7,7 @@ use Hypervel\Contracts\Container\Container as ContainerContract; use Hypervel\Filesystem\Filesystem; use Hypervel\Filesystem\LocalFilesystemAdapter; +use Hypervel\Support\Attributes\TestsOnly; use UnitEnum; use function Hypervel\Support\enum_value; @@ -110,9 +111,10 @@ class Storage extends Facade /** * Replace the given disk with a local testing disk. * - * Tests only. Swaps a cached filesystem disk on the shared filesystem - * manager for the worker lifetime. + * Swaps a cached filesystem disk on the shared filesystem manager for + * the worker lifetime. */ + #[TestsOnly] public static function fake(UnitEnum|string|null $disk = null, array $config = []): LocalFilesystemAdapter { if ($disk instanceof UnitEnum) { @@ -151,9 +153,10 @@ public static function fake(UnitEnum|string|null $disk = null, array $config = [ /** * Replace the given disk with a persistent local testing disk. * - * Tests only. Swaps a cached filesystem disk on the shared filesystem - * manager for the worker lifetime. + * Swaps a cached filesystem disk on the shared filesystem manager for + * the worker lifetime. */ + #[TestsOnly] public static function persistentFake(UnitEnum|string|null $disk = null, array $config = []): LocalFilesystemAdapter { if ($disk instanceof UnitEnum) { diff --git a/src/support/src/Lottery.php b/src/support/src/Lottery.php index a653d59ed4..2e81ac5682 100644 --- a/src/support/src/Lottery.php +++ b/src/support/src/Lottery.php @@ -4,6 +4,7 @@ namespace Hypervel\Support; +use Hypervel\Support\Attributes\TestsOnly; use RuntimeException; class Lottery @@ -152,9 +153,10 @@ protected static function resultFactory(): callable /** * Force the lottery to always result in a win. * - * Tests only. Without a callback, the result factory persists in static - * state for the worker lifetime and affects every subsequent lottery. + * Without a callback, the result factory persists in static state for + * the worker lifetime and affects every subsequent lottery. */ + #[TestsOnly] public static function alwaysWin(?callable $callback = null): void { self::setResultFactory(fn () => true); @@ -173,9 +175,10 @@ public static function alwaysWin(?callable $callback = null): void /** * Force the lottery to always result in a loss. * - * Tests only. Without a callback, the result factory persists in static - * state for the worker lifetime and affects every subsequent lottery. + * Without a callback, the result factory persists in static state for + * the worker lifetime and affects every subsequent lottery. */ + #[TestsOnly] public static function alwaysLose(?callable $callback = null): void { self::setResultFactory(fn () => false); @@ -194,9 +197,10 @@ public static function alwaysLose(?callable $callback = null): void /** * Set the sequence that will be used to determine lottery results. * - * Tests only. The sequence factory persists in a static property for the - * worker lifetime and affects every subsequent lottery result. + * The sequence factory persists in a static property for the worker + * lifetime and affects every subsequent lottery result. */ + #[TestsOnly] public static function fix(array $sequence, ?callable $whenMissing = null): void { static::forceResultWithSequence($sequence, $whenMissing); @@ -205,9 +209,10 @@ public static function fix(array $sequence, ?callable $whenMissing = null): void /** * Set the sequence that will be used to determine lottery results. * - * Tests only. The sequence factory persists in a static property for the - * worker lifetime and affects every subsequent lottery result. + * The sequence factory persists in a static property for the worker + * lifetime and affects every subsequent lottery result. */ + #[TestsOnly] public static function forceResultWithSequence(array $sequence, ?callable $whenMissing = null): void { $next = 0; @@ -239,7 +244,11 @@ public static function forceResultWithSequence(array $sequence, ?callable $whenM /** * Indicate that the lottery results should be determined normally. + * + * Clears the worker-wide result factory; concurrent coroutines may observe + * different lottery behavior depending on timing. */ + #[TestsOnly] public static function determineResultsNormally(): void { static::determineResultNormally(); @@ -248,9 +257,10 @@ public static function determineResultsNormally(): void /** * Indicate that the lottery results should be determined normally. * - * Tests only. Clears the worker-wide result factory; concurrent coroutines - * may observe different lottery behavior depending on timing. + * Clears the worker-wide result factory; concurrent coroutines may observe + * different lottery behavior depending on timing. */ + #[TestsOnly] public static function determineResultNormally(): void { static::$resultFactory = null; @@ -259,9 +269,10 @@ public static function determineResultNormally(): void /** * Set the factory that should be used to determine the lottery results. * - * Tests only. The factory persists in a static property for the worker - * lifetime and affects every subsequent lottery result. + * The factory persists in a static property for the worker lifetime + * and affects every subsequent lottery result. */ + #[TestsOnly] public static function setResultFactory(callable $factory): void { self::$resultFactory = $factory; diff --git a/src/support/src/Manager.php b/src/support/src/Manager.php index 3d76fc4655..6282a0cf57 100644 --- a/src/support/src/Manager.php +++ b/src/support/src/Manager.php @@ -7,6 +7,9 @@ use Closure; use Hypervel\Config\Repository; use Hypervel\Contracts\Container\Container; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; use ReflectionException; use RuntimeException; @@ -129,11 +132,12 @@ protected function callCustomCreator(string $driver): mixed * Anonymous closures run in this manager's class scope; non-static closures * also receive the manager as $this. * - * Boot-only. The callback persists in the singleton's customCreators array - * for the worker lifetime and applies to every subsequent driver resolution. + * The callback persists in the singleton's customCreators array for the + * worker lifetime and applies to every subsequent driver resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $driver, Closure $callback): static { try { @@ -169,12 +173,13 @@ public function getContainer(): Container /** * Set the container instance used by the manager. * - * Tests only. Swaps the singleton's container and configuration references; - * per-request use races across coroutines and breaks every concurrent driver - * resolution through this manager. + * Swaps the singleton's container and configuration references; per-request + * use races across coroutines and breaks every concurrent driver resolution + * through this manager. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; @@ -186,12 +191,12 @@ public function setContainer(Container $container): static /** * Forget all of the resolved driver instances. * - * Boot or tests only. Clears the singleton's driver cache; concurrent - * coroutines may already hold references that next resolution will not - * share. + * Clears the singleton's driver cache; concurrent coroutines may + * already hold references that next resolution will not share. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetDrivers(): static { $this->drivers = []; diff --git a/src/support/src/MultipleInstanceManager.php b/src/support/src/MultipleInstanceManager.php index c56ef25d18..679380e2e8 100644 --- a/src/support/src/MultipleInstanceManager.php +++ b/src/support/src/MultipleInstanceManager.php @@ -7,6 +7,9 @@ use Closure; use Hypervel\Config\Repository; use Hypervel\Contracts\Foundation\Application; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; use ReflectionException; use RuntimeException; @@ -52,9 +55,10 @@ abstract public function getDefaultInstance(): string; /** * Set the default instance name. * - * Boot-only. Implementations typically mutate process-global config; per-request - * use races across coroutines. Use coroutine Context for request-scoped overrides. + * Implementations typically mutate process-global config; per-request use races + * across coroutines. Use coroutine Context for request-scoped overrides. */ + #[BootOnly] abstract public function setDefaultInstance(string $name): void; /** @@ -128,12 +132,13 @@ protected function callCustomCreator(array $config): mixed /** * Unset the given instances. * - * Boot or tests only. Mutates the singleton's instance cache; concurrent - * coroutines may already hold a reference to the forgotten instance and - * next resolution will rebuild. + * Mutates the singleton's instance cache; concurrent coroutines may + * already hold a reference to the forgotten instance and next resolution + * will rebuild. * * @return $this */ + #[BootOrTestsOnly(whenShared: true)] public function forgetInstance(array|string|null $name = null): static { $name ??= $this->getDefaultInstance(); @@ -150,10 +155,11 @@ public function forgetInstance(array|string|null $name = null): static /** * Disconnect the given instance and remove from local cache. * - * Boot or tests only. Mutates the singleton's instance cache; concurrent - * coroutines may already hold a reference to the instance and next - * resolution will rebuild. + * Mutates the singleton's instance cache; concurrent coroutines may + * already hold a reference to the instance and next resolution will + * rebuild. */ + #[BootOrTestsOnly(whenShared: true)] public function purge(?string $name = null): void { $name ??= $this->getDefaultInstance(); @@ -167,12 +173,12 @@ public function purge(?string $name = null): void * Anonymous closures run in this manager's class scope; non-static closures * also receive the manager as $this. * - * Boot-only. The callback persists in the singleton's customCreators array - * for the worker lifetime and applies to every subsequent instance - * resolution. + * The callback persists in the singleton's customCreators array for the + * worker lifetime and applies to every subsequent instance resolution. * * @return $this */ + #[BootOnly(whenShared: true)] public function extend(string $name, Closure $callback): static { try { @@ -190,12 +196,13 @@ public function extend(string $name, Closure $callback): static /** * Set the application instance used by the manager. * - * Tests only. Swaps the singleton's application and configuration references - * without rebuilding resolved instances; per-request use races across - * coroutines and breaks every concurrent resolution through this manager. + * Swaps the singleton's application and configuration references without + * rebuilding resolved instances; per-request use races across coroutines and + * breaks every concurrent resolution through this manager. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setApplication(Application $app): static { $this->app = $app; diff --git a/src/support/src/Pluralizer.php b/src/support/src/Pluralizer.php index 37b9303340..919beccd8f 100644 --- a/src/support/src/Pluralizer.php +++ b/src/support/src/Pluralizer.php @@ -7,6 +7,7 @@ use Countable; use Doctrine\Inflector\Inflector; use Doctrine\Inflector\InflectorFactory; +use Hypervel\Support\Attributes\BootOnly; class Pluralizer { @@ -104,10 +105,11 @@ public static function inflector(): Inflector /** * Specify the language that should be used by the inflector. * - * Boot-only. The language and inflector instance persist in static - * properties for the worker lifetime and are used by every Str::plural() / - * Str::singular() call across all coroutines. + * The language and inflector instance persist in static properties for the + * worker lifetime and are used by every Str::plural() / Str::singular() + * call across all coroutines. */ + #[BootOnly] public static function useLanguage(string $language): void { static::$language = $language; diff --git a/src/support/src/ServiceProvider.php b/src/support/src/ServiceProvider.php index e5a41fde90..98553d13d8 100644 --- a/src/support/src/ServiceProvider.php +++ b/src/support/src/ServiceProvider.php @@ -16,6 +16,7 @@ use Hypervel\Di\ClassMap\ClassMapManager; use Hypervel\Filesystem\Filesystem; use Hypervel\Foundation\Configuration\ConfigMutationTracker; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\View\Compilers\CompilerInterface; use RuntimeException; @@ -149,7 +150,10 @@ public function callBootedCallbacks(): void * Keys declared in mergeableOptions() get an additional one-level-deeper * merge so the app can add entries to collection arrays (stores, connections, * guards, etc.) without losing the package's default entries. + * + * Called later, the merge changes configuration every request in the worker shares. */ + #[BootOnly] protected function mergeConfigFrom(string $path, string $key): void { $mergeableOptions = $this->mergeableOptions($key); @@ -189,7 +193,10 @@ protected function mergeableOptions(string $name): array /** * Replace the given configuration with the existing configuration recursively. + * + * Called later, the change reaches configuration every request in the worker shares. */ + #[BootOnly] protected function replaceConfigRecursivelyFrom(string $path, string $key): void { $this->configureUsing(static function (Repository $config) use ($path, $key): void { @@ -208,10 +215,11 @@ protected function replaceConfigRecursivelyFrom(string $path, string $key): void * the rebuilt configuration rather than its first result kept. Cached * configuration already holds the result, so the callback does not run. * - * Boot-only. Called later, the change reaches configuration every request in the worker shares, and is not replayed. + * Called later, the change reaches configuration every request in the worker shares, and is not replayed. * * @param Closure(Repository): void $callback */ + #[BootOnly] protected function configureUsing(Closure $callback): void { if ($this->app instanceof CachesConfiguration && $this->app->configurationIsCached()) { @@ -568,7 +576,10 @@ protected function getProviderKey(?string $key = null): string /** * Register the package's custom Artisan commands. + * + * The commands are added to every later console application the worker creates. */ + #[BootOnly] public function commands(array $commands): void { Artisan::starting(function ($artisan) use ($commands) { @@ -585,6 +596,7 @@ public function commands(array $commands): void * * @param array|string $aspects */ + #[BootOnly] protected function aspects(string|array $aspects): void { $aspects = is_array($aspects) ? $aspects : func_get_args(); @@ -603,6 +615,7 @@ protected function aspects(string|array $aspects): void * * @param array $map originalClass => replacementFilePath */ + #[BootOnly] protected function classMap(array $map): void { ClassMapManager::add($map); diff --git a/src/support/src/Sleep.php b/src/support/src/Sleep.php index e7ee5e64ab..8979e113ca 100644 --- a/src/support/src/Sleep.php +++ b/src/support/src/Sleep.php @@ -8,6 +8,7 @@ use Closure; use DateInterval; use DateTimeInterface; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Facades\Date; use Hypervel\Support\Traits\Macroable; use PHPUnit\Framework\Assert as PHPUnit; @@ -349,9 +350,10 @@ protected function pullPending(): float|int /** * Stay awake and capture any attempts to sleep. * - * Tests only. Sleep fakes persist in static state for the worker lifetime - * and affect every subsequent sleep call. + * Sleep fakes persist in static state for the worker lifetime and affect + * every subsequent sleep call. */ + #[TestsOnly] public static function fake(bool $value = true, bool $syncWithCarbon = false): void { static::$fake = $value; @@ -485,9 +487,10 @@ public function unless(bool|Closure $condition): static /** * Specify a callback that should be invoked when faking sleep within a test. * - * Tests only. The callback persists in static state for the worker lifetime - * and runs for every subsequent faked sleep. + * The callback persists in static state for the worker lifetime and runs + * for every subsequent faked sleep. */ + #[TestsOnly] public static function whenFakingSleep(callable $callback): void { static::$fakeSleepCallbacks[] = $callback; @@ -496,9 +499,10 @@ public static function whenFakingSleep(callable $callback): void /** * Indicate that Carbon's "now" should be kept in sync when sleeping. * - * Tests only. The flag persists in static state for the worker lifetime and - * affects every subsequent faked sleep. + * The flag persists in static state for the worker lifetime and affects + * every subsequent faked sleep. */ + #[TestsOnly] public static function syncWithCarbon(bool $value = true): void { static::$syncWithCarbon = $value; diff --git a/src/support/src/Str.php b/src/support/src/Str.php index ce246cde24..f64374489f 100644 --- a/src/support/src/Str.php +++ b/src/support/src/Str.php @@ -8,6 +8,8 @@ use Countable; use DateTimeInterface; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\TestsOnly; +use Hypervel\Support\Attributes\Unsupported; use Hypervel\Support\Traits\Macroable; use InvalidArgumentException; use League\CommonMark\Environment\Environment; @@ -1003,11 +1005,12 @@ public static function random(int $length = 16): string /** * Set the callable that will be used to generate random strings. * - * Tests only. The factory persists in a static property for the worker - * lifetime and affects every subsequent random string generation. + * The factory persists in a static property for the worker lifetime + * and affects every subsequent random string generation. * * @param null|(callable(int): string) $factory */ + #[TestsOnly] public static function createRandomStringsUsing(?callable $factory = null): void { static::$randomStringFactory = $factory === null ? null : $factory(...); @@ -1016,12 +1019,13 @@ public static function createRandomStringsUsing(?callable $factory = null): void /** * Set the sequence that will be used to generate random strings. * - * Tests only. The sequence factory persists in a static property for the - * worker lifetime and affects every subsequent random string generation. + * The sequence factory persists in a static property for the worker + * lifetime and affects every subsequent random string generation. * * @param string[] $sequence * @param null|(callable(int): string) $whenMissing */ + #[TestsOnly] public static function createRandomStringsUsingSequence(array $sequence, ?callable $whenMissing = null): void { $next = 0; @@ -1054,10 +1058,10 @@ public static function createRandomStringsUsingSequence(array $sequence, ?callab /** * Indicate that random strings should be created normally and not using a custom factory. * - * Tests only. Clears the worker-wide random string factory; concurrent - * coroutines may observe different random string behavior depending on - * timing. + * Clears the worker-wide random string factory; concurrent coroutines + * may observe different random string behavior depending on timing. */ + #[TestsOnly] public static function createRandomStringsNormally(): void { static::$randomStringFactory = null; @@ -1897,11 +1901,12 @@ protected static function factoryUuid(): Uuid /** * Set the callable that will be used to generate UUIDs. * - * Tests only. The factory persists in a static property for the worker - * lifetime and affects every subsequent UUID generation. + * The factory persists in a static property for the worker lifetime + * and affects every subsequent UUID generation. * * @param null|(callable(): (string|Uuid)) $factory */ + #[TestsOnly] public static function createUuidsUsing(?callable $factory = null): void { static::$uuidFactory = $factory === null ? null : $factory(...); @@ -1910,12 +1915,13 @@ public static function createUuidsUsing(?callable $factory = null): void /** * Set the sequence that will be used to generate UUIDs. * - * Tests only. The sequence factory persists in a static property for the - * worker lifetime and affects every subsequent UUID generation. + * The sequence factory persists in a static property for the worker + * lifetime and affects every subsequent UUID generation. * * @param array $sequence * @param null|(callable(): (string|Uuid)) $whenMissing */ + #[TestsOnly] public static function createUuidsUsingSequence(array $sequence, ?callable $whenMissing = null): void { $next = 0; @@ -1948,12 +1954,12 @@ public static function createUuidsUsingSequence(array $sequence, ?callable $when /** * Always return the same UUID when generating new UUIDs. * - * Tests only unless a callback is supplied. Without a callback, the factory - * persists in a static property for the worker lifetime and affects every - * subsequent UUID generation. + * The factory persists in a static property and affects every UUID the + * worker generates, until the callback returns when one is supplied. * * @param null|(Closure(Uuid): mixed) $callback */ + #[TestsOnly] public static function freezeUuids(?Closure $callback = null): Uuid { $uuid = Str::uuid(); @@ -1974,9 +1980,10 @@ public static function freezeUuids(?Closure $callback = null): Uuid /** * Indicate that UUIDs should be created normally and not using a custom factory. * - * Tests only. Clears the worker-wide UUID factory; concurrent coroutines - * may observe different UUID behavior depending on timing. + * Clears the worker-wide UUID factory; concurrent coroutines may observe + * different UUID behavior depending on timing. */ + #[TestsOnly] public static function createUuidsNormally(): void { static::$uuidFactory = null; @@ -2001,9 +2008,10 @@ public static function ulid(?DateTimeInterface $time = null): Ulid /** * Indicate that ULIDs should be created normally and not using a custom factory. * - * Tests only. Clears the worker-wide ULID factory; concurrent coroutines - * may observe different ULID behavior depending on timing. + * Clears the worker-wide ULID factory; concurrent coroutines may observe + * different ULID behavior depending on timing. */ + #[TestsOnly] public static function createUlidsNormally(): void { static::$ulidFactory = null; @@ -2012,11 +2020,12 @@ public static function createUlidsNormally(): void /** * Set the callable that will be used to generate ULIDs. * - * Tests only. The factory persists in a static property for the worker - * lifetime and affects every subsequent ULID generation. + * The factory persists in a static property for the worker lifetime + * and affects every subsequent ULID generation. * * @param null|(callable(): Ulid) $factory */ + #[TestsOnly] public static function createUlidsUsing(?callable $factory = null): void { static::$ulidFactory = $factory === null ? null : $factory(...); @@ -2025,12 +2034,13 @@ public static function createUlidsUsing(?callable $factory = null): void /** * Set the sequence that will be used to generate ULIDs. * - * Tests only. The sequence factory persists in a static property for the - * worker lifetime and affects every subsequent ULID generation. + * The sequence factory persists in a static property for the worker + * lifetime and affects every subsequent ULID generation. * * @param Ulid[] $sequence * @param null|(callable(): Ulid) $whenMissing */ + #[TestsOnly] public static function createUlidsUsingSequence(array $sequence, ?callable $whenMissing = null): void { $next = 0; @@ -2063,12 +2073,12 @@ public static function createUlidsUsingSequence(array $sequence, ?callable $when /** * Always return the same ULID when generating new ULIDs. * - * Tests only unless a callback is supplied. Without a callback, the factory - * persists in a static property for the worker lifetime and affects every - * subsequent ULID generation. + * The factory persists in a static property and affects every ULID the + * worker generates, until the callback returns when one is supplied. * * @param null|(Closure(Ulid): mixed) $callback */ + #[TestsOnly] public static function freezeUlids(?Closure $callback = null): Ulid { $ulid = Str::ulid(); @@ -2089,6 +2099,7 @@ public static function freezeUlids(?Closure $callback = null): Ulid /** * Remove all strings from the casing caches. */ + #[Unsupported('Str keeps no casing caches; use StrCache for persistent casing caching.')] public static function flushCache(): void { throw new LogicException('Str::flushCache() is not implemented in Hypervel because Str casing caches are intentionally not used. Use StrCache for persistent casing caching.'); @@ -2097,9 +2108,10 @@ public static function flushCache(): void /** * Return all factory functions to their default state. * - * Tests only. Clears the worker-wide random string, UUID, and ULID - * factories, affecting generation in every coroutine. + * Clears the worker-wide random string, UUID, and ULID factories, + * affecting generation in every coroutine. */ + #[TestsOnly] public static function resetFactoryState(): void { static::createRandomStringsNormally(); diff --git a/src/support/src/Testing/Fakes/BusFake.php b/src/support/src/Testing/Fakes/BusFake.php index 4b0af6a83f..f7bf52e076 100644 --- a/src/support/src/Testing/Fakes/BusFake.php +++ b/src/support/src/Testing/Fakes/BusFake.php @@ -780,6 +780,8 @@ protected function getCommandRepresentation(mixed $command): mixed /** * Set the pipes commands should be piped through before dispatching. * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. + * * @return $this */ public function pipeThrough(array $pipes): static @@ -808,6 +810,8 @@ public function getCommandHandler(mixed $command): mixed /** * Map a command to a handler. * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. + * * @return $this */ public function map(array $map): static @@ -820,7 +824,7 @@ public function map(array $map): static /** * Allow dispatching after responses. * - * Tests only. Changes the underlying dispatcher for all coroutines until the setting is restored. + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. * * @return $this */ @@ -834,7 +838,7 @@ public function withDispatchingAfterResponses(): static /** * Disable dispatching after responses. * - * Tests only. Changes the underlying dispatcher for all coroutines until the setting is restored. + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. * * @return $this */ diff --git a/src/support/src/Testing/Fakes/EventFake.php b/src/support/src/Testing/Fakes/EventFake.php index c5c739fcb3..dd59450a25 100644 --- a/src/support/src/Testing/Fakes/EventFake.php +++ b/src/support/src/Testing/Fakes/EventFake.php @@ -233,6 +233,8 @@ public function hasDispatched(string $event): bool /** * Register an event listener with the dispatcher. + * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. */ public function listen( array|Closure|QueuedClosure|string $events, @@ -243,6 +245,8 @@ public function listen( /** * Register a passive event observer with the dispatcher. + * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. */ public function observe(array|string $events, array|object|string $observer): void { @@ -294,6 +298,8 @@ public function push(string $event, mixed $payload = []): void /** * Register an event subscriber with the dispatcher. + * + * This changes the wrapped dispatcher, which is usually the application's worker-shared dispatcher. */ public function subscribe(object|string $subscriber): void { diff --git a/src/support/src/Traits/CapsuleManagerTrait.php b/src/support/src/Traits/CapsuleManagerTrait.php index 349477dcac..4fbc41bcbe 100644 --- a/src/support/src/Traits/CapsuleManagerTrait.php +++ b/src/support/src/Traits/CapsuleManagerTrait.php @@ -6,6 +6,7 @@ use Hypervel\Config\Repository; use Hypervel\Contracts\Container\Container; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait CapsuleManagerTrait { @@ -34,9 +35,10 @@ protected function setupContainer(Container $container): void /** * Make this capsule instance available globally. * - * Boot or tests only. Swaps the global Capsule instance used by static - * calls; request-time use races across coroutines. + * Swaps the global Capsule instance used by static calls; request-time + * use races across coroutines. */ + #[BootOrTestsOnly] public function setAsGlobal(): void { static::$instance = $this; @@ -53,9 +55,10 @@ public function getContainer(): Container /** * Set the IoC container instance. * - * Boot or tests only. Swaps the Capsule container used by this standalone - * manager; request-time use races across coroutines. + * Swaps the Capsule container used by this standalone manager; + * request-time use races across coroutines. */ + #[BootOrTestsOnly(whenShared: true)] public function setContainer(Container $container): void { $this->container = $container; diff --git a/src/support/src/Uri.php b/src/support/src/Uri.php index 1704e4c409..cf343a05f0 100644 --- a/src/support/src/Uri.php +++ b/src/support/src/Uri.php @@ -12,6 +12,7 @@ use Hypervel\Contracts\Support\Htmlable; use Hypervel\Contracts\Support\Responsable; use Hypervel\Http\RedirectResponse; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Traits\Conditionable; use Hypervel\Support\Traits\Dumpable; use Hypervel\Support\Traits\Macroable; @@ -425,9 +426,10 @@ public function dump(mixed ...$args): static /** * Set the URL generator resolver. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and is used by every subsequent URI route/action helper. + * The resolver persists in a static property for the worker lifetime + * and is used by every subsequent URI route/action helper. */ + #[BootOnly] public static function setUrlGeneratorResolver(Closure $urlGeneratorResolver): void { static::$urlGeneratorResolver = $urlGeneratorResolver; diff --git a/src/telescope/composer.json b/src/telescope/composer.json index 21626acc1c..220b42cc8c 100644 --- a/src/telescope/composer.json +++ b/src/telescope/composer.json @@ -26,6 +26,7 @@ "php": "^8.4", "ext-mbstring": "*", "ext-pdo": "*", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/bus": "^0.4", diff --git a/src/telescope/src/AuthorizesRequests.php b/src/telescope/src/AuthorizesRequests.php index 08b58666ec..302e5498a2 100644 --- a/src/telescope/src/AuthorizesRequests.php +++ b/src/telescope/src/AuthorizesRequests.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; trait AuthorizesRequests { @@ -17,10 +18,10 @@ trait AuthorizesRequests /** * Register the Telescope authentication callback. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every Telescope dashboard request across all - * coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every Telescope dashboard request across all coroutines. */ + #[BootOnly] public static function auth(?Closure $callback): static { static::$authUsing = $callback; diff --git a/src/telescope/src/Avatar.php b/src/telescope/src/Avatar.php index 7a41dc24ca..b875d86b2e 100644 --- a/src/telescope/src/Avatar.php +++ b/src/telescope/src/Avatar.php @@ -5,6 +5,7 @@ namespace Hypervel\Telescope; use Closure; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Str; class Avatar @@ -33,9 +34,10 @@ public static function url(array $user): ?string /** * Register the Telescope user avatar callback. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every Telescope avatar lookup. + * The callback persists in a static property for the worker lifetime + * and runs on every Telescope avatar lookup. */ + #[BootOnly] public static function register(Closure $callback): void { static::$callback = $callback; diff --git a/src/telescope/src/ListensForStorageOpportunities.php b/src/telescope/src/ListensForStorageOpportunities.php index 55858eebba..286fe92e0c 100644 --- a/src/telescope/src/ListensForStorageOpportunities.php +++ b/src/telescope/src/ListensForStorageOpportunities.php @@ -17,6 +17,7 @@ use Hypervel\Queue\Events\JobFailed; use Hypervel\Queue\Events\JobProcessed; use Hypervel\Queue\Events\JobProcessing; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Telescope\Contracts\EntriesRepository; trait ListensForStorageOpportunities @@ -33,9 +34,10 @@ trait ListensForStorageOpportunities /** * Register listeners that store the recorded Telescope entries. * - * Boot-only. Registers worker-lifetime event listeners; runtime use would - * accumulate duplicate listeners for every subsequent request/job. + * Registers worker-lifetime event listeners; runtime use would accumulate + * duplicate listeners for every subsequent request/job. */ + #[BootOnly] public static function listenForStorageOpportunities(Container $app): void { static::recordEntriesForRequests($app); @@ -46,9 +48,10 @@ public static function listenForStorageOpportunities(Container $app): void /** * Set the callback that determines if Telescope should start recording. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and is checked before every Telescope recording opportunity. + * The callback persists in a static property for the worker lifetime + * and is checked before every Telescope recording opportunity. */ + #[BootOnly] public static function shouldListenUsing(?Closure $callback): void { static::$shouldListenCallback = $callback; @@ -69,9 +72,10 @@ public static function shouldListen(): bool /** * Record the entries in queue before the request termination. * - * Boot-only. Registers a worker-lifetime request listener; runtime use - * would accumulate duplicate listeners. + * Registers a worker-lifetime request listener; runtime use would + * accumulate duplicate listeners. */ + #[BootOnly] public static function recordEntriesForRequests(Container $app): void { $app->make(Dispatcher::class) @@ -87,9 +91,10 @@ public static function recordEntriesForRequests(Container $app): void /** * Start recording for approved console commands and scheduled tasks. * - * Boot-only. Registers worker-lifetime command and scheduled-task listeners; - * runtime use would accumulate duplicate listeners. + * Registers worker-lifetime command and scheduled-task listeners; runtime + * use would accumulate duplicate listeners. */ + #[BootOnly] public static function manageRecordingStateForCommands(Container $app): void { $events = $app->make(Dispatcher::class); diff --git a/src/telescope/src/RegistersWatchers.php b/src/telescope/src/RegistersWatchers.php index 6cb26f226d..e0c1d1ff40 100644 --- a/src/telescope/src/RegistersWatchers.php +++ b/src/telescope/src/RegistersWatchers.php @@ -5,6 +5,7 @@ namespace Hypervel\Telescope; use Hypervel\Contracts\Foundation\Application; +use Hypervel\Support\Attributes\BootOrTestsOnly; trait RegistersWatchers { @@ -24,10 +25,10 @@ public static function hasWatcher(string $class): bool /** * Flush the registered watchers. * - * Boot or tests only. Clears the worker-wide watcher registry; runtime use - * can cause later watcher checks to diverge from already-registered - * listeners. + * Clears the worker-wide watcher registry; runtime use can cause later + * watcher checks to diverge from already-registered listeners. */ + #[BootOrTestsOnly] public static function flushWatchers(): void { static::$watchers = []; diff --git a/src/telescope/src/Telescope.php b/src/telescope/src/Telescope.php index 0ed6bc19be..745c64029c 100644 --- a/src/telescope/src/Telescope.php +++ b/src/telescope/src/Telescope.php @@ -15,6 +15,7 @@ use Hypervel\Http\Request; use Hypervel\Log\Events\MessageLogged; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Facades\Auth; use Hypervel\Support\HtmlString; @@ -152,7 +153,11 @@ class Telescope /** * Register the Telescope watchers and start recording if necessary. + * + * Registers worker-lifetime watchers, listeners and the mailable tag + * extractor; runtime use registers them again for later requests. */ + #[BootOnly] public static function start(Application $app): void { if (! config()->boolean('telescope.enabled')) { @@ -599,9 +604,10 @@ public static function catch(Throwable $e, array $tags = []): void /** * Set the callback that filters the entries that should be recorded. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every Telescope entry across all coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every Telescope entry across all coroutines. */ + #[BootOnly] public static function filter(Closure $callback): static { static::$filterUsing[] = $callback; @@ -612,9 +618,10 @@ public static function filter(Closure $callback): static /** * Set the callback that filters the batches that should be recorded. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every Telescope batch across all coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every Telescope batch across all coroutines. */ + #[BootOnly] public static function filterBatch(Closure $callback): static { static::$filterBatchUsing[] = $callback; @@ -625,9 +632,10 @@ public static function filterBatch(Closure $callback): static /** * Set the callback that will be executed after an entry is recorded in the queue. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every entry recording across all coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every entry recording across all coroutines. */ + #[BootOnly] public static function afterRecording(Closure $callback): static { static::$afterRecordingHook = $callback; @@ -638,9 +646,10 @@ public static function afterRecording(Closure $callback): static /** * Add a callback that will be executed after an entry is stored. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs after every batch store across all coroutines. + * The callback persists in a static property for the worker lifetime + * and runs after every batch store across all coroutines. */ + #[BootOnly] public static function afterStoring(Closure $callback): static { static::$afterStoringHooks[] = $callback; @@ -651,9 +660,10 @@ public static function afterStoring(Closure $callback): static /** * Add a callback that adds tags to the record. * - * Boot-only. The callback persists in a static property for the worker - * lifetime and runs on every tag computation across all coroutines. + * The callback persists in a static property for the worker lifetime + * and runs on every tag computation across all coroutines. */ + #[BootOnly] public static function tag(Closure $callback): static { static::$tagUsing[] = $callback; @@ -760,9 +770,10 @@ protected static function getBatchId(): string /** * Hide the given request header. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every recorded request across all coroutines. + * The list persists in a static property for the worker lifetime and + * applies to every recorded request across all coroutines. */ + #[BootOnly] public static function hideRequestHeaders(array $headers): static { static::$hiddenRequestHeaders = array_values(array_unique(array_merge( @@ -776,9 +787,10 @@ public static function hideRequestHeaders(array $headers): static /** * Hide the given request parameters. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every recorded request across all coroutines. + * The list persists in a static property for the worker lifetime and + * applies to every recorded request across all coroutines. */ + #[BootOnly] public static function hideRequestParameters(array $attributes): static { static::$hiddenRequestParameters = array_merge( @@ -792,9 +804,10 @@ public static function hideRequestParameters(array $attributes): static /** * Hide the given response parameters. * - * Boot-only. The list persists in a static property for the worker lifetime - * and applies to every recorded response across all coroutines. + * The list persists in a static property for the worker lifetime and + * applies to every recorded response across all coroutines. */ + #[BootOnly] public static function hideResponseParameters(array $attributes): static { static::$hiddenResponseParameters = array_values(array_unique(array_merge( @@ -808,9 +821,10 @@ public static function hideResponseParameters(array $attributes): static /** * Specifies that Telescope should record events fired by Hypervel. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every framework-event filter across all coroutines. + * The flag persists in a static property for the worker lifetime and + * applies to every framework-event filter across all coroutines. */ + #[BootOnly] public static function recordFrameworkEvents(): static { static::$ignoreFrameworkEvents = false; @@ -821,9 +835,10 @@ public static function recordFrameworkEvents(): static /** * Specifies that Telescope should use the dark theme. * - * Boot-only. The flag persists in a static property for the worker lifetime - * and applies to every dashboard render. + * The flag persists in a static property for the worker lifetime and + * applies to every dashboard render. */ + #[BootOnly] public static function night(): static { static::$useDarkTheme = true; @@ -834,9 +849,10 @@ public static function night(): static /** * Register the Telescope user avatar callback. * - * Boot-only. The callback persists on the Avatar registry for the worker - * lifetime and runs on every avatar lookup across all coroutines. + * The callback persists on the Avatar registry for the worker lifetime + * and runs on every avatar lookup across all coroutines. */ + #[BootOnly] public static function avatar(Closure $callback): static { Avatar::register($callback); diff --git a/src/telescope/src/Watchers/ClientRequestWatcher.php b/src/telescope/src/Watchers/ClientRequestWatcher.php index 43f88d87bd..3d6f31a5d2 100644 --- a/src/telescope/src/Watchers/ClientRequestWatcher.php +++ b/src/telescope/src/Watchers/ClientRequestWatcher.php @@ -12,6 +12,7 @@ use Hypervel\Contracts\Foundation\Application; use Hypervel\Coroutine\Coroutine; use Hypervel\Http\Client\PendingRequest; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Json; use Hypervel\Support\Str; use Hypervel\Telescope\IncomingEntry; @@ -34,8 +35,9 @@ class ClientRequestWatcher extends Watcher /** * Register the watcher. * - * Boot-only. The middleware persists for the worker lifetime and affects every Guzzle client. + * The middleware persists for the worker lifetime and affects every Guzzle client. */ + #[BootOnly] public function register(Application $app): void { Client::pushGlobalMiddleware($this->recordRequests(...), self::MIDDLEWARE_NAME); diff --git a/src/telescope/src/Watchers/RedisWatcher.php b/src/telescope/src/Watchers/RedisWatcher.php index ebc5fda19b..333abb631d 100644 --- a/src/telescope/src/Watchers/RedisWatcher.php +++ b/src/telescope/src/Watchers/RedisWatcher.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Foundation\Application; use Hypervel\Redis\Events\CommandExecuted; use Hypervel\Redis\RedisManager; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Telescope\IncomingEntry; use Hypervel\Telescope\Telescope; @@ -34,9 +35,10 @@ public function register(Application $app): void /** * Enable Redis events. * - * Boot-only. Applies a worker-wide Redis event override, refreshes existing - * mismatched pools, and sets a static flag; runtime use races across coroutines. + * Applies a worker-wide Redis event override, refreshes existing mismatched + * pools, and sets a static flag; runtime use races across coroutines. */ + #[BootOnly] public static function enableRedisEvents(Application $app): void { $app->make(RedisManager::class)->enableEvents(); diff --git a/src/testing/composer.json b/src/testing/composer.json index c5c99141d0..670a081289 100644 --- a/src/testing/composer.json +++ b/src/testing/composer.json @@ -28,6 +28,7 @@ "ext-dom": "*", "ext-mbstring": "*", "composer-runtime-api": "^2.2", + "hypervel/attributes": "^0.4", "hypervel/auth": "^0.4", "hypervel/broadcasting": "^0.4", "hypervel/bus": "^0.4", diff --git a/src/testing/src/Concerns/RunsInParallel.php b/src/testing/src/Concerns/RunsInParallel.php index c813fb2c55..4356a20eca 100644 --- a/src/testing/src/Concerns/RunsInParallel.php +++ b/src/testing/src/Concerns/RunsInParallel.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Foundation\Application as ApplicationContract; use Hypervel\Coroutine\Coroutine; use Hypervel\Foundation\Application; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Facades\ParallelTesting; use Hypervel\Testing\ParallelConsoleOutput; @@ -65,9 +66,10 @@ public function __construct(Options $options, OutputInterface $output) /** * Set the application resolver callback. * - * Tests only. The resolver persists in a static property for the worker - * lifetime and affects every subsequent parallel test runner instance. + * The resolver persists in a static property for the worker lifetime + * and affects every subsequent parallel test runner instance. */ + #[TestsOnly] public static function resolveApplicationUsing(?Closure $resolver): void { static::$applicationResolver = $resolver; @@ -76,9 +78,10 @@ public static function resolveApplicationUsing(?Closure $resolver): void /** * Set the runner resolver callback. * - * Tests only. The resolver persists in a static property for the worker - * lifetime and affects every subsequent parallel test runner instance. + * The resolver persists in a static property for the worker lifetime + * and affects every subsequent parallel test runner instance. */ + #[TestsOnly] public static function resolveRunnerUsing(?Closure $resolver): void { static::$runnerResolver = $resolver; diff --git a/src/testing/src/PHPUnit/AfterEachTestCleanup.php b/src/testing/src/PHPUnit/AfterEachTestCleanup.php index 21ad3c35d9..87100aff17 100644 --- a/src/testing/src/PHPUnit/AfterEachTestCleanup.php +++ b/src/testing/src/PHPUnit/AfterEachTestCleanup.php @@ -6,6 +6,7 @@ use Closure; use Hypervel\Coroutine\Coroutine; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Throwable; /** @@ -27,9 +28,10 @@ class AfterEachTestCleanup /** * Register a callback to flush test state after every test. * - * Boot or tests only. The callback persists in static state for the - * PHPUnit worker lifetime and runs after every subsequent test. + * The callback persists in static state for the PHPUnit worker + * lifetime and runs after every subsequent test. */ + #[BootOrTestsOnly] public static function flushUsing(string $name, callable $callback): void { static::$callbacks[$name] = Closure::fromCallable($callback); @@ -61,9 +63,10 @@ public static function runCallbacks(): void /** * Forget a registered callback by name. * - * Boot or tests only. This removes one process-local cleanup registration - * from the current PHPUnit worker. + * This removes one process-local cleanup registration from the current + * PHPUnit worker. */ + #[BootOrTestsOnly] public static function forget(string $name): void { unset(static::$callbacks[$name]); @@ -72,10 +75,11 @@ public static function forget(string $name): void /** * Forget all registered callbacks. * - * Boot or tests only. This removes process-local cleanup registrations - * for the current PHPUnit worker, including callbacks discovered from - * app and package metadata. + * This removes process-local cleanup registrations for the current + * PHPUnit worker, including callbacks discovered from app and package + * metadata. */ + #[BootOrTestsOnly] public static function forgetCallbacks(): void { static::$callbacks = []; diff --git a/src/translation/composer.json b/src/translation/composer.json index 2500f648f2..8392f8eb15 100644 --- a/src/translation/composer.json +++ b/src/translation/composer.json @@ -25,6 +25,7 @@ }, "require": { "php": "^8.4", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/context": "^0.4", "hypervel/contracts": "^0.4", diff --git a/src/translation/src/FileLoader.php b/src/translation/src/FileLoader.php index b780e6cb36..33939a8fb0 100644 --- a/src/translation/src/FileLoader.php +++ b/src/translation/src/FileLoader.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Translation\Loader; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use InvalidArgumentException; @@ -147,7 +148,11 @@ protected function loadJsonPaths(string $locale): array /** * Add a new namespace to the loader. + * + * On the shared loader, the namespace affects every subsequent translation + * lookup in the worker. */ + #[BootOnly(whenShared: true)] public function addNamespace(string $namespace, string $hint): void { $this->hints[$namespace] = $hint; @@ -165,7 +170,11 @@ public function namespaces(): array /** * Add a new path to the loader. + * + * On the shared loader, the path affects every subsequent translation lookup + * in the worker. */ + #[BootOnly(whenShared: true)] public function addPath(string $path): void { $this->paths[] = $path; @@ -173,7 +182,11 @@ public function addPath(string $path): void /** * Add a new JSON path to the loader. + * + * On the shared loader, the path affects every subsequent translation lookup + * in the worker. */ + #[BootOnly(whenShared: true)] public function addJsonPath(string $path): void { $this->jsonPaths[] = $path; diff --git a/src/translation/src/Translator.php b/src/translation/src/Translator.php index 23e7790f54..5b6008fb6f 100644 --- a/src/translation/src/Translator.php +++ b/src/translation/src/Translator.php @@ -10,6 +10,7 @@ use Hypervel\Contracts\Translation\Loader; use Hypervel\Contracts\Translation\Translator as TranslatorContract; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\NamespacedItemResolver; use Hypervel\Support\Str; use Hypervel\Support\Traits\Macroable; @@ -363,9 +364,10 @@ protected function makeReplacements(array|string $line, array $replace): array|s /** * Add translation lines to the given locale. * - * Boot-only. The lines are stored on the shared Translator instance and - * affect every subsequent lookup in the worker. + * The lines are stored on the shared Translator instance and affect + * every subsequent lookup in the worker. */ + #[BootOnly(whenShared: true)] public function addLines(array $lines, string $locale, string $namespace = '*'): void { foreach ($lines as $key => $value) { @@ -509,12 +511,13 @@ protected function withoutMissingKeyHandling(callable $callback): mixed * fallback flag. Choice lookups add the automatic count replacement only * after selecting the plural form. * - * Boot-only. The callback is stored on the shared Translator instance and - * affects every subsequent missing-key lookup in the worker. + * The callback is stored on the shared Translator instance and affects + * every subsequent missing-key lookup in the worker. * * @param null|callable(string, array, ?string, bool): ?string $callback * @return $this */ + #[BootOnly(whenShared: true)] public function handleMissingKeysUsing(?callable $callback): static { $this->missingTranslationKeyCallback = $callback; @@ -525,8 +528,8 @@ public function handleMissingKeysUsing(?callable $callback): static /** * Add a new namespace to the loader. * - * Boot-only. The namespace is added to the shared loader and affects every - * subsequent translation lookup in the worker. + * When the loader is the application's worker-shared loader, the namespace + * affects every subsequent translation lookup in the worker. */ public function addNamespace(string $namespace, string $hint): void { @@ -536,8 +539,8 @@ public function addNamespace(string $namespace, string $hint): void /** * Add a new path to the loader. * - * Boot-only. The path is added to the shared loader and affects every - * subsequent translation lookup in the worker. + * When the loader is the application's worker-shared loader, the path + * affects every subsequent translation lookup in the worker. */ public function addPath(string $path): void { @@ -547,8 +550,8 @@ public function addPath(string $path): void /** * Add a new JSON path to the loader. * - * Boot-only. The path is added to the shared loader and affects every - * subsequent translation lookup in the worker. + * When the loader is the application's worker-shared loader, the path + * affects every subsequent translation lookup in the worker. */ public function addJsonPath(string $path): void { @@ -584,9 +587,10 @@ protected function localeArray(?string $locale): array /** * Specify a callback that should be invoked to determine the applicable locale array. * - * Boot-only. The callback is stored on the shared Translator instance and - * affects every subsequent locale resolution in the worker. + * The callback is stored on the shared Translator instance and affects + * every subsequent locale resolution in the worker. */ + #[BootOnly(whenShared: true)] public function determineLocalesUsing(callable $callback): void { $this->determineLocalesUsing = $callback; @@ -607,9 +611,10 @@ public function getSelector(): MessageSelector /** * Set the message selector instance. * - * Boot-only. The selector is stored on the shared Translator instance and - * affects every subsequent choice() call in the worker. + * The selector is stored on the shared Translator instance and affects + * every subsequent choice() call in the worker. */ + #[BootOnly(whenShared: true)] public function setSelector(MessageSelector $selector): void { $this->selector = $selector; @@ -654,11 +659,12 @@ public function setLocale(string $locale): void /** * Set the base locale used by the translator. * - * Boot-only. The locale is shared by the worker's Translator instance and - * affects requests without a coroutine-local locale override. + * The locale is shared by the worker's Translator instance and affects + * requests without a coroutine-local locale override. * * @throws InvalidArgumentException */ + #[BootOnly(whenShared: true)] public function setBaseLocale(string $locale): void { $this->ensureValidLocale($locale); @@ -690,10 +696,11 @@ public function getFallback(): string /** * Set the fallback locale being used. * - * Boot-only. The fallback is shared by the worker's Translator instance and - * affects every subsequent translation lookup in that worker. Use - * setLocale() for a current-request locale override. + * The fallback is shared by the worker's Translator instance and affects + * every subsequent translation lookup in that worker. Use setLocale() for a + * current-request locale override. */ + #[BootOnly(whenShared: true)] public function setFallback(string $fallback): void { $this->ensureValidLocale($fallback); @@ -704,9 +711,10 @@ public function setFallback(string $fallback): void /** * Set the loaded translation groups. * - * Boot-only. The groups are stored on the shared Translator instance and - * affect every subsequent translation lookup in the worker. + * The groups are stored on the shared Translator instance and affect + * every subsequent translation lookup in the worker. */ + #[BootOnly(whenShared: true)] public function setLoaded(array $loaded): void { $this->loaded = $loaded; @@ -720,11 +728,12 @@ public function setLoaded(array $loaded): void /** * Add a handler to be executed in order to format a given class to a string during translation replacements. * - * Boot-only. The handler is stored on the shared Translator instance and - * affects every subsequent replacement in the worker. + * The handler is stored on the shared Translator instance and affects + * every subsequent replacement in the worker. * * @param class-string|Closure $class */ + #[BootOnly(whenShared: true)] public function stringable(Closure|string $class, ?callable $handler = null): void { if ($class instanceof Closure) { diff --git a/src/validation/composer.json b/src/validation/composer.json index e805d2b4c8..b2ed9f3b85 100644 --- a/src/validation/composer.json +++ b/src/validation/composer.json @@ -29,6 +29,7 @@ "ext-mbstring": "*", "brick/math": "^1.0", "egulias/email-validator": "^4.0", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/console": "^0.4", diff --git a/src/validation/src/Factory.php b/src/validation/src/Factory.php index 849de82a4c..c4152046b8 100644 --- a/src/validation/src/Factory.php +++ b/src/validation/src/Factory.php @@ -8,6 +8,8 @@ use Hypervel\Contracts\Container\Container; use Hypervel\Contracts\Translation\Translator; use Hypervel\Contracts\Validation\Factory as FactoryContract; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Str; class Factory implements FactoryContract @@ -151,9 +153,10 @@ protected function addExtensions(Validator $validator): void /** * Register a custom validator extension. * - * Boot-only. Request-time registration persists for the worker lifetime and - * exposes the extension to validators built by every concurrent request. + * On the shared factory, the extension persists for the worker lifetime and + * applies to validators built by every concurrent request. */ + #[BootOnly(whenShared: true)] public function extend(string $rule, Closure|string $extension, ?string $message = null): void { $this->extensions[$rule] = $extension; @@ -166,9 +169,10 @@ public function extend(string $rule, Closure|string $extension, ?string $message /** * Register a custom implicit validator extension. * - * Boot-only. Request-time registration persists for the worker lifetime and - * exposes the extension to validators built by every concurrent request. + * On the shared factory, the extension persists for the worker lifetime and + * applies to validators built by every concurrent request. */ + #[BootOnly(whenShared: true)] public function extendImplicit(string $rule, Closure|string $extension, ?string $message = null): void { $this->implicitExtensions[$rule] = $extension; @@ -181,9 +185,10 @@ public function extendImplicit(string $rule, Closure|string $extension, ?string /** * Register a custom dependent validator extension. * - * Boot-only. Request-time registration persists for the worker lifetime and - * exposes the extension to validators built by every concurrent request. + * On the shared factory, the extension persists for the worker lifetime and + * applies to validators built by every concurrent request. */ + #[BootOnly(whenShared: true)] public function extendDependent(string $rule, Closure|string $extension, ?string $message = null): void { $this->dependentExtensions[$rule] = $extension; @@ -196,9 +201,10 @@ public function extendDependent(string $rule, Closure|string $extension, ?string /** * Register a custom validator message replacer. * - * Boot-only. Request-time registration persists for the worker lifetime and - * exposes the message replacer to validators built by every concurrent request. + * On the shared factory, the message replacer persists for the worker + * lifetime and applies to validators built by every concurrent request. */ + #[BootOnly(whenShared: true)] public function replacer(string $rule, Closure|string $replacer): void { $this->replacers[$rule] = $replacer; @@ -207,9 +213,10 @@ public function replacer(string $rule, Closure|string $replacer): void /** * Indicate that unvalidated array keys should be included in validated data when the parent array is validated. * - * Boot-only. Request-time mutation persists for the worker lifetime and can - * change validated-data shape for validators built by concurrent requests. + * On the shared factory, the setting persists for the worker lifetime and + * can change validated-data shape for validators built by concurrent requests. */ + #[BootOnly(whenShared: true)] public function includeUnvalidatedArrayKeys(): void { $this->excludeUnvalidatedArrayKeys = false; @@ -218,9 +225,10 @@ public function includeUnvalidatedArrayKeys(): void /** * Indicate that unvalidated array keys should be excluded from the validated data, even if the parent array was validated. * - * Boot-only. Request-time mutation persists for the worker lifetime and can - * change validated-data shape for validators built by concurrent requests. + * On the shared factory, the setting persists for the worker lifetime and + * can change validated-data shape for validators built by concurrent requests. */ + #[BootOnly(whenShared: true)] public function excludeUnvalidatedArrayKeys(): void { $this->excludeUnvalidatedArrayKeys = true; @@ -229,9 +237,10 @@ public function excludeUnvalidatedArrayKeys(): void /** * Fake the DNS lookups performed by validation rules so they always succeed. * - * Tests only. The setting persists for the worker lifetime; failing to reset - * it through Validator::flushState() can make later tests bypass real DNS validation. + * The setting persists for the worker lifetime; failing to reset it through + * Validator::flushState() can make later tests bypass real DNS validation. */ + #[TestsOnly] public function fakeDnsLookups(bool $value = true): void { Validator::fakeDnsLookups($value); @@ -240,9 +249,10 @@ public function fakeDnsLookups(bool $value = true): void /** * Set the Validator instance resolver. * - * Boot-only. Request-time mutation persists for the worker lifetime and can - * change the Validator class used by concurrent builds. + * On the shared factory, the resolver persists for the worker lifetime and + * can change the Validator class used by concurrent builds. */ + #[BootOnly(whenShared: true)] public function resolver(Closure $resolver): void { $this->resolver = $resolver; @@ -267,9 +277,10 @@ public function getPresenceVerifier(): ?PresenceVerifierInterface /** * Set the Presence Verifier implementation. * - * Boot-only. Request-time mutation persists for the worker lifetime and can - * route concurrent exists and unique checks through the wrong verifier. + * On the shared factory, the verifier persists for the worker lifetime and + * can route concurrent exists and unique checks through the wrong verifier. */ + #[BootOnly(whenShared: true)] public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier): void { $this->verifier = $presenceVerifier; @@ -286,12 +297,12 @@ public function getContainer(): ?Container /** * Set the container instance used by the validation factory. * - * Tests only. Swaps the container on the singleton validation Factory; - * per-request use races across coroutines and breaks every concurrent - * validator build. + * Swaps the container on the singleton validation Factory; per-request use + * races across coroutines and breaks every concurrent validator build. * * @return $this */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): static { $this->container = $container; diff --git a/src/validation/src/RulePlanCache.php b/src/validation/src/RulePlanCache.php index 3d8b9e0bed..d68643f9b1 100644 --- a/src/validation/src/RulePlanCache.php +++ b/src/validation/src/RulePlanCache.php @@ -5,6 +5,7 @@ namespace Hypervel\Validation; use Hypervel\Contracts\Validation\ParameterizedRule; +use Hypervel\Support\Attributes\TestsOnly; use InvalidArgumentException; /** @@ -91,11 +92,12 @@ public static function put(array $rules, AttributePlan $plan): void } /** - * Set the maximum cache size. Intended for tests. + * Set the maximum cache size. * - * Tests only. The cache size persists in a static property for the worker - * lifetime and affects every subsequent validation rule plan cache write. + * The cache size persists in a static property for the worker lifetime + * and affects every subsequent validation rule plan cache write. */ + #[TestsOnly] public static function setMaxSize(int $size): void { if ($size < 1) { diff --git a/src/validation/src/Rules/Email.php b/src/validation/src/Rules/Email.php index b2fd8bd8f2..361aeb20c6 100644 --- a/src/validation/src/Rules/Email.php +++ b/src/validation/src/Rules/Email.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Validation\Validator as ValidatorContract; use Hypervel\Contracts\Validation\ValidatorAwareRule; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Validator; use Hypervel\Support\Traits\Conditionable; use Hypervel\Support\Traits\Macroable; @@ -66,15 +67,15 @@ class Email implements Rule, DataAwareRule, ValidatorAwareRule * * If no arguments are passed, the default email rule configuration will be returned. * - * Boot-only when setting. The default callback persists in a static - * property for the worker lifetime and is used by every Email::default() - * call across all coroutines. + * A given callback persists in a static property for the worker lifetime + * and is used by every Email::default() call across all coroutines. * * @param null|callable|static $callback * @return ($callback is null ? static : null) * * @throws InvalidArgumentException */ + #[BootOnly(whenNotDefault: 'callback')] public static function defaults(mixed $callback = null): ?static { if (is_null($callback)) { diff --git a/src/validation/src/Rules/File.php b/src/validation/src/Rules/File.php index c77b2921ad..55ede001c4 100644 --- a/src/validation/src/Rules/File.php +++ b/src/validation/src/Rules/File.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Validation\Validator as ValidatorContract; use Hypervel\Contracts\Validation\ValidatorAwareRule; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Validator; use Hypervel\Support\Str; use Hypervel\Support\Traits\Conditionable; @@ -81,15 +82,15 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule * * If no arguments are passed, the default file rule configuration will be returned. * - * Boot-only when setting. The default callback persists in a static - * property for the worker lifetime and is used by every File::default() - * call across all coroutines. + * A given callback persists in a static property for the worker lifetime + * and is used by every File::default() call across all coroutines. * * @param null|callable|static $callback * @return ($callback is null ? static : null) * * @throws InvalidArgumentException */ + #[BootOnly(whenNotDefault: 'callback')] public static function defaults(mixed $callback = null): ?static { if (is_null($callback)) { diff --git a/src/validation/src/Rules/Password.php b/src/validation/src/Rules/Password.php index b224ad3968..4e96f27c96 100644 --- a/src/validation/src/Rules/Password.php +++ b/src/validation/src/Rules/Password.php @@ -14,6 +14,7 @@ use Hypervel\Contracts\Validation\Validator as ValidatorContract; use Hypervel\Contracts\Validation\ValidatorAwareRule; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Facades\Validator; use Hypervel\Support\Traits\Conditionable; use Hypervel\Validation\Rules\Concerns\ClonesCustomRules; @@ -117,15 +118,15 @@ public function __construct(int $min) * * If no arguments are passed, the default password rule configuration will be returned. * - * Boot-only when setting. The default callback persists in a static - * property for the worker lifetime and is used by every Password::default() - * call across all coroutines. + * A given callback persists in a static property for the worker lifetime + * and is used by every Password::default() call across all coroutines. * * @param null|callable|static $callback * @return ($callback is null ? static : null) * * @throws InvalidArgumentException */ + #[BootOnly(whenNotDefault: 'callback')] public static function defaults(mixed $callback = null): ?static { if (is_null($callback)) { diff --git a/src/validation/src/Validator.php b/src/validation/src/Validator.php index d3f64a6619..35520cc660 100644 --- a/src/validation/src/Validator.php +++ b/src/validation/src/Validator.php @@ -15,6 +15,7 @@ use Hypervel\Contracts\Validation\Validator as ValidatorContract; use Hypervel\Contracts\Validation\ValidatorAwareRule; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Collection; use Hypervel\Support\Fluent; use Hypervel\Support\MessageBag; @@ -2145,9 +2146,10 @@ protected static function decodeAttributeWithPlaceholder(string $attribute): str /** * Fake the DNS lookups performed by validation rules so they always succeed. * - * Tests only. The setting persists for the worker lifetime; failing to reset - * it through Validator::flushState() can make later tests bypass real DNS validation. + * The setting persists for the worker lifetime; failing to reset it through + * Validator::flushState() can make later tests bypass real DNS validation. */ + #[TestsOnly] public static function fakeDnsLookups(bool $value = true): void { static::$fakeDnsLookups = $value; diff --git a/src/view/composer.json b/src/view/composer.json index 5fd3b495f7..8cea02ac83 100644 --- a/src/view/composer.json +++ b/src/view/composer.json @@ -26,6 +26,7 @@ "php": "^8.4", "ext-filter": "*", "ext-tokenizer": "*", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/conditionable": "^0.4", "hypervel/container": "^0.4", diff --git a/src/view/src/Compilers/BladeCompiler.php b/src/view/src/Compilers/BladeCompiler.php index 2fa557ddd4..c15f275f61 100644 --- a/src/view/src/Compilers/BladeCompiler.php +++ b/src/view/src/Compilers/BladeCompiler.php @@ -11,6 +11,7 @@ use Hypervel\Contracts\View\Factory as ViewFactory; use Hypervel\Contracts\View\View; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Collection; use Hypervel\Support\Str; use Hypervel\Support\Stringable; @@ -752,9 +753,10 @@ protected function splitTopLevel(string $expression, int $limit): array /** * Register a custom Blade compiler. * - * Boot-only. The compiler persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. + * The compiler persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function extend(callable $compiler): void { $this->extensions[] = $compiler; @@ -771,10 +773,11 @@ public function getExtensions(): array /** * Register an "if" statement directive. * - * Boot-only. The condition and its generated directives persist on the - * singleton BladeCompiler for the worker lifetime and apply to every - * subsequent template compilation. + * The condition and its generated directives persist on the singleton + * BladeCompiler for the worker lifetime and apply to every subsequent + * template compilation. */ + #[BootOnly(whenShared: true)] public function if(string $name, callable $callback): void { $this->conditions[$name] = $callback; @@ -813,10 +816,11 @@ public function check(string $name, mixed ...$parameters): bool /** * Register a class-based component alias directive. * - * Boot-only. The alias persists in the singleton BladeCompiler's class - * component aliases for the worker lifetime and applies to every subsequent - * template compilation. + * The alias persists in the singleton BladeCompiler's class component + * aliases for the worker lifetime and applies to every subsequent template + * compilation. */ + #[BootOnly(whenShared: true)] public function component(string $alias, ?string $class = null, string $prefix = ''): void { // Alias-first only — Laravel's class-first swap is intentionally not ported. @@ -839,9 +843,10 @@ public function component(string $alias, ?string $class = null, string $prefix = /** * Register an array of class-based components. * - * Boot-only. Delegates to component() per entry; all aliases persist on the - * singleton BladeCompiler for the worker lifetime. + * Delegates to component() per entry; all aliases persist on the singleton + * BladeCompiler for the worker lifetime. */ + #[BootOnly(whenShared: true)] public function components(array $components, string $prefix = ''): void { foreach ($components as $key => $value) { @@ -864,10 +869,11 @@ public function getClassComponentAliases(): array /** * Register a new anonymous component path. * - * Boot-only. The path persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. - * Also registers a namespace on the singleton ViewFactory. + * The path persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. Also + * registers a namespace on the singleton ViewFactory. */ + #[BootOnly] public function anonymousComponentPath(string $path, ?string $prefix = null): void { $prefixHash = hash('xxh128', $prefix ?: $path); @@ -886,9 +892,10 @@ public function anonymousComponentPath(string $path, ?string $prefix = null): vo /** * Register an anonymous component namespace. * - * Boot-only. The namespace persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. + * The namespace persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function anonymousComponentNamespace(string $directory, ?string $prefix = null): void { $prefix ??= $directory; @@ -902,9 +909,10 @@ public function anonymousComponentNamespace(string $directory, ?string $prefix = /** * Register a class-based component namespace. * - * Boot-only. The namespace persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. + * The namespace persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function componentNamespace(string $namespace, string $prefix): void { $this->classComponentNamespaces[$prefix] = $namespace; @@ -937,9 +945,9 @@ public function getClassComponentNamespaces(): array /** * Register a component alias directive. * - * Boot-only. Registers a directive on the singleton BladeCompiler for the - * worker lifetime. + * Registers a directive on the singleton BladeCompiler for the worker lifetime. */ + #[BootOnly(whenShared: true)] public function aliasComponent(string $path, ?string $alias = null): void { $alias = $alias ?: array_last(explode('.', $path)); @@ -958,9 +966,9 @@ public function aliasComponent(string $path, ?string $alias = null): void /** * Register an include alias directive. * - * Boot-only. Registers a directive on the singleton BladeCompiler for the - * worker lifetime. + * Registers a directive on the singleton BladeCompiler for the worker lifetime. */ + #[BootOnly(whenShared: true)] public function include(string $path, ?string $alias = null): void { $this->aliasInclude($path, $alias); @@ -969,9 +977,9 @@ public function include(string $path, ?string $alias = null): void /** * Register an include alias directive. * - * Boot-only. Registers a directive on the singleton BladeCompiler for the - * worker lifetime. + * Registers a directive on the singleton BladeCompiler for the worker lifetime. */ + #[BootOnly(whenShared: true)] public function aliasInclude(string $path, ?string $alias = null): void { $alias = $alias ?: array_last(explode('.', $path)); @@ -986,11 +994,12 @@ public function aliasInclude(string $path, ?string $alias = null): void /** * Register a handler for custom directives, binding the handler to the compiler. * - * Boot-only. Delegates to directive() with bind=true; the handler persists - * on the singleton BladeCompiler for the worker lifetime. + * Delegates to directive() with bind=true; the handler persists on the + * singleton BladeCompiler for the worker lifetime. * * @throws InvalidArgumentException */ + #[BootOnly(whenShared: true)] public function bindDirective(string $name, callable $handler): void { $this->directive($name, $handler, bind: true); @@ -999,12 +1008,13 @@ public function bindDirective(string $name, callable $handler): void /** * Register a handler for custom directives. * - * Boot-only. The handler persists in the singleton BladeCompiler's + * The handler persists in the singleton BladeCompiler's * customDirectives array for the worker lifetime and applies to every * subsequent template compilation. * * @throws InvalidArgumentException */ + #[BootOnly(whenShared: true)] public function directive(string $name, callable $handler, bool $bind = false): void { if (! preg_match('/^\w+(?:::\w+)?$/x', $name)) { @@ -1025,11 +1035,12 @@ public function getCustomDirectives(): array /** * Indicate that the following callable should be used to prepare strings for compilation. * - * Boot-only. The callback persists on the singleton BladeCompiler for the - * worker lifetime and runs on every subsequent template compilation. + * The callback persists on the singleton BladeCompiler for the worker + * lifetime and runs on every subsequent template compilation. * * @return $this */ + #[BootOnly(whenShared: true)] public function prepareStringsForCompilationUsing(callable $callback): static { $this->prepareStringsForCompilationUsing[] = $callback; @@ -1040,9 +1051,10 @@ public function prepareStringsForCompilationUsing(callable $callback): static /** * Register a new precompiler. * - * Boot-only. The precompiler persists on the singleton BladeCompiler for - * the worker lifetime and runs on every subsequent template compilation. + * The precompiler persists on the singleton BladeCompiler for the worker + * lifetime and runs on every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function precompiler(callable $precompiler): void { $this->precompilers[] = $precompiler; @@ -1087,9 +1099,10 @@ public function usingEchoFormat(string $format, callable $callback): string /** * Set the echo format to be used by the compiler. * - * Boot-only. The format persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. + * The format persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function setEchoFormat(string $format): void { $this->echoFormat = $format; @@ -1106,9 +1119,10 @@ protected function getEchoFormat(): string /** * Set the "echo" format to double encode entities. * - * Boot-only. The format persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. + * The format persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function withDoubleEncoding(): void { $this->setEchoFormat('e(%s, true)'); @@ -1117,9 +1131,10 @@ public function withDoubleEncoding(): void /** * Set the "echo" format to not double encode entities. * - * Boot-only. The format persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. + * The format persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function withoutDoubleEncoding(): void { $this->setEchoFormat('e(%s, false)'); @@ -1128,9 +1143,10 @@ public function withoutDoubleEncoding(): void /** * Indicate that component tags should not be compiled. * - * Boot-only. The setting persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent template compilation. + * The setting persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent template compilation. */ + #[BootOnly(whenShared: true)] public function withoutComponentTags(): void { $this->compilesComponentTags = false; diff --git a/src/view/src/Compilers/Concerns/CompilesEchos.php b/src/view/src/Compilers/Concerns/CompilesEchos.php index 648ef688e9..ca07b941ed 100644 --- a/src/view/src/Compilers/Concerns/CompilesEchos.php +++ b/src/view/src/Compilers/Concerns/CompilesEchos.php @@ -5,6 +5,7 @@ namespace Hypervel\View\Compilers\Concerns; use Closure; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Stringable; trait CompilesEchos @@ -19,9 +20,10 @@ trait CompilesEchos /** * Add a handler to be executed before echoing a given class. * - * Boot-only. The handler persists on the singleton BladeCompiler for the - * worker lifetime and applies to every subsequent rendered echo. + * The handler persists on the singleton BladeCompiler for the worker + * lifetime and applies to every subsequent rendered echo. */ + #[BootOnly(whenShared: true)] public function stringable(Closure|string $class, ?callable $handler = null): void { if ($class instanceof Closure) { diff --git a/src/view/src/Component.php b/src/view/src/Component.php index ec8f836db2..fc00a4d3e1 100644 --- a/src/view/src/Component.php +++ b/src/view/src/Component.php @@ -11,6 +11,8 @@ use Hypervel\Contracts\View\Factory; use Hypervel\Contracts\View\View as ViewContract; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use Hypervel\Support\Collection; use ReflectionClass; use ReflectionMethod; @@ -397,9 +399,9 @@ public static function ignoredParameterNames(): array /** * Flush the component's cached state. * - * Boot or tests only when called directly. Clears the worker-wide reflection - * and view caches shared by every coroutine; next render rebuilds them. The - * view:clear command owns runtime invalidation. + * Clears the worker-wide reflection and view caches shared by every + * coroutine; next render rebuilds them. The view:clear command owns runtime + * invalidation. */ public static function flushCache(): void { @@ -423,9 +425,10 @@ public static function forgetBladeView(string $contents): void /** * Forget the component's factory instance. * - * Boot or tests only. Clears the worker-wide ViewFactory reference shared - * by every coroutine; next render re-resolves it from the container. + * Clears the worker-wide ViewFactory reference shared by every coroutine; + * next render re-resolves it from the container. */ + #[BootOrTestsOnly] public static function forgetFactory(): void { static::$factory = null; @@ -434,11 +437,12 @@ public static function forgetFactory(): void /** * Forget the component's resolver callback. * - * Boot or tests only. Clears the worker-wide resolver shared by every - * coroutine; next component resolution falls back to default behavior. + * Clears the worker-wide resolver shared by every coroutine; next + * component resolution falls back to default behavior. * * @internal */ + #[BootOrTestsOnly] public static function forgetComponentsResolver(): void { static::$componentsResolver = null; @@ -447,13 +451,14 @@ public static function forgetComponentsResolver(): void /** * Set the callback that should be used to resolve components within views. * - * Boot-only. The resolver persists in a static property for the worker - * lifetime and runs on every subsequent component resolution. + * The resolver persists in a static property for the worker lifetime + * and runs on every subsequent component resolution. * * @param Closure(string $component, array $data): static $resolver * * @internal */ + #[BootOnly] public static function resolveComponentsUsing(Closure $resolver): void { static::$componentsResolver = $resolver; diff --git a/src/view/src/Concerns/ManagesEvents.php b/src/view/src/Concerns/ManagesEvents.php index 3a0393b468..63c907638f 100644 --- a/src/view/src/Concerns/ManagesEvents.php +++ b/src/view/src/Concerns/ManagesEvents.php @@ -13,8 +13,8 @@ trait ManagesEvents /** * Register a view creator event. * - * Boot-only. The listener persists on the singleton View Factory's event - * dispatcher for the worker lifetime and affects every subsequent request. + * When the dispatcher is the application's worker-shared dispatcher, the + * listener persists for the worker lifetime and affects every subsequent request. */ public function creator(array|string $views, Closure|string $callback): array { @@ -30,8 +30,8 @@ public function creator(array|string $views, Closure|string $callback): array /** * Register multiple view composers via an array. * - * Boot-only. The listeners persist on the singleton View Factory's event - * dispatcher for the worker lifetime and affect every subsequent request. + * When the dispatcher is the application's worker-shared dispatcher, the + * listeners persist for the worker lifetime and affect every subsequent request. */ public function composers(array $composers): array { @@ -47,8 +47,8 @@ public function composers(array $composers): array /** * Register a view composer event. * - * Boot-only. The listener persists on the singleton View Factory's event - * dispatcher for the worker lifetime and affects every subsequent request. + * When the dispatcher is the application's worker-shared dispatcher, the + * listener persists for the worker lifetime and affects every subsequent request. */ public function composer(array|string $views, Closure|string $callback): array { diff --git a/src/view/src/Engines/CompilerEngine.php b/src/view/src/Engines/CompilerEngine.php index f33f53d707..73d2750f4c 100755 --- a/src/view/src/Engines/CompilerEngine.php +++ b/src/view/src/Engines/CompilerEngine.php @@ -149,9 +149,9 @@ public function getCompiler(): CompilerInterface /** * Clear the cache of compiled view paths verified as not expired. * - * Boot or tests only when called directly. Clears the worker-wide compile-check - * cache shared by every coroutine; concurrent compilations may re-check disk - * redundantly. The view:clear command owns runtime invalidation. + * Clears the worker-wide compile-check cache shared by every coroutine; + * concurrent compilations may re-check disk redundantly. The view:clear + * command owns runtime invalidation. */ public static function forgetCompiledOrNotExpired(): void { diff --git a/src/view/src/Engines/EngineResolver.php b/src/view/src/Engines/EngineResolver.php index 835cc514ba..518888d896 100755 --- a/src/view/src/Engines/EngineResolver.php +++ b/src/view/src/Engines/EngineResolver.php @@ -6,6 +6,8 @@ use Closure; use Hypervel\Contracts\View\Engine; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; use InvalidArgumentException; class EngineResolver @@ -25,9 +27,10 @@ class EngineResolver * * The engine string typically corresponds to a file extension. * - * Boot-only. The resolver persists on the singleton EngineResolver for the - * worker lifetime and applies to every subsequent engine resolution. + * The resolver persists on the singleton EngineResolver for the worker + * lifetime and applies to every subsequent engine resolution. */ + #[BootOnly(whenShared: true)] public function register(string $engine, Closure $resolver): void { $this->forget($engine); @@ -56,9 +59,10 @@ public function resolve(string $engine): Engine /** * Remove a resolved engine. * - * Boot or tests only. Clears a worker-wide resolved engine shared by every - * coroutine; the next resolution rebuilds it from its registered resolver. + * On the shared resolver, this clears an engine used by every coroutine; + * the next resolution rebuilds it from its registered resolver. */ + #[BootOrTestsOnly(whenShared: true)] public function forget(string $engine): void { unset($this->resolved[$engine]); diff --git a/src/view/src/Factory.php b/src/view/src/Factory.php index 2f45b1a1a8..14292adcb4 100755 --- a/src/view/src/Factory.php +++ b/src/view/src/Factory.php @@ -13,6 +13,9 @@ use Hypervel\Contracts\View\Factory as FactoryContract; use Hypervel\Contracts\View\View as ViewContract; use Hypervel\Support\Arr; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\BootOrTestsOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\FailureReporter; use Hypervel\Support\Traits\Macroable; use Hypervel\View\Engines\EngineResolver; @@ -288,9 +291,10 @@ protected function getExtension(string $path): ?string /** * Add a piece of shared data to the environment. * - * Boot-only. Shared data persists on the singleton factory for the worker - * lifetime and is visible to every request handled by that worker. + * Shared data persists on the singleton factory for the worker lifetime + * and is visible to every request handled by that worker. */ + #[BootOnly(whenShared: true)] public function share(array|string $key, mixed $value = null): mixed { $keys = is_array($key) ? $key : [$key => $value]; @@ -325,10 +329,11 @@ public function decrementRender(): void /** * Register a pre-render observer. * - * Boot-only. The observer is called before each view's engine executes, - * receiving the View instance. Observers persist for the worker lifetime - * on this singleton. + * The observer is called before each view's engine executes, receiving + * the View instance. Observers persist for the worker lifetime on this + * singleton. */ + #[BootOnly(whenShared: true)] public function observeRendering(callable $observer): void { $this->renderingObservers[] = $observer; @@ -347,10 +352,11 @@ public function notifyRendering(ViewContract $view): void /** * Register a post-render observer. * - * Boot-only. The observer is called after each view's engine completes, - * receiving the View instance and any rendering failure or cancellation. - * Observers persist for the worker lifetime on this singleton. + * The observer is called after each view's engine completes, receiving + * the View instance and any rendering failure or cancellation. Observers + * persist for the worker lifetime on this singleton. */ + #[BootOnly(whenShared: true)] public function observeRendered(callable $observer): void { $this->renderedObservers[] = $observer; @@ -433,9 +439,10 @@ public function markAsRenderedOnce(string $id): void /** * Add a location to the array of view locations. * - * Boot-only. The location persists on the singleton view finder for the - * worker lifetime and affects every subsequent request. + * The location persists on the shared factory's finder for the worker + * lifetime and affects every subsequent request. */ + #[BootOnly(whenShared: true)] public function addLocation(string $location): void { $this->finder->addLocation($location); @@ -444,9 +451,10 @@ public function addLocation(string $location): void /** * Prepend a location to the array of view locations. * - * Boot-only. The location persists on the singleton view finder for the - * worker lifetime and affects every subsequent request. + * The location persists on the shared factory's finder for the worker + * lifetime and affects every subsequent request. */ + #[BootOnly(whenShared: true)] public function prependLocation(string $location): void { $this->finder->prependLocation($location); @@ -455,11 +463,12 @@ public function prependLocation(string $location): void /** * Add a new namespace to the loader. * - * Boot-only. The namespace persists on the singleton view finder for the - * worker lifetime and affects every subsequent request. + * The namespace persists on the shared factory's finder for the worker + * lifetime and affects every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function addNamespace(string $namespace, string|array $hints): static { $this->finder->addNamespace($namespace, $hints); @@ -470,11 +479,12 @@ public function addNamespace(string $namespace, string|array $hints): static /** * Prepend a new namespace to the loader. * - * Boot-only. The namespace persists on the singleton view finder for the - * worker lifetime and affects every subsequent request. + * The namespace persists on the shared factory's finder for the worker + * lifetime and affects every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function prependNamespace(string $namespace, string|array $hints): static { $this->finder->prependNamespace($namespace, $hints); @@ -485,11 +495,12 @@ public function prependNamespace(string $namespace, string|array $hints): static /** * Replace the namespace hints for the given namespace. * - * Boot-only. The namespace persists on the singleton view finder for the - * worker lifetime and affects every subsequent request. + * The namespace persists on the shared factory's finder for the worker + * lifetime and affects every subsequent request. * * @return $this */ + #[BootOnly(whenShared: true)] public function replaceNamespace(string $namespace, string|array $hints): static { $this->finder->replaceNamespace($namespace, $hints); @@ -500,9 +511,12 @@ public function replaceNamespace(string $namespace, string|array $hints): static /** * Register a valid view extension and its engine. * - * Boot-only. The extension persists on the singleton Factory and engine - * resolver for the worker lifetime and affects every subsequent request. + * On the shared factory, the extension persists for the worker lifetime and + * affects every subsequent request. A given resolver is also registered on + * the engine resolver, which cloned factories share and which is usually the + * application's worker-shared resolver. */ + #[BootOnly(whenShared: true)] public function addExtension(string $extension, string $engine, ?Closure $resolver = null): void { $this->finder->addExtension($extension); @@ -570,10 +584,10 @@ public function getFinder(): ViewFinderInterface /** * Set the view finder instance. * - * Boot or tests only. Swaps the finder on the singleton View Factory used - * by every coroutine; per-request use races and changes resolution for all - * concurrent renders. + * Swaps the finder on the singleton View Factory used by every coroutine; + * per-request use races and changes resolution for all concurrent renders. */ + #[BootOrTestsOnly(whenShared: true)] public function setFinder(ViewFinderInterface $finder): void { $this->finder = $finder; @@ -581,9 +595,6 @@ public function setFinder(ViewFinderInterface $finder): void /** * Flush the cache of views located by the finder. - * - * Boot or tests only. Clears the singleton finder's path cache shared - * across all coroutines. */ public function flushFinderCache(): void { @@ -601,10 +612,10 @@ public function getDispatcher(): Dispatcher /** * Set the event dispatcher instance. * - * Tests only. Swaps the dispatcher on the singleton View Factory used by - * every coroutine; per-request use races and breaks every concurrent view - * event. + * Swaps the dispatcher on the singleton View Factory used by every coroutine; + * per-request use races and breaks every concurrent view event. */ + #[TestsOnly(whenShared: true)] public function setDispatcher(Dispatcher $events): void { $this->events = $events; @@ -621,10 +632,10 @@ public function getContainer(): Container /** * Set the IoC container instance. * - * Tests only. Swaps the container on the singleton View Factory used by - * every coroutine; per-request use races and breaks every concurrent - * view resolution. + * Swaps the container on the singleton View Factory used by every coroutine; + * per-request use races and breaks every concurrent view resolution. */ + #[TestsOnly(whenShared: true)] public function setContainer(Container $container): void { $this->container = $container; diff --git a/src/view/src/FileViewFinder.php b/src/view/src/FileViewFinder.php index bf77feb5d3..d0481c1f34 100755 --- a/src/view/src/FileViewFinder.php +++ b/src/view/src/FileViewFinder.php @@ -5,6 +5,7 @@ namespace Hypervel\View; use Hypervel\Filesystem\Filesystem; +use Hypervel\Support\Attributes\BootOnly; use InvalidArgumentException; class FileViewFinder implements ViewFinderInterface @@ -123,9 +124,10 @@ protected function getPossibleViewFiles(string $name): array /** * Add a location to the finder. * - * Boot-only. Request-time use changes shared view paths while concurrent - * coroutines may still be resolving views. + * On the shared factory's finder, request-time use changes view paths while + * concurrent coroutines may still be resolving views. */ + #[BootOnly(whenShared: true)] public function addLocation(string $location): void { $this->paths[] = $this->resolvePath($location); @@ -134,9 +136,10 @@ public function addLocation(string $location): void /** * Prepend a location to the finder. * - * Boot-only. Request-time use changes shared view paths while concurrent - * coroutines may still be resolving views. + * On the shared factory's finder, request-time use changes view paths while + * concurrent coroutines may still be resolving views. */ + #[BootOnly(whenShared: true)] public function prependLocation(string $location): void { array_unshift($this->paths, $this->resolvePath($location)); @@ -153,9 +156,10 @@ protected function resolvePath(string $path): string /** * Add a namespace hint to the finder. * - * Boot-only. Request-time use changes shared view hints while concurrent - * coroutines may still be resolving views. + * On the shared factory's finder, request-time use changes view hints while + * concurrent coroutines may still be resolving views. */ + #[BootOnly(whenShared: true)] public function addNamespace(string $namespace, string|array $hints): void { $hints = array_map($this->resolvePath(...), (array) $hints); @@ -170,9 +174,10 @@ public function addNamespace(string $namespace, string|array $hints): void /** * Prepend a namespace hint to the finder. * - * Boot-only. Request-time use changes shared view hints while concurrent - * coroutines may still be resolving views. + * On the shared factory's finder, request-time use changes view hints while + * concurrent coroutines may still be resolving views. */ + #[BootOnly(whenShared: true)] public function prependNamespace(string $namespace, string|array $hints): void { $hints = array_map($this->resolvePath(...), (array) $hints); @@ -187,9 +192,10 @@ public function prependNamespace(string $namespace, string|array $hints): void /** * Replace the namespace hints for the given namespace. * - * Boot-only. Request-time use changes shared view hints while concurrent - * coroutines may still be resolving views. + * On the shared factory's finder, request-time use changes view hints while + * concurrent coroutines may still be resolving views. */ + #[BootOnly(whenShared: true)] public function replaceNamespace(string $namespace, string|array $hints): void { $this->hints[$namespace] = array_map($this->resolvePath(...), (array) $hints); @@ -198,9 +204,10 @@ public function replaceNamespace(string $namespace, string|array $hints): void /** * Register an extension with the view finder. * - * Boot-only. Request-time use changes shared view extensions while - * concurrent coroutines may still be resolving views. + * On the shared factory's finder, request-time use changes view extensions + * while concurrent coroutines may still be resolving views. */ + #[BootOnly(whenShared: true)] public function addExtension(string $extension): void { if (($index = array_search($extension, $this->extensions)) !== false) { @@ -220,9 +227,6 @@ public function hasHintInformation(string $name): bool /** * Flush the cache of located views. - * - * Boot or tests only. Request-time use clears shared lookup state while - * concurrent coroutines may still be resolving views. */ public function flush(): void { @@ -240,11 +244,12 @@ public function getFilesystem(): Filesystem /** * Set the active view paths. * - * Boot-only. Request-time use changes shared view paths while concurrent - * coroutines may still be resolving views. + * On the shared factory's finder, request-time use changes view paths while + * concurrent coroutines may still be resolving views. * * @return $this */ + #[BootOnly(whenShared: true)] public function setPaths(array $paths): static { $this->paths = $paths; diff --git a/src/view/src/ViewFinderInterface.php b/src/view/src/ViewFinderInterface.php index 8b4006088c..ea9d2aba95 100755 --- a/src/view/src/ViewFinderInterface.php +++ b/src/view/src/ViewFinderInterface.php @@ -4,6 +4,8 @@ namespace Hypervel\View; +use Hypervel\Support\Attributes\BootOnly; + interface ViewFinderInterface { /** @@ -19,26 +21,31 @@ public function find(string $view): string; /** * Add a location to the finder. */ + #[BootOnly(whenShared: true)] public function addLocation(string $location): void; /** * Add a namespace hint to the finder. */ + #[BootOnly(whenShared: true)] public function addNamespace(string $namespace, string|array $hints): void; /** * Prepend a namespace hint to the finder. */ + #[BootOnly(whenShared: true)] public function prependNamespace(string $namespace, string|array $hints): void; /** * Replace the namespace hints for the given namespace. */ + #[BootOnly(whenShared: true)] public function replaceNamespace(string $namespace, string|array $hints): void; /** * Add a valid view extension to the finder. */ + #[BootOnly(whenShared: true)] public function addExtension(string $extension): void; /** @@ -49,6 +56,7 @@ public function flush(): void; /** * Prepend a location to the finder. */ + #[BootOnly(whenShared: true)] public function prependLocation(string $location): void; /** diff --git a/src/websocket-server/composer.json b/src/websocket-server/composer.json index 0149be5d71..f69692e987 100644 --- a/src/websocket-server/composer.json +++ b/src/websocket-server/composer.json @@ -31,6 +31,7 @@ "require": { "php": "^8.4", "ext-swoole": "^6.3.0RC1", + "hypervel/attributes": "^0.4", "hypervel/collections": "^0.4", "hypervel/container": "^0.4", "hypervel/context": "^0.4", diff --git a/src/websocket-server/src/Server.php b/src/websocket-server/src/Server.php index aa4cf78c5c..8572385222 100644 --- a/src/websocket-server/src/Server.php +++ b/src/websocket-server/src/Server.php @@ -29,6 +29,7 @@ use Hypervel\HttpServer\RequestBridge; use Hypervel\HttpServer\ResponseBridge; use Hypervel\Routing\Router; +use Hypervel\Support\Attributes\BootOnly; use Hypervel\Support\Defer\DeferredCallback; use Hypervel\Support\Defer\DeferredCallbackCollection; use Hypervel\Support\FailureReporter; @@ -536,12 +537,13 @@ public function getServerName(): string /** * Set the server name. * - * Boot-only. Mutates the worker-lifetime WebSocket handler before Swoole - * starts; runtime use races across connections and changes emitted server + * Mutates the worker-lifetime WebSocket handler before Swoole starts; + * runtime use races across connections and changes emitted server * context. * * @return $this */ + #[BootOnly(whenShared: true)] public function setServerName(string $serverName): static { $this->serverName = $serverName; diff --git a/tests/Auth/AuthRequestGuardTest.php b/tests/Auth/AuthRequestGuardTest.php index b37e864420..31bb892de9 100644 --- a/tests/Auth/AuthRequestGuardTest.php +++ b/tests/Auth/AuthRequestGuardTest.php @@ -156,6 +156,25 @@ public function testTwoGuardNamesDoNotCollideInContext(): void $this->assertSame($user2, $guard2->user()); } + public function testNewGuardWithTheSameNameStartsWithoutThePreviousGuardsUsers(): void + { + RequestContext::set(Request::create('/')); + $previousGuard = new RequestGuard('custom', fn () => m::mock(Authenticatable::class), $this->app); + $previousGuard->user(); + $previousGuard->setUser(m::mock(Authenticatable::class)); + $callCount = 0; + + $guard = new RequestGuard('custom', function () use (&$callCount) { + ++$callCount; + + return null; + }, $this->app); + + $this->assertFalse($guard->hasUser()); + $this->assertNull($guard->user()); + $this->assertSame(1, $callCount); + } + public function testReplacingRequestInContextChangesWhatGuardSees(): void { $request1 = Request::create('/one'); @@ -189,7 +208,7 @@ public function testAuthContextKeysIncludeOnlyDurableExplicitState(): void $guard = new RequestGuard('custom', fn () => null, $this->app); $this->assertSame([ - '__auth.guards.custom.user.explicit', + (fn (): string => $this->getExplicitUserContextKey())->call($guard), ], $guard->getAuthContextKeys()); } diff --git a/tests/Auth/AuthTokenGuardTest.php b/tests/Auth/AuthTokenGuardTest.php index 08506fb788..becb2aaa80 100644 --- a/tests/Auth/AuthTokenGuardTest.php +++ b/tests/Auth/AuthTokenGuardTest.php @@ -5,7 +5,6 @@ namespace Hypervel\Tests\Auth; use Hypervel\Auth\TokenGuard; -use Hypervel\Context\CoroutineContext; use Hypervel\Context\RequestContext; use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Auth\UserProvider; @@ -472,6 +471,26 @@ public function testExplicitUsersAreIsolatedByGuard(): void $this->assertSame($secondUser, $secondGuard->user()); } + public function testNewGuardWithTheSameNameStartsWithoutThePreviousGuardsUsers(): void + { + $tokenUser = new AuthTokenGuardTestUser; + $tokenUser->id = 1; + $explicitUser = new AuthTokenGuardTestUser; + $explicitUser->id = 2; + $previousProvider = m::mock(UserProvider::class); + $previousProvider->expects('retrieveByCredentials')->with(['api_token' => 'request-token'])->andReturn($tokenUser); + $previousGuard = $this->createGuard($previousProvider, Request::create('/', 'GET', ['api_token' => 'request-token'])); + $this->assertSame($tokenUser, $previousGuard->user()); + $previousGuard->setUser($explicitUser); + $provider = m::mock(UserProvider::class); + $provider->expects('retrieveByCredentials')->with(['api_token' => 'request-token'])->andReturnNull(); + + $guard = new TokenGuard('token', $provider, $this->app); + + $this->assertFalse($guard->hasUser()); + $this->assertNull($guard->user()); + } + public function testExplicitUsersAreIsolatedBetweenCoroutines(): void { $provider = m::mock(UserProvider::class); @@ -498,7 +517,7 @@ function () use ($guard): int { ]); $this->assertSame([1, 2], [$first, $second]); - $this->assertNull(CoroutineContext::get('__auth.guards.token.user.explicit')); + $this->assertFalse($guard->hasUser()); } public function testAuthContextKeysIncludeOnlyDurableExplicitState(): void @@ -510,7 +529,7 @@ public function testAuthContextKeysIncludeOnlyDurableExplicitState(): void ); $this->assertSame([ - '__auth.guards.token.user.explicit', + (fn (): string => $this->getExplicitUserContextKey())->call($guard), ], $guard->getAuthContextKeys()); } diff --git a/tests/Auth/PackageMetadataTest.php b/tests/Auth/PackageMetadataTest.php index ac50afe80b..141f1d1404 100644 --- a/tests/Auth/PackageMetadataTest.php +++ b/tests/Auth/PackageMetadataTest.php @@ -36,6 +36,7 @@ public function testDependenciesAndProvidersAreDeclared(): void foreach ([ 'nesbot/carbon', + 'hypervel/attributes', 'hypervel/cache', 'hypervel/collections', 'hypervel/config', diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 3a3ee67e99..dd22810855 100644 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -6,6 +6,7 @@ use __PHP_Incomplete_Class; use Hypervel\Cache\ArrayStore; +use Hypervel\Cache\Repository; use Hypervel\Cache\SerializableClassPolicy; use Hypervel\Contracts\Cache\RefreshableLock; use Hypervel\Support\CarbonImmutable; @@ -522,6 +523,29 @@ public function testSeparateArrayStoreInstancesDoNotShareContextData(): void $this->assertSame('second', $second->get('key')); } + public function testClonedRepositoryStartsWithTheCurrentValuesAndLocksButChangesIndependently(): void + { + $repository = new Repository(new InspectableArrayStore); + $repository->put('shared', 'original', 60); + $repository->lock('shared-lock', 10, 'owner')->acquire(); + + $clone = clone $repository; + + $this->assertSame('original', $clone->get('shared')); + $this->assertFalse($clone->lock('shared-lock', 10, 'other')->acquire()); + + $clone->put('shared', 'changed', 60); + $clone->put('clone-only', 'value', 60); + $clone->restoreLock('shared-lock', 'owner')->release(); + $repository->lock('original-lock', 10, 'owner')->acquire(); + + $this->assertSame('original', $repository->get('shared')); + $this->assertNull($repository->get('clone-only')); + $this->assertSame(['shared-lock', 'original-lock'], array_keys($repository->getStore()->lockRecords())); + $this->assertSame(['changed', 'value'], [$clone->get('shared'), $clone->get('clone-only')]); + $this->assertSame([], $clone->getStore()->lockRecords()); + } + public function testAllOnlyReturnsCurrentStoreContextData(): void { CarbonImmutable::setTestNow(CarbonImmutable::now()); diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index d256c8ca4c..5ed82a37d3 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -737,24 +737,19 @@ public function testForgetDriverForgets(): void $this->assertNull($cacheManager->store('forget')->get('foo')); } - public function testForgetDriverForgetsTheCallingCoroutinesMemoizedRepository(): void + #[DataProvider('memoizedStoresToForget')] + public function testForgetDriverForgetsTheCallingCoroutinesMemoizedRepository(string $store, bool $forgetConfiguredStores): void { - $app = $this->getApp([ - 'cache' => [ - 'stores' => [ - 'array' => ['driver' => 'array'], - ], - ], - ]); - $cacheManager = new CacheManager($app); - $firstRepository = $cacheManager->memo('array'); + $stores = [$store => ['driver' => 'array']]; + $cacheManager = new CacheManager($this->getApp(['cache' => ['stores' => $stores]])); + $firstRepository = $cacheManager->memo($store); $firstMemoizedStore = $firstRepository->getStore(); $this->assertInstanceOf(MemoizedStore::class, $firstMemoizedStore); - $cacheManager->forgetDriver('array'); + $cacheManager->forgetDriver($forgetConfiguredStores ? array_keys($stores) : $store); - $secondRepository = $cacheManager->memo('array'); + $secondRepository = $cacheManager->memo($store); $secondMemoizedStore = $secondRepository->getStore(); $this->assertInstanceOf(MemoizedStore::class, $secondMemoizedStore); @@ -762,6 +757,35 @@ public function testForgetDriverForgetsTheCallingCoroutinesMemoizedRepository(): $this->assertNotSame($firstMemoizedStore->getInnerStore(), $secondMemoizedStore->getInnerStore()); } + /** + * Provide store names and whether to forget them through the configured store keys. + */ + public static function memoizedStoresToForget(): array + { + return [ + 'string name' => ['array', false], + 'numeric name from the configured store keys' => ['0', true], + ]; + } + + public function testMemoizedRepositoriesBelongToTheirManager(): void + { + $config = ['cache' => ['stores' => ['array' => ['driver' => 'array']]]]; + $first = new CacheManager($this->getApp($config)); + $second = new CacheManager($this->getApp($config)); + + $first->memo('array')->put('key', 'first', 60); + $second->memo('array')->put('key', 'second', 60); + $firstMemo = $first->memo('array'); + + $this->assertSame('first', $firstMemo->get('key')); + $this->assertSame('second', $second->memo('array')->get('key')); + + $second->forgetDriver('array'); + + $this->assertSame($firstMemo, $first->memo('array')); + } + public function testMemoizedRepositoriesPreserveStoreIdentityAndSuppressDuplicateEvents(): void { $app = $this->getApp(['cache' => ['stores' => ['named' => ['driver' => 'worker-array']]]]); diff --git a/tests/Cache/CacheMemoizedStoreTest.php b/tests/Cache/CacheMemoizedStoreTest.php index ea87cf27ea..51ce91a06d 100644 --- a/tests/Cache/CacheMemoizedStoreTest.php +++ b/tests/Cache/CacheMemoizedStoreTest.php @@ -304,6 +304,29 @@ public function flush(): bool $this->assertNull($memoized->tags(['users'])->get('name')); } + public function testClonedTaggedCacheChangesIndependentlyOfTheOriginal(): void + { + $repository = new Repository(new ArrayStore); + $memoized = new Repository(new MemoizedStore('array', $repository)); + + $repository->tags(['users'])->put('name', 'Tim', 60); + $tagged = $memoized->tags(['users']); + $this->assertSame('Tim', $tagged->get('name')); + + $copy = clone $tagged; + $copy->put('clone-only', 'value', 60); + + $this->assertSame('value', $copy->get('clone-only')); + $this->assertNull($repository->tags(['users'])->get('clone-only')); + + $copy->flush(); + + $this->assertNull($copy->get('name')); + $this->assertNull($copy->tags(['users'])->get('name')); + $this->assertSame('Tim', $tagged->get('name')); + $this->assertSame('Tim', $repository->tags(['users'])->get('name')); + } + public function testMemoizedStoreCanWrapStackStore(): void { $stackRepo = $this->createStackRepository(); diff --git a/tests/Cache/CacheTaggedCacheTest.php b/tests/Cache/CacheTaggedCacheTest.php index 176edb72f5..7893bdd3ef 100644 --- a/tests/Cache/CacheTaggedCacheTest.php +++ b/tests/Cache/CacheTaggedCacheTest.php @@ -498,6 +498,19 @@ public function testTagsCacheForever(): void $this->assertSame('bar', $store->tags($tags)->get('foo')); } + public function testFlushingAClonedTaggedCacheLeavesTheOriginalIntact(): void + { + $tagged = (new Repository(new ArrayStore))->tags('group'); + $tagged->put('key', 'value', 60); + + $copy = clone $tagged; + $copy->flush(); + + $this->assertNull($copy->get('key')); + $this->assertNull($copy->tags('group')->get('key')); + $this->assertSame('value', $tagged->get('key')); + } + public function testIncrementAcceptsStringBackedEnum(): void { $store = new ArrayStore; diff --git a/tests/Console/PackageMetadataTest.php b/tests/Console/PackageMetadataTest.php index b38a6e7b52..7738f1d0f8 100644 --- a/tests/Console/PackageMetadataTest.php +++ b/tests/Console/PackageMetadataTest.php @@ -38,6 +38,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'symfony/event-dispatcher', 'symfony/finder', 'symfony/process', + 'hypervel/attributes', 'hypervel/bus', 'hypervel/cache', 'hypervel/collections', diff --git a/tests/Container/ContainerCallTest.php b/tests/Container/ContainerCallTest.php index 78670ce1fc..8d0987483c 100644 --- a/tests/Container/ContainerCallTest.php +++ b/tests/Container/ContainerCallTest.php @@ -11,7 +11,6 @@ use Hypervel\Container\BoundMethod; use Hypervel\Container\Container; use Hypervel\Container\ParameterRecipe; -use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Container\BindingResolutionException; use Hypervel\Tests\TestCase; use ReflectionProperty; @@ -715,7 +714,7 @@ class ContainerCallStateInspectionStub extends Container { public function hasResolutionState(): bool { - return CoroutineContext::has(self::RESOLUTION_STATE_CONTEXT_KEY); + return $this->getResolutionState() !== null; } } diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 8c8d50ba30..d662878003 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -5,12 +5,12 @@ namespace Hypervel\Tests\Container\ContainerTest; use Attribute; +use Closure; use Hypervel\Container\Attributes\Bind; use Hypervel\Container\Attributes\Scoped; use Hypervel\Container\Attributes\Singleton; use Hypervel\Container\Container; use Hypervel\Container\EntryNotFoundException; -use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Container\BindingResolutionException; use Hypervel\Contracts\Container\CircularDependencyException; use Hypervel\Contracts\Container\ContextualAttribute; @@ -41,9 +41,9 @@ use Hypervel\Tests\TestCase; use InvalidArgumentException; use LogicException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; use Psr\Container\ContainerExceptionInterface; -use ReflectionClass; use ReflectionProperty; use stdClass; use Swoole\Coroutine\CanceledException; @@ -805,6 +805,95 @@ function () use ($container) { $this->assertFalse($observer, 'A scoped instance resolved in one coroutine leaked into another.'); } + public function testScopedInstancesAreIsolatedBetweenContainers(): void + { + $first = new Container; + $second = new Container; + $first->scoped(ContainerConcreteStub::class); + $second->scoped(ContainerConcreteStub::class); + + $instance = $first->make(ContainerConcreteStub::class); + + $this->assertFalse($second->resolvedScoped(ContainerConcreteStub::class)); + $this->assertFalse($second->isScopedInstance($instance)); + $this->assertNotSame($instance, $second->make(ContainerConcreteStub::class)); + } + + /** + * @param Closure(Container): void $invalidate + */ + #[DataProvider('scopedInstanceInvalidationProvider')] + public function testInvalidatingScopedInstancesLeavesOtherContainersIntact(Closure $invalidate): void + { + $first = new Container; + $second = new Container; + $first->scoped(ContainerConcreteStub::class); + $second->scoped(ContainerConcreteStub::class); + + $instance = $first->make(ContainerConcreteStub::class); + $second->make(ContainerConcreteStub::class); + + $invalidate($second); + + $this->assertSame($instance, $first->make(ContainerConcreteStub::class)); + } + + public static function scopedInstanceInvalidationProvider(): array + { + return [ + 'forgetInstance' => [static fn (Container $container) => $container->forgetInstance(ContainerConcreteStub::class)], + 'forgetInstances' => [static fn (Container $container) => $container->forgetInstances()], + 'forgetScopedInstances' => [static fn (Container $container) => $container->forgetScopedInstances()], + 'bind' => [static fn (Container $container) => $container->bind(ContainerConcreteStub::class)], + 'instance' => [static fn (Container $container) => $container->instance(ContainerConcreteStub::class, new ContainerConcreteStub)], + 'extend' => [static fn (Container $container) => $container->extend(ContainerConcreteStub::class, static fn (): ContainerConcreteStub => new ContainerConcreteStub)], + ]; + } + + public function testResolvingThroughAnotherContainerIsNotCircular(): void + { + $inner = new Container; + $outer = new Container; + $outer->bind(ContainerConcreteStub::class, static fn (): ContainerConcreteStub => $inner->make(ContainerConcreteStub::class)); + + $this->assertInstanceOf(ContainerConcreteStub::class, $outer->make(ContainerConcreteStub::class)); + } + + public function testClonedContainerStartsWithTheCurrentScopedInstances(): void + { + $container = new Container; + $container->scoped(ContainerConcreteStub::class); + $instance = $container->make(ContainerConcreteStub::class); + + $clone = clone $container; + + $this->assertTrue($clone->isScopedInstance($instance)); + $this->assertSame($instance, $clone->make(ContainerConcreteStub::class)); + } + + public function testInvalidatingAClonedContainersScopedInstancesLeavesTheOriginalIntact(): void + { + $container = new Container; + $container->scoped(ContainerConcreteStub::class); + $instance = $container->make(ContainerConcreteStub::class); + + $clone = clone $container; + $clone->forgetScopedInstances(); + + $this->assertNotSame($instance, $clone->make(ContainerConcreteStub::class)); + $this->assertSame($instance, $container->make(ContainerConcreteStub::class)); + } + + public function testContainerClonedBeforeResolvingScopedInstancesResolvesItsOwn(): void + { + $container = new Container; + $container->scoped(ContainerConcreteStub::class); + + $clone = clone $container; + + $this->assertNotSame($container->make(ContainerConcreteStub::class), $clone->make(ContainerConcreteStub::class)); + } + public function testExplicitTransientBindingOverridesScopedAttribute(): void { $container = new Container; @@ -1777,11 +1866,7 @@ public function testTransientBindingsOwnTheLifetimeOfTheirConcrete(): void (new ReflectionProperty($container, 'autoSingletons'))->getValue($container), ); - $scopedContextPrefix = (new ReflectionClass(Container::class)) - ->getReflectionConstant('SCOPED_CONTEXT_PREFIX') - ->getValue(); - - $this->assertFalse(CoroutineContext::has($scopedContextPrefix . ContainerBoundExecutionScopedConsumer::class)); + $this->assertFalse($container->resolvedScoped(ContainerBoundExecutionScopedConsumer::class)); } public function testTransientBindingDoesNotAdoptAPriorImplicitConcreteLifetime(): void @@ -2933,7 +3018,7 @@ class ContainerStateInspectionStub extends Container { public function hasResolutionState(): bool { - return CoroutineContext::has(self::RESOLUTION_STATE_CONTEXT_KEY); + return $this->getResolutionState() !== null; } public function resolutionLimit(): int diff --git a/tests/Container/CoroutineSafetyTest.php b/tests/Container/CoroutineSafetyTest.php index fb44ec2e9f..36be738bc5 100644 --- a/tests/Container/CoroutineSafetyTest.php +++ b/tests/Container/CoroutineSafetyTest.php @@ -127,6 +127,21 @@ public function testScopedInstanceReturnsSameInstanceWithinCoroutine(): void $this->assertTrue($results['co1']); } + public function testForkedCoroutineInheritsScopedInstances(): void + { + $container = new Container; + $container->scoped(CoroutineCounter::class); + $instance = $container->make(CoroutineCounter::class); + $result = new Channel(1); + + $childId = Coroutine::fork(static function () use ($container, $result): void { + $result->push($container->make(CoroutineCounter::class)); + }); + + $this->assertSame($instance, $result->pop(1.0)); + Coroutine::join([$childId], 1.0); + } + public function testForgetScopedInstancesCleansUpForNextRequest(): void { $container = new Container; diff --git a/tests/Database/PackageMetadataTest.php b/tests/Database/PackageMetadataTest.php index 290c103763..a49c7224af 100644 --- a/tests/Database/PackageMetadataTest.php +++ b/tests/Database/PackageMetadataTest.php @@ -34,6 +34,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'symfony/finder', 'symfony/polyfill-php86', 'symfony/process', + 'hypervel/attributes', 'hypervel/broadcasting', 'hypervel/collections', 'hypervel/conditionable', diff --git a/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php b/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php index c8ffb12acf..79cb075513 100644 --- a/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php +++ b/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php @@ -389,4 +389,38 @@ public function testActingAsGuestClearsTheUser(): void $this->get('/me') ->assertRedirect(route('login')); } + + public function testForgettingGuardsDropsActingAsUsers(): void + { + Route::get('me', function (Request $request) { + return 'Hello ' . $request->user()->username; + })->middleware(['auth']); + Route::get('login', function () { + return 'Login'; + })->name('login'); + + $user = User::where('username', '=', 'taylorotwell')->first(); + + $this->actingAs($user) + ->get('/me') + ->assertSuccessful(); + + Auth::forgetGuards(); + + $this->assertGuest(); + $this->get('/me') + ->assertRedirect(route('login')); + } + + public function testForgettingGuardsReloadsUsersFromAValidSession(): void + { + $user = User::where('username', '=', 'taylorotwell')->first(); + Auth::guard('web')->login($user); + + Auth::forgetGuards(); + + $resolved = Auth::guard('web')->user(); + $this->assertNotSame($user, $resolved); + $this->assertTrue($user->is($resolved)); + } } diff --git a/tests/Integration/Redis/RedisProxyIntegrationTest.php b/tests/Integration/Redis/RedisProxyIntegrationTest.php index 3fb266ddaf..6b2b6fb0ed 100644 --- a/tests/Integration/Redis/RedisProxyIntegrationTest.php +++ b/tests/Integration/Redis/RedisProxyIntegrationTest.php @@ -244,7 +244,7 @@ public function testCopiedSiblingContextsUseDistinctPinnedRedisConnections(): vo ); $redis = Redis::connection($connectionName); $redis->multi(); - $contextKey = RedisProxy::CONNECTION_CONTEXT_PREFIX . $connectionName; + $contextKey = (fn (): string => $this->getContextKey())->call($redis); $parentConnection = CoroutineContext::get($contextKey); $childrenReady = new Channel(2); $releaseChildren = new Channel(2); diff --git a/tests/Jwt/JwtGuardTest.php b/tests/Jwt/JwtGuardTest.php index fcd290ae9a..38886edf8a 100644 --- a/tests/Jwt/JwtGuardTest.php +++ b/tests/Jwt/JwtGuardTest.php @@ -113,6 +113,33 @@ public function testUserCachesResultInContext(): void $this->assertSame($user, $guard->user()); // Should not call decode again } + public function testNewGuardWithTheSameNameStartsWithoutThePreviousGuardsState(): void + { + $previousUser = m::mock(Authenticatable::class); + $previousProvider = m::mock(UserProvider::class); + $previousProvider->expects('retrieveById')->with(42)->andReturn($previousUser); + $previousManager = m::mock(ManagerContract::class); + $previousManager->expects('decode')->with('request-token')->andReturn(['sub' => 42]); + $previousGuard = $this->createGuard( + provider: $previousProvider, + jwtManager: $previousManager, + request: $this->createRequestWithBearer('request-token'), + ); + $this->assertSame($previousUser, $previousGuard->user()); + $previousGuard->setUser(m::mock(Authenticatable::class)); + $previousGuard->setToken('previous-token'); + $provider = m::mock(UserProvider::class); + $provider->expects('retrieveById')->with(7)->andReturnNull(); + $manager = m::mock(ManagerContract::class); + $manager->expects('decode')->with('request-token')->andReturn(['sub' => 7]); + + $guard = $this->createGuard(provider: $provider, jwtManager: $manager); + + $this->assertFalse($guard->hasUser()); + $this->assertNull($guard->user()); + $this->assertSame(['sub' => 7], $guard->payload()); + } + public function testCopiedRequestRechecksBlacklistAfterWarmingUserAndPayload(): void { $user = m::mock(Authenticatable::class); @@ -984,7 +1011,7 @@ public function testAuthContextKeysIncludeOnlyDurableExplicitState(): void ); $this->assertSame([ - '__auth.guards.jwt.user.explicit', + (fn (): string => $this->getExplicitUserContextKey())->call($guard), ], $guard->getAuthContextKeys()); } diff --git a/tests/PHPStan/Rules/Coroutine/WorkerStateRuleTest.php b/tests/PHPStan/Rules/Coroutine/WorkerStateRuleTest.php index 45be05d956..277dee1c87 100644 --- a/tests/PHPStan/Rules/Coroutine/WorkerStateRuleTest.php +++ b/tests/PHPStan/Rules/Coroutine/WorkerStateRuleTest.php @@ -209,64 +209,64 @@ private static function getExpectedErrors(): array return [ 'shared' => [ - $sharedState('SharedService', 'request', 'the current request', 84), - $sharedState('NestedService', 'user', 'a user', 127), - $sharedState('SelfBuildingSingleton', 'request', 'the current request', 170), - $sharedState('CurrentUserSingleton', 'user', 'a user', 197), - $sharedState('CachedService', 'request', 'the current request', 221), - $sharedState('SingletonBoundService', 'request', 'the current request', 265), - $sharedState('ContractImplementation', 'request', 'the current request', 289), - $sharedState('AttributeSingleton', 'request', 'the current request', 311), - $sharedState('ReassignedUserService', 'user', 'a user', 355), - $sharedState('CurrentUserCacheService', 'user', 'a user', 383), - $sharedState('RequestCaptureService', 'request', 'the current request', 389), - $sharedState('ContextualImplementation', 'request', 'the current request', 404), - $sharedState('OtherConsumer', 'model', 'a user', 417), - $sharedWrite('CountingMiddleware', 'handle', 'handled', 434), - $sharedWrite('ReportController', 'show', 'shown', 452), - $sharedState('ReportService', 'request', 'the current request', 493), - $sharedState('RoutableService', 'request', 'the current request', 503), - $sharedState('ClosureService', 'request', 'the current request', 565), - $sharedState('JobService', 'request', 'the current request', 630), - $sharedState('SendReportListener', 'request', 'the current request', 647), - $sharedState('MappedHandler', 'request', 'the current request', 697), - $sharedWrite('HelperController', 'track', 'tracked', 736), - $sharedWrite('OverridingRecordingController', 'record', 'overridden', 798), - $sharedWrite('LexicalParentController', 'resolve', 'resolved', 816), + $sharedState('SharedService', 'request', 'the current request', 86), + $sharedState('NestedService', 'user', 'a user', 140), + $sharedState('SelfBuildingSingleton', 'request', 'the current request', 183), + $sharedState('CurrentUserSingleton', 'user', 'a user', 210), + $sharedState('CachedService', 'request', 'the current request', 234), + $sharedState('SingletonBoundService', 'request', 'the current request', 278), + $sharedState('ContractImplementation', 'request', 'the current request', 302), + $sharedState('AttributeSingleton', 'request', 'the current request', 324), + $sharedState('ReassignedUserService', 'user', 'a user', 368), + $sharedState('CurrentUserCacheService', 'user', 'a user', 396), + $sharedState('RequestCaptureService', 'request', 'the current request', 402), + $sharedState('ContextualImplementation', 'request', 'the current request', 417), + $sharedState('OtherConsumer', 'model', 'a user', 430), + $sharedWrite('CountingMiddleware', 'handle', 'handled', 447), + $sharedWrite('ReportController', 'show', 'shown', 465), + $sharedState('ReportService', 'request', 'the current request', 506), + $sharedState('RoutableService', 'request', 'the current request', 516), + $sharedState('ClosureService', 'request', 'the current request', 578), + $sharedState('JobService', 'request', 'the current request', 643), + $sharedState('SendReportListener', 'request', 'the current request', 660), + $sharedState('MappedHandler', 'request', 'the current request', 710), + $sharedWrite('HelperController', 'track', 'tracked', 750), + $sharedWrite('OverridingRecordingController', 'record', 'overridden', 812), + $sharedWrite('LexicalParentController', 'resolve', 'resolved', 830), ], 'static' => [ - [sprintf(self::STATIC_STATE, self::NAMESPACE . 'SharedService', 'current', 'the current request'), 85, self::STATIC_TIP], - $staticWrite('CountingMiddleware', 'seen', self::REQUEST, 435), - $staticWrite('StoreReportRequest', 'validated', self::REQUEST, 575), - $staticWrite('ProcessReport', 'runs', self::JOB, 590), - $staticWrite('RebuildReport', 'rebuilt', self::JOB, 843), + [sprintf(self::STATIC_STATE, self::NAMESPACE . 'SharedService', 'current', 'the current request'), 87, self::STATIC_TIP], + $staticWrite('CountingMiddleware', 'seen', self::REQUEST, 448), + $staticWrite('StoreReportRequest', 'validated', self::REQUEST, 588), + $staticWrite('ProcessReport', 'runs', self::JOB, 603), + $staticWrite('RebuildReport', 'rebuilt', self::JOB, 857), ], 'cache' => [ - $cache('Property', 'SharedService', 'visits', 86), - $cache('Property', 'SharedService', 'visits', 87), $cache('Property', 'SharedService', 'visits', 88), - $cache('Static property', 'SharedService', 'memo', 93), - $cache('Static property', 'CountingMiddleware', 'clients', 436), + $cache('Property', 'SharedService', 'visits', 89), + $cache('Property', 'SharedService', 'visits', 90), + $cache('Static property', 'SharedService', 'memo', 95), + $cache('Static property', 'CountingMiddleware', 'clients', 449), ], // Without the cache check, the request-keyed static write in middleware is still a request-time write. 'static cache write' => [ - $staticWrite('CountingMiddleware', 'clients', self::REQUEST, 436), + $staticWrite('CountingMiddleware', 'clients', self::REQUEST, 449), ], 'config' => [ - [sprintf(self::CONFIG, self::REQUEST), 437, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::REQUEST), 453, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::REQUEST), 454, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::REQUEST), 455, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::REQUEST), 456, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::REQUEST), 457, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::JOB), 591, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::JOB), 605, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::JOB), 609, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::JOB), 610, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::REQUEST), 737, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::JOB), 844, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::JOB), 855, self::CONFIG_TIP], - [sprintf(self::CONFIG, self::JOB), 857, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::REQUEST), 450, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::REQUEST), 466, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::REQUEST), 467, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::REQUEST), 468, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::REQUEST), 469, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::REQUEST), 470, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::JOB), 604, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::JOB), 618, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::JOB), 622, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::JOB), 623, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::REQUEST), 751, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::JOB), 858, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::JOB), 869, self::CONFIG_TIP], + [sprintf(self::CONFIG, self::JOB), 871, self::CONFIG_TIP], ], ]; } diff --git a/tests/PHPStan/Rules/Coroutine/data/worker-state.php b/tests/PHPStan/Rules/Coroutine/data/worker-state.php index b4b56755d1..3037358683 100644 --- a/tests/PHPStan/Rules/Coroutine/data/worker-state.php +++ b/tests/PHPStan/Rules/Coroutine/data/worker-state.php @@ -24,6 +24,8 @@ use Hypervel\Foundation\Bus\Dispatchable; use Hypervel\Foundation\Http\FormRequest; use Hypervel\Http\Request; +use Hypervel\Support\Attributes\BootOnly; +use Hypervel\Support\Attributes\TestsOnly; use Hypervel\Support\Facades\App; use Hypervel\Support\Facades\Auth; use WeakMap; @@ -102,13 +104,24 @@ public function rememberLatest(Request $request): void /** * Set the request used for testing. * - * Tests only. The request persists on the shared service. + * The request persists on the shared service. */ + #[TestsOnly] public function setRequestForTesting(Request $request): void { $this->request = $request; } + #[TestsOnly(whenNotDefault: 'request')] + public function request(?Request $request = null): ?Request + { + if ($request !== null) { + $this->request = $request; + } + + return $this->request; + } + /** * Flush all static state. */ @@ -202,7 +215,7 @@ public function __construct(#[CurrentUser] public User $user) #[Singleton] class TestingRequestHolder { - /** Tests only. */ + #[TestsOnly] public function __construct(public Request $request) { } @@ -724,8 +737,9 @@ public function index(): string } /** - * Boot-only: the provider configures the controller once. + * The provider configures the controller once. */ + #[BootOnly] public function configure(): void { $this->apply(); diff --git a/tests/Passkeys/PackageMetadataTest.php b/tests/Passkeys/PackageMetadataTest.php index 306802889e..7a238e6b20 100644 --- a/tests/Passkeys/PackageMetadataTest.php +++ b/tests/Passkeys/PackageMetadataTest.php @@ -32,6 +32,7 @@ public function testDirectDependenciesMatchTheMonorepoConstraints(): void foreach ([ 'php', + 'hypervel/attributes', 'hypervel/auth', 'hypervel/collections', 'hypervel/config', diff --git a/tests/Passport/Feature/TokenGuardTest.php b/tests/Passport/Feature/TokenGuardTest.php index abcbb28149..117446e53c 100644 --- a/tests/Passport/Feature/TokenGuardTest.php +++ b/tests/Passport/Feature/TokenGuardTest.php @@ -166,6 +166,22 @@ public function testGuardMemosFollowServerChangesOnTheSameRequest(): void $this->assertNull($guard->client()); } + public function testNewGuardWithTheSameNameStartsWithoutThePreviousGuardsBearerUser(): void + { + $user = UserFactory::new()->create(); + ClientFactory::new()->asPersonalAccessTokenClient()->create(); + $request = Request::create('/guard'); + $request->headers->set('Authorization', 'Bearer ' . $user->createToken('test')->accessToken); + RequestContext::set($request); + $this->assertSame($user->id, auth('passport')->user()->getKey()); + + $this->app->make('auth')->forgetGuards(); + $user->delete(); + + $this->assertFalse(auth('passport')->hasUser()); + $this->assertNull(auth('passport')->user()); + } + public function testNothingIsReturnedWhenOauthThrowsException(): void { Exceptions::fake(); diff --git a/tests/Passport/Unit/TokenGuardTest.php b/tests/Passport/Unit/TokenGuardTest.php index 00a127fc23..68db5556da 100644 --- a/tests/Passport/Unit/TokenGuardTest.php +++ b/tests/Passport/Unit/TokenGuardTest.php @@ -14,6 +14,7 @@ use Hypervel\Http\Request; use Hypervel\Passport\ApiTokenCookieFactory; use Hypervel\Passport\Bridge\BearerRequestFactory; +use Hypervel\Passport\Client; use Hypervel\Passport\ClientRepository; use Hypervel\Passport\Guards\TokenGuard; use Hypervel\Passport\Passport; @@ -444,6 +445,36 @@ public function testUserProviderFailuresAreNotTreatedAsInvalidCookies(): void $guard->user(); } + public function testNewGuardWithTheSameNameStartsWithoutThePreviousGuardsUsersAndClients(): void + { + $encrypter = new Encrypter(str_repeat('a', 32), 'aes-256-cbc'); + $request = Request::create('/'); + $request->headers->set('X-CSRF-TOKEN', 'token'); + $request->cookies->set(Passport::cookie(), $encrypter->encrypt( + CookieValuePrefix::create(Passport::cookie(), $encrypter->getKey()) . JWT::encode([ + 'provider' => 'users', + 'sub' => 1, + 'csrf' => 'token', + 'exp' => time() + 600, + ], hash_hkdf('sha256', $encrypter->getKey(), 32, 'hypervel-passport-cookie'), 'HS256'), + false + )); + $previousProvider = m::mock(PassportUserProvider::class); + $previousProvider->expects('retrieveById')->with(1)->andReturn(new TokenGuardTestUser); + $previousGuard = $this->makeGuard($previousProvider, m::mock(ClientRepository::class), $encrypter, $request); + $this->assertNotNull($previousGuard->user()); + $previousGuard->setUser(new TokenGuardTestUser); + $previousGuard->setClient(new Client); + $provider = m::mock(PassportUserProvider::class); + $provider->expects('retrieveById')->with(1)->andReturnNull(); + + $guard = $this->makeGuard($provider, m::mock(ClientRepository::class), $encrypter, $request); + + $this->assertFalse($guard->hasUser()); + $this->assertNull($guard->user()); + $this->assertNull($guard->client()); + } + /** * Create the guard with real key derivation and the coroutine's request. */ diff --git a/tests/Queue/PackageMetadataTest.php b/tests/Queue/PackageMetadataTest.php index a9203c7eae..c552fd4a25 100644 --- a/tests/Queue/PackageMetadataTest.php +++ b/tests/Queue/PackageMetadataTest.php @@ -31,6 +31,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'nunomaduro/termwind', 'symfony/console', 'symfony/process', + 'hypervel/attributes', 'hypervel/bus', 'hypervel/cache', 'hypervel/collections', diff --git a/tests/RateLimiter/PackageMetadataTest.php b/tests/RateLimiter/PackageMetadataTest.php index 54a6d8772d..b8fbcf4232 100644 --- a/tests/RateLimiter/PackageMetadataTest.php +++ b/tests/RateLimiter/PackageMetadataTest.php @@ -27,6 +27,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void foreach ([ 'php', 'ext-swoole', + 'hypervel/attributes', 'hypervel/collections', 'hypervel/console', 'hypervel/container', @@ -34,6 +35,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'hypervel/coordinator', 'hypervel/core', 'hypervel/database', + 'hypervel/macroable', 'hypervel/redis', 'hypervel/support', 'psr/log', diff --git a/tests/Redis/MultiExecTest.php b/tests/Redis/MultiExecTest.php index e602671d42..cd09db1079 100644 --- a/tests/Redis/MultiExecTest.php +++ b/tests/Redis/MultiExecTest.php @@ -18,12 +18,6 @@ class MultiExecTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - CoroutineContext::forget(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'); - } - public function testPipelineWithoutCallbackReturnsInstanceForChaining(): void { $pipelineInstance = m::mock(PhpRedis::class); @@ -152,13 +146,12 @@ public function testPipelineWithCallbackDoesNotReleaseExistingContextConnection( $phpRedis->shouldReceive('pipeline')->once()->andReturn($pipelineInstance); $connection = $this->createMockConnection($phpRedis); + $redis = $this->createRedis($connection); // Set up existing connection in context BEFORE the pipeline call - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + CoroutineContext::set($this->contextKey($redis), $connection); $connection->shouldNotReceive('release'); - $redis = $this->createRedis($connection); - $redis->pipeline(function ($pipe) { // empty callback }); @@ -196,14 +189,13 @@ public function testTransactionWithCallbackDoesNotReleaseExistingContextConnecti $phpRedis->shouldReceive('multi')->once()->andReturn($multiInstance); $connection = $this->createMockConnection($phpRedis); + $redis = $this->createRedis($connection); // Set up existing connection in context BEFORE the transaction call - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + CoroutineContext::set($this->contextKey($redis), $connection); $connection->shouldNotReceive('release'); $connection->shouldReceive('clearWatchState')->once(); - $redis = $this->createRedis($connection); - $redis->transaction(function ($tx) { // empty callback }); @@ -243,10 +235,11 @@ public function testTransactionWithCallbackInvalidatesExistingContextConnectionO $connection = $this->createMockConnection($phpRedis); $connection->shouldReceive('invalidate')->once(); $connection->shouldNotReceive('release'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); try { - $this->createRedis($connection)->transaction(static function (): void { + $redis->transaction(static function (): void { }); $this->fail('Expected the transaction failure to propagate.'); } catch (RuntimeException $throwable) { @@ -255,7 +248,7 @@ public function testTransactionWithCallbackInvalidatesExistingContextConnectionO $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'), + CoroutineContext::get($this->contextKey($redis)), ); } @@ -284,7 +277,7 @@ public function testShouldTransformIsResetWhenConnectionReleasedAfterCallback(): // After pipeline callback completes, connection was released. // The connection should no longer be in context. - $this->assertNull(CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertNull(CoroutineContext::get($this->contextKey($redis))); } /** @@ -312,7 +305,7 @@ private function createRedis(m\MockInterface|RedisConnection $connection): Redis $pool = m::mock(RedisPool::class); $pool->shouldReceive('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); return new RedisProxy( @@ -321,4 +314,12 @@ private function createRedis(m\MockInterface|RedisConnection $connection): Redis m::mock(RedisSentinelFactory::class), ); } + + /** + * Get the coroutine context key of the given proxy's pinned connection. + */ + private function contextKey(RedisProxy $redis): string + { + return (fn (): string => $this->getContextKey())->call($redis); + } } diff --git a/tests/Redis/PackageMetadataTest.php b/tests/Redis/PackageMetadataTest.php index 47f32ca2c1..e18a355e98 100644 --- a/tests/Redis/PackageMetadataTest.php +++ b/tests/Redis/PackageMetadataTest.php @@ -31,6 +31,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'ext-filter', 'ext-swoole', 'psr/log', + 'hypervel/attributes', 'hypervel/collections', 'hypervel/config', 'hypervel/container', diff --git a/tests/Redis/RedisEventsTest.php b/tests/Redis/RedisEventsTest.php index 1cc85f5076..5f171a0782 100644 --- a/tests/Redis/RedisEventsTest.php +++ b/tests/Redis/RedisEventsTest.php @@ -245,7 +245,7 @@ private function createRedis(m\MockInterface|RedisConnection $connection): Redis $pool->shouldReceive('borrow')->andReturn($connection); $pool->shouldReceive('getOptions')->andReturn(PoolOptions::fromArray([])); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); return new RedisProxy( diff --git a/tests/Redis/RedisManagerTest.php b/tests/Redis/RedisManagerTest.php index 876266d33a..299c3ba966 100644 --- a/tests/Redis/RedisManagerTest.php +++ b/tests/Redis/RedisManagerTest.php @@ -29,12 +29,6 @@ */ class RedisManagerTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - CoroutineContext::forget(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'); - } - public function testConnectionReturnsRedisProxy(): void { $manager = $this->createManager(['default']); @@ -78,7 +72,7 @@ public function testConnectionDefaultsToDefault(): void public function testPurgeAcceptsUnitEnum(): void { - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('purge')->with('default'); $manager = $this->createManager(['default'], poolManager: $poolManager); $manager->connection('default'); @@ -92,7 +86,7 @@ public function testPurgeAcceptsUnitEnum(): void public function testIntegerBackedEnumConnectionNameIsNormalizedForResolutionAndPurge(): void { - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('purge')->once()->with('0'); $manager = $this->createManager(['0'], poolManager: $poolManager); @@ -104,12 +98,12 @@ public function testIntegerBackedEnumConnectionNameIsNormalizedForResolutionAndP $manager->purge(RedisConnectionName::Zero); $this->assertSame([], $manager->connections()); - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . '0')); + $this->assertFalse(CoroutineContext::has($this->contextKey($connection))); } public function testPurgeClearsProxyContextAndPool(): void { - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('purge')->once()->with('default'); $manager = $this->createManager(['default'], poolManager: $poolManager); @@ -119,7 +113,7 @@ public function testPurgeClearsProxyContextAndPool(): void $manager->purge(''); // Context should be cleared - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertFalse(CoroutineContext::has($this->contextKey($first))); // Next connection() should return a new instance $second = $manager->connection('default'); @@ -131,15 +125,15 @@ public function testPurgeDiscardsContextPinnedConnection(): void $pinnedConnection = m::mock(PhpRedisConnection::class); $pinnedConnection->expects('discard'); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('purge')->with('default'); $manager = $this->createManager(['default'], poolManager: $poolManager); - $manager->connection('default'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $pinnedConnection); + $contextKey = $this->contextKey($manager->connection('default')); + CoroutineContext::set($contextKey, $pinnedConnection); $manager->purge('default'); - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertFalse(CoroutineContext::has($contextKey)); } public function testReleaseConnectionsExhaustsProxiesAndPreservesFirstFailure(): void @@ -150,10 +144,8 @@ public function testReleaseConnectionsExhaustsProxiesAndPreservesFirstFailure(): $second = m::mock(PhpRedisConnection::class); $second->expects('release'); $manager = $this->createManager(['first', 'second']); - $manager->connection('first'); - $manager->connection('second'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'first', $first); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'second', $second); + CoroutineContext::set($this->contextKey($manager->connection('first')), $first); + CoroutineContext::set($this->contextKey($manager->connection('second')), $second); try { $manager->releaseConnections(); @@ -175,12 +167,9 @@ public function testReleaseConnectionsExhaustsProxiesAndPreservesFirstCancellati $third = m::mock(PhpRedisConnection::class); $third->expects('release')->andThrow($laterCancellation); $manager = $this->createManager(['first', 'second', 'third']); - $manager->connection('first'); - $manager->connection('second'); - $manager->connection('third'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'first', $first); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'second', $second); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'third', $third); + CoroutineContext::set($this->contextKey($manager->connection('first')), $first); + CoroutineContext::set($this->contextKey($manager->connection('second')), $second); + CoroutineContext::set($this->contextKey($manager->connection('third')), $third); try { $manager->releaseConnections(); @@ -197,10 +186,8 @@ public function testDiscardConnectionsExhaustsEveryCreatedProxy(): void $second = m::mock(PhpRedisConnection::class); $second->expects('discard'); $manager = $this->createManager(['first', 'second']); - $manager->connection('first'); - $manager->connection('second'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'first', $first); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'second', $second); + CoroutineContext::set($this->contextKey($manager->connection('first')), $first); + CoroutineContext::set($this->contextKey($manager->connection('second')), $second); $manager->discardConnections(); } @@ -210,13 +197,12 @@ public function testPurgeClosesPoolAfterDiscardFailureAndPreservesFirstFailure() $discardException = new RuntimeException('Discard failed.'); $connection = m::mock(PhpRedisConnection::class); $connection->expects('discard')->andThrow($discardException); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('purge') ->with('alias') ->andThrow(new RuntimeException('Purge failed.')); $manager = $this->createManager(['alias'], poolManager: $poolManager); - $manager->connection('alias'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'alias', $connection); + CoroutineContext::set($this->contextKey($manager->connection('alias')), $connection); try { $manager->purge('alias'); @@ -233,13 +219,12 @@ public function testPurgeClosesPoolAndLetsCancellationSupersedeDiscardFailure(): $cancellation = new CanceledException('Purge canceled.'); $connection = m::mock(PhpRedisConnection::class); $connection->expects('discard')->andThrow(new RuntimeException('Discard failed.')); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('purge') ->with('alias') ->andThrow($cancellation); $manager = $this->createManager(['alias'], poolManager: $poolManager); - $manager->connection('alias'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'alias', $connection); + CoroutineContext::set($this->contextKey($manager->connection('alias')), $connection); try { $manager->purge('alias'); @@ -412,7 +397,7 @@ private function createManager( ?PoolManager $poolManager = null ): RedisManager { $app = m::mock(ContainerContract::class); - $poolManager ??= m::mock(PoolManager::class); + $poolManager ??= m::mock(PoolManager::class)->makePartial(); $config = $this->createRedisConfig($configuredConnections); return new RedisManager( @@ -423,6 +408,14 @@ private function createManager( ); } + /** + * Get the coroutine context key of the given proxy's pinned connection. + */ + private function contextKey(RedisProxy $redis): string + { + return (fn (): string => $this->getContextKey())->call($redis); + } + /** * Create a RedisConfig mock that validates connection names. * diff --git a/tests/Redis/RedisPoolHeartbeatTest.php b/tests/Redis/RedisPoolHeartbeatTest.php index d57a31197d..bb89fa1a7f 100644 --- a/tests/Redis/RedisPoolHeartbeatTest.php +++ b/tests/Redis/RedisPoolHeartbeatTest.php @@ -799,7 +799,7 @@ public function testPinnedConnectionDoesNotRecycleExpiredGenerationMidBorrow(): $redis = $this->createProxy($pool); $redis->withPinnedConnection(function () use ($redis, $connection) { - $contextConnection = CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'heartbeat_test'); + $contextConnection = CoroutineContext::get((fn (): string => $this->getContextKey())->call($redis)); $this->assertSame($connection, $contextConnection); $this->assertSame(2, $connection->reconnectCount); @@ -883,7 +883,7 @@ protected function createPool(array $poolOptions = [], string $poolClass = Inspe protected function createProxy(RedisPool $pool): RedisProxy { - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('heartbeat_test')->andReturn($pool); return new RedisProxy( diff --git a/tests/Redis/RedisProxyNonCoroutineTest.php b/tests/Redis/RedisProxyNonCoroutineTest.php index 6f18effbc1..e663ec5548 100644 --- a/tests/Redis/RedisProxyNonCoroutineTest.php +++ b/tests/Redis/RedisProxyNonCoroutineTest.php @@ -18,13 +18,6 @@ class RedisProxyNonCoroutineTest extends TestCase { protected bool $runTestsInCoroutine = false; - protected function tearDown(): void - { - CoroutineContext::forget(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'); - - parent::tearDown(); - } - public function testMultiPinsConnectionUntilTerminalCleanup(): void { $this->assertCommandPinsConnection('multi', [], new stdClass); @@ -62,24 +55,20 @@ private function assertCommandPinsConnection( $pool = m::mock(RedisPool::class); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); $redis = new RedisProxy( $poolManager, 'default', m::mock(RedisSentinelFactory::class), ); + $contextKey = (fn (): string => $this->getContextKey())->call($redis); $this->assertSame($result, $redis->{$command}(...$arguments)); - $this->assertSame( - $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default') - ); + $this->assertSame($connection, CoroutineContext::get($contextKey)); $redis->releaseContextConnection(); - $this->assertFalse( - CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default') - ); + $this->assertFalse(CoroutineContext::has($contextKey)); } } diff --git a/tests/Redis/RedisProxyTest.php b/tests/Redis/RedisProxyTest.php index 337e1c2b80..f69569551d 100644 --- a/tests/Redis/RedisProxyTest.php +++ b/tests/Redis/RedisProxyTest.php @@ -50,13 +50,6 @@ */ class RedisProxyTest extends TestCase { - protected function tearDown(): void - { - parent::tearDown(); - CoroutineContext::forget(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'); - CoroutineContext::forget(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'cache'); - } - public function testCommandIsProxiedToConnection(): void { $connection = $this->mockConnection(); @@ -72,7 +65,7 @@ public function testCommandIsProxiedToConnection(): void public function testMacroRegistrationMethodsDoNotCheckoutRedis(): void { - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->never(); $redis = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); @@ -95,7 +88,7 @@ protected function farewell(): callable public function testMixedCaseSubscriptionsUseDedicatedProxyRoute(): void { - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->never(); $redis = new class($poolManager, 'default', $this->sentinelFactory()) extends RedisProxy { public array $subscriptions = []; @@ -291,7 +284,7 @@ public function testMixedCaseMultiStoresConnectionInContext(): void $this->assertSame($multiInstance, $result); // Connection should be stored in context - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testMixedCasePipelineStoresConnectionInContext(): void @@ -308,7 +301,7 @@ public function testMixedCasePipelineStoresConnectionInContext(): void $result = $redis->__call('PIPELINE', []); $this->assertSame($pipelineInstance, $result); - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testMixedCaseSelectStoresConnectionInContext(): void @@ -323,7 +316,7 @@ public function testMixedCaseSelectStoresConnectionInContext(): void $result = $redis->__call('SELECT', [1]); $this->assertTrue($result); - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testConnectionIsStoredInContextForSelectZeroDatabase(): void @@ -337,7 +330,7 @@ public function testConnectionIsStoredInContextForSelectZeroDatabase(): void $result = $redis->select(0); $this->assertTrue($result); - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testMixedCaseWatchStoresConnectionInContext(): void @@ -351,7 +344,7 @@ public function testMixedCaseWatchStoresConnectionInContext(): void $this->assertTrue($redis->__call('WATCH', ['key'])); $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default') + CoroutineContext::get($this->contextKey($redis)) ); } @@ -361,14 +354,13 @@ public function testNativeDiscardDoesNotInvokePoolDiscard(): void $connection->shouldReceive('discardTransaction')->once()->andReturn(true); $connection->shouldReceive('discard')->never(); $connection->shouldReceive('release')->never(); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); - $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); $this->assertTrue($redis->__call('DISCARD', [])); $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default') + CoroutineContext::get($this->contextKey($redis)) ); } @@ -451,22 +443,19 @@ public function testCopiedSiblingContextsBorrowDistinctPinnedConnectionsAndOwnTh $secondChildConnection, ); $redis->multi(); + $contextKey = $this->contextKey($redis); $childrenReady = new Channel(2); $releaseChildren = new Channel(2); $childCoroutineIds = [ - go(static function () use ($redis, $childrenReady, $releaseChildren): void { + go(static function () use ($redis, $contextKey, $childrenReady, $releaseChildren): void { $redis->multi(); - $childrenReady->push(CoroutineContext::get( - RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', - )); + $childrenReady->push(CoroutineContext::get($contextKey)); $releaseChildren->pop(); }, copyContext: true), - go(static function () use ($redis, $childrenReady, $releaseChildren): void { + go(static function () use ($redis, $contextKey, $childrenReady, $releaseChildren): void { $redis->multi(); - $childrenReady->push(CoroutineContext::get( - RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', - )); + $childrenReady->push(CoroutineContext::get($contextKey)); $releaseChildren->pop(); }, copyContext: true), ]; @@ -475,9 +464,7 @@ public function testCopiedSiblingContextsBorrowDistinctPinnedConnectionsAndOwnTh $firstBorrowedConnection = $childrenReady->pop(1.0); $secondBorrowedConnection = $childrenReady->pop(1.0); - $this->assertSame($parentConnection, CoroutineContext::get( - RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', - )); + $this->assertSame($parentConnection, CoroutineContext::get($contextKey)); $this->assertNotSame($parentConnection, $firstBorrowedConnection); $this->assertNotSame($parentConnection, $secondBorrowedConnection); $this->assertNotSame($firstBorrowedConnection, $secondBorrowedConnection); @@ -511,18 +498,17 @@ public function testDetachedCopiedChildBorrowsAfterItsParentReleases(): void }); $redis = $this->createCountingRedis($parentConnection, $childConnection); + $contextKey = $this->contextKey($redis); $allowChild = new Channel(1); $childBorrowed = new Channel(1); $childCoroutineId = new Channel(1); - $parentCoroutineId = go(static function () use ($redis, $allowChild, $childBorrowed, $childCoroutineId): void { + $parentCoroutineId = go(static function () use ($redis, $contextKey, $allowChild, $childBorrowed, $childCoroutineId): void { $redis->multi(); - $childCoroutineId->push(go(static function () use ($redis, $allowChild, $childBorrowed): void { + $childCoroutineId->push(go(static function () use ($redis, $contextKey, $allowChild, $childBorrowed): void { $allowChild->pop(); $redis->multi(); - $childBorrowed->push(CoroutineContext::get( - RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', - )); + $childBorrowed->push(CoroutineContext::get($contextKey)); }, copyContext: true)); }); @@ -572,7 +558,7 @@ public function testSelectPinnedConnectionDoesNotLeakAcrossCoroutines(): void ); $pool->shouldReceive('getOptions')->andReturn(PoolOptions::fromArray([])); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); $redis = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); @@ -607,7 +593,7 @@ public function testPinnedConnectionInOneCoroutineIsNotReusedInAnotherCoroutine( $pool->shouldReceive('borrow')->times(2)->andReturn($pinnedConnection, $otherCoroutineConnection); $pool->shouldReceive('getOptions')->andReturn(PoolOptions::fromArray([])); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); $redis = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); @@ -633,11 +619,11 @@ public function testExistingContextConnectionIsReused(): void // but allow release() call for test cleanup $connection->shouldReceive('release')->zeroOrMoreTimes(); - // Pre-set connection in context - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); - $redis = $this->createRedis($connection); + // Pre-set connection in context + CoroutineContext::set($this->contextKey($redis), $connection); + // Both calls should use the same connection from context $result1 = $redis->get('key1'); $result2 = $redis->get('key2'); @@ -687,11 +673,11 @@ public function testExceptionWithContextConnectionDoesNotReleaseConnection(): vo $mockRedisConnection = $this->createMockRedisConnection('get', null, $expectedException); $mockRedisConnection->shouldReceive('release')->never(); - // Pre-set context connection - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $mockRedisConnection); - $redis = $this->createRedis($mockRedisConnection); + // Pre-set context connection + CoroutineContext::set($this->contextKey($redis), $mockRedisConnection); + try { $redis->get('key'); $this->fail('Expected exception was not thrown'); @@ -718,7 +704,7 @@ public function testExceptionWithSameConnectionCommandReleasesConnectionInsteadO } // Connection should NOT be stored in context on error - $this->assertNull(CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertNull(CoroutineContext::get($this->contextKey($redis))); } public function testEventDispatchedOnSuccess(): void @@ -751,41 +737,43 @@ public function testSuccessEventTemporarilyPublishesTheOwnedConnection(): void { $dispatcher = m::mock(Dispatcher::class); $connection = $this->createMockRedisConnection('get', 'value', eventDispatcher: $dispatcher); + $redis = $this->createRedis($connection); $dispatcher->expects('hasListeners')->with(CommandExecuted::class)->andReturnTrue(); $dispatcher->expects('dispatch') ->with(m::type(CommandExecuted::class)) - ->andReturnUsing(function () use ($connection): void { + ->andReturnUsing(function () use ($connection, $redis): void { $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'), + CoroutineContext::get($this->contextKey($redis)), ); }); $connection->expects('release'); - $this->assertSame('value', $this->createRedis($connection)->get('key')); - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertSame('value', $redis->get('key')); + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); } public function testSuccessEventPreservesAPreExistingContextConnection(): void { $dispatcher = m::mock(Dispatcher::class); $connection = $this->createMockRedisConnection('get', 'value', eventDispatcher: $dispatcher); + $redis = $this->createRedis($connection); $dispatcher->expects('hasListeners')->with(CommandExecuted::class)->andReturnTrue(); $dispatcher->expects('dispatch') ->with(m::type(CommandExecuted::class)) - ->andReturnUsing(function () use ($connection): void { + ->andReturnUsing(function () use ($connection, $redis): void { $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'), + CoroutineContext::get($this->contextKey($redis)), ); }); $connection->shouldNotReceive('release'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + CoroutineContext::set($this->contextKey($redis), $connection); - $this->assertSame('value', $this->createRedis($connection)->get('key')); + $this->assertSame('value', $redis->get('key')); $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'), + CoroutineContext::get($this->contextKey($redis)), ); } @@ -807,7 +795,7 @@ public function testSuccessEventCanRunANestedCommandOnTheOwnedConnection(): void }); $this->assertSame('outer', $redis->get('outer')); - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); } public function testEventDispatchedOnErrorWithExceptionInfo(): void @@ -895,25 +883,26 @@ public function testFailureEventTemporarilyPublishesTheOwnedConnection(): void $commandException = new RuntimeException('Command failed.'); $dispatcher = m::mock(Dispatcher::class); $connection = $this->createMockRedisConnection('get', exception: $commandException, eventDispatcher: $dispatcher); + $redis = $this->createRedis($connection); $dispatcher->expects('hasListeners')->with(CommandFailed::class)->andReturnTrue(); $dispatcher->expects('dispatch') ->with(m::type(CommandFailed::class)) - ->andReturnUsing(function () use ($connection): void { + ->andReturnUsing(function () use ($connection, $redis): void { $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'), + CoroutineContext::get($this->contextKey($redis)), ); }); $connection->expects('release'); try { - $this->createRedis($connection)->get('key'); + $redis->get('key'); $this->fail('Expected the command failure to propagate.'); } catch (RuntimeException $throwable) { $this->assertSame($commandException, $throwable); } - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); } public function testFailureEventCanRunANestedCommandOnTheOwnedConnection(): void @@ -939,7 +928,7 @@ public function testFailureEventCanRunANestedCommandOnTheOwnedConnection(): void $this->assertSame($commandException, $throwable); } - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); } public function testMultiListenerQueuesNestedCommandsOnTheOwnedTransaction(): void @@ -963,7 +952,7 @@ public function testMultiListenerQueuesNestedCommandsOnTheOwnedTransaction(): vo $this->assertSame($transaction, $redis->multi()); $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'), + CoroutineContext::get($this->contextKey($redis)), ); } @@ -975,15 +964,16 @@ public function testThrowingSuccessListenerStillReleasesOrdinaryConnection(): vo $dispatcher->expects('dispatch')->andThrow($eventException); $connection = $this->createMockRedisConnection('get', 'value', null, $dispatcher); $connection->expects('release'); + $redis = $this->createRedis($connection); try { - $this->createRedis($connection)->get('key'); + $redis->get('key'); $this->fail('Expected the event listener failure to propagate.'); } catch (RuntimeException $throwable) { $this->assertSame($eventException, $throwable); } - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); } public function testThrowingSuccessListenerDoesNotSkipSameConnectionHandoff(): void @@ -1006,7 +996,7 @@ public function testThrowingSuccessListenerDoesNotSkipSameConnectionHandoff(): v $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default') + CoroutineContext::get($this->contextKey($redis)) ); } @@ -1019,15 +1009,16 @@ public function testThrowingFailureListenerStillReleasesAndReplacesCommandFailur $dispatcher->expects('dispatch')->andThrow($eventException); $connection = $this->createMockRedisConnection('get', null, $commandException, $dispatcher); $connection->expects('release'); + $redis = $this->createRedis($connection); try { - $this->createRedis($connection)->get('key'); + $redis->get('key'); $this->fail('Expected the event listener failure to propagate.'); } catch (RuntimeException $throwable) { $this->assertSame($eventException, $throwable); } - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); } public function testCommandFailureRemainsPrimaryOverCleanupFailure(): void @@ -1114,11 +1105,12 @@ public function testCallbackTransactionClearsWatchStateAfterSuccessfulExec(): vo $connection->expects('multi')->andReturn($transaction); $connection->expects('clearWatchState'); $connection->shouldNotReceive('release'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); $this->assertSame( [], - $this->createRedis($connection)->transaction(static function (): void { + $redis->transaction(static function (): void { }) ); } @@ -1131,10 +1123,11 @@ public function testCallbackTransactionClearsWatchStateAfterOptimisticLockConfli $connection->expects('multi')->andReturn($transaction); $connection->expects('clearWatchState'); $connection->shouldNotReceive('release'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); $this->assertFalse( - $this->createRedis($connection)->transaction(static function (): void { + $redis->transaction(static function (): void { }) ); } @@ -1148,12 +1141,13 @@ public function testCallbackTransactionInvalidatesWithoutClearingWatchStateWhenE $connection->expects('invalidate'); $connection->shouldNotReceive('clearWatchState'); $connection->shouldNotReceive('release'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); $this->expectException(RuntimeException::class); $this->expectExceptionMessageIs('Exec failed.'); - $this->createRedis($connection)->transaction(static function (): void { + $redis->transaction(static function (): void { }); } @@ -1242,11 +1236,12 @@ public function testCallbackPipelineDoesNotClearWatchState(): void $connection->expects('pipeline')->andReturn($pipeline); $connection->shouldNotReceive('clearWatchState'); $connection->shouldNotReceive('release'); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); + $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); $this->assertSame( [], - $this->createRedis($connection)->pipeline(static function (): void { + $redis->pipeline(static function (): void { }) ); } @@ -1254,20 +1249,19 @@ public function testCallbackPipelineDoesNotClearWatchState(): void public function testRegularCommandDoesNotStoreConnectionInContext(): void { $mockRedisConnection = $this->mockConnection(); + $mockRedisConnection->shouldReceive('release')->once(); + $redis = $this->createRedis($mockRedisConnection); $mockRedisConnection->expects('get') ->with('key') - ->andReturnUsing(function (): string { - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + ->andReturnUsing(function () use ($redis): string { + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); return 'value'; }); - $mockRedisConnection->shouldReceive('release')->once(); - - $redis = $this->createRedis($mockRedisConnection); $redis->get('key'); - $this->assertNull(CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertNull(CoroutineContext::get($this->contextKey($redis))); } public function testWithConnectionExecutesCallbackAndReleasesConnection(): void @@ -1292,11 +1286,11 @@ public function testWithConnectionReusesExistingContextConnection(): void // Should NOT release since connection was already in context $connection->shouldReceive('release')->never(); - // Pre-set connection in context (simulating an active multi/pipeline) - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); - $redis = $this->createRedis($connection); + // Pre-set connection in context (simulating an active multi/pipeline) + CoroutineContext::set($this->contextKey($redis), $connection); + $result = $redis->withConnection(function (RedisConnection $redisConnection) use ($connection) { $this->assertSame($connection, $redisConnection); @@ -1305,7 +1299,7 @@ public function testWithConnectionReusesExistingContextConnection(): void $this->assertSame('reused-connection', $result); // Connection should still be in context - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testWithConnectionReleasesOnException(): void @@ -1389,10 +1383,10 @@ public function testWithConnectionDoesNotReleaseContextConnectionOnException(): // Should NOT release since connection was in context $connection->shouldReceive('release')->never(); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); - $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); + try { $redis->withConnection(function (RedisConnection $redisConnection) { throw new RuntimeException('Callback failed'); @@ -1403,7 +1397,7 @@ public function testWithConnectionDoesNotReleaseContextConnectionOnException(): } // Connection should still be in context - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testWithConnectionDefaultsToTransformTrue(): void @@ -1501,32 +1495,31 @@ public function testWithoutSerializationOrCompressionPinsConnectionAndDelegates( $redis = $this->createRedis($connection); - $result = $redis->withoutSerializationOrCompression(function () use ($connection) { + $result = $redis->withoutSerializationOrCompression(function () use ($connection, $redis) { // Connection must be pinned in Context during callback - $this->assertSame($connection, CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertSame($connection, CoroutineContext::get($this->contextKey($redis))); return 'result'; }); $this->assertSame('result', $result); // Connection should be unpinned after completion - $this->assertNull(CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertNull(CoroutineContext::get($this->contextKey($redis))); } public function testWithPinnedConnectionPreservesCallbackCancellationAndUnpinsBeforeCleanup(): void { $cancellation = new CanceledException('Callback canceled.'); $connection = $this->mockConnection(); - $connection->expects('release')->andReturnUsing(function (): void { - $this->assertFalse(CoroutineContext::has( - RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', - )); + $redis = $this->createRedis($connection); + $connection->expects('release')->andReturnUsing(function () use ($redis): void { + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); throw new RuntimeException('Cleanup failed.'); }); try { - $this->createRedis($connection)->withPinnedConnection( + $redis->withPinnedConnection( static function () use ($cancellation): never { throw $cancellation; }, @@ -1536,9 +1529,7 @@ static function () use ($cancellation): never { $this->assertSame($cancellation, $throwable); } - $this->assertFalse(CoroutineContext::has( - RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', - )); + $this->assertFalse(CoroutineContext::has($this->contextKey($redis))); } public function testWithoutSerializationOrCompressionReusesExistingContextConnection(): void @@ -1550,21 +1541,21 @@ public function testWithoutSerializationOrCompressionReusesExistingContextConnec // Should NOT release since connection was already in context $connection->shouldReceive('release')->never(); - // Pre-set connection in context (simulating an active multi/pipeline) - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); - $redis = $this->createRedis($connection); - $result = $redis->withoutSerializationOrCompression(function () use ($connection) { + // Pre-set connection in context (simulating an active multi/pipeline) + CoroutineContext::set($this->contextKey($redis), $connection); + + $result = $redis->withoutSerializationOrCompression(function () use ($connection, $redis) { // Connection should still be the same one that was pre-set - $this->assertSame($connection, CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertSame($connection, CoroutineContext::get($this->contextKey($redis))); return 'reused'; }); $this->assertSame('reused', $result); // Connection should still be in context - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testWithoutSerializationOrCompressionCleansUpOnException(): void @@ -1580,9 +1571,9 @@ public function testWithoutSerializationOrCompressionCleansUpOnException(): void $redis = $this->createRedis($connection); try { - $redis->withoutSerializationOrCompression(function () use ($connection) { + $redis->withoutSerializationOrCompression(function () use ($connection, $redis) { // Connection must be pinned even when callback throws - $this->assertSame($connection, CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertSame($connection, CoroutineContext::get($this->contextKey($redis))); throw new RuntimeException('Callback failed'); }); @@ -1592,7 +1583,7 @@ public function testWithoutSerializationOrCompressionCleansUpOnException(): void } // Connection should be unpinned and released - $this->assertNull(CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertNull(CoroutineContext::get($this->contextKey($redis))); } public function testWithoutSerializationOrCompressionDoesNotReleaseContextConnectionOnException(): void @@ -1606,14 +1597,14 @@ public function testWithoutSerializationOrCompressionDoesNotReleaseContextConnec // Should NOT release since connection was already in context $connection->shouldReceive('release')->never(); - CoroutineContext::set(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default', $connection); - $redis = $this->createRedis($connection); + CoroutineContext::set($this->contextKey($redis), $connection); + try { - $redis->withoutSerializationOrCompression(function () use ($connection) { + $redis->withoutSerializationOrCompression(function () use ($connection, $redis) { // Connection should still be pinned during callback - $this->assertSame($connection, CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertSame($connection, CoroutineContext::get($this->contextKey($redis))); throw new RuntimeException('Callback failed'); }); @@ -1623,7 +1614,7 @@ public function testWithoutSerializationOrCompressionDoesNotReleaseContextConnec } // Connection should still be in context - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($redis))); } public function testSubscriberUsesTheCompleteStandaloneConfiguration(): void @@ -1640,7 +1631,7 @@ public function testSubscriberUsesTheCompleteStandaloneConfiguration(): void 'options' => ['prefix' => 'app:'], ])); $pool->shouldNotReceive('borrow'); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); $subscriber = (new RedisProxy( $poolManager, @@ -1682,7 +1673,7 @@ public function testSubscriberResolvesSentinelMasterFreshWithConnectionCredentia $pool = m::mock(RedisPool::class); $pool->expects('getConfig')->twice()->andReturn($config); $pool->shouldNotReceive('borrow'); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->twice()->with('default')->andReturn($pool); $sentinelFactory = m::mock(RedisSentinelFactory::class); $sentinelFactory->expects('resolveMaster') @@ -1741,7 +1732,7 @@ public function testClusterSubscriberUsesConnectionTransportAndReleasesDiscovery $pool = m::mock(RedisPool::class); $pool->expects('getConfig')->andReturn($config); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); $subscriber = (new RedisProxy( $poolManager, @@ -1804,7 +1795,7 @@ public function testClusterSubscriberUsesTlsConnectionTransportForMaster(): void $pool = m::mock(RedisPool::class); $pool->expects('getConfig')->andReturn($config); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); $subscriber = (new RedisProxy( $poolManager, @@ -1841,7 +1832,7 @@ public function testClusterSubscriberAggregatesEndpointFailures(): void $pool = m::mock(RedisPool::class); $pool->expects('getConfig')->andReturn($config); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); try { @@ -1872,7 +1863,7 @@ public function testClusterDiscoveryFailureRemainsPrimaryOverReleaseFailure(): v ], ]); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); try { @@ -1898,7 +1889,7 @@ public function testPinnedClusterDiscoveryFailureKeepsTheCallersConnection(): vo $pool = m::mock(RedisPool::class); $pool->expects('getConfig')->andReturn($this->clusterConfig()); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->allows('pool')->with('default')->andReturn($pool); $redis = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); @@ -1912,7 +1903,7 @@ public function testPinnedClusterDiscoveryFailureKeepsTheCallersConnection(): vo $this->assertSame( $connection, - CoroutineContext::get(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default'), + CoroutineContext::get($this->contextKey($redis)), ); }); } @@ -1932,7 +1923,7 @@ public function testClusterDiscoveryNormalizesWrappedCancellationAndStillRelease ], ]); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); $redis = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); @@ -1964,7 +1955,7 @@ public function testClusterDiscoveryReleaseCancellationSupersedesOrdinaryDiscove ], ]); $pool->expects('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->expects('pool')->with('default')->andReturn($pool); try { @@ -2057,7 +2048,7 @@ public function testIsClusterReturnsFalseForStandardConfig(): void ]); $pool->shouldReceive('borrow')->never(); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); $redis = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); @@ -2073,7 +2064,7 @@ public function testIsClusterReturnsTrueForClusterConfig(): void ]); $pool->shouldReceive('borrow')->never(); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('cache')->andReturn($pool); $proxy = new RedisProxy($poolManager, 'cache', $this->sentinelFactory()); @@ -2090,7 +2081,7 @@ public function testProxyUsesSpecifiedPoolName(): void $cachePool = m::mock(RedisPool::class); $cachePool->shouldReceive('borrow')->andReturn($cacheConnection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('cache')->andReturn($cachePool); $proxy = new RedisProxy($poolManager, 'cache', $this->sentinelFactory()); @@ -2110,16 +2101,76 @@ public function testProxyContextKeyUsesPoolName(): void $pool = m::mock(RedisPool::class); $pool->shouldReceive('borrow')->andReturn($connection); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('cache')->andReturn($pool); $proxy = new RedisProxy($poolManager, 'cache', $this->sentinelFactory()); + $defaultProxy = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); $proxy->pipeline(); // Context key should use the pool name - $this->assertTrue(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'cache')); - $this->assertFalse(CoroutineContext::has(RedisProxy::CONNECTION_CONTEXT_PREFIX . 'default')); + $this->assertTrue(CoroutineContext::has($this->contextKey($proxy))); + $this->assertFalse(CoroutineContext::has($this->contextKey($defaultProxy))); + } + + public function testSameNamedConnectionsOnDifferentPoolManagersKeepSeparatePins(): void + { + $firstConnection = $this->mockConnection(); + $firstConnection->expects('multi')->andReturn(m::mock(PhpRedis::class)); + $firstConnection->expects('get')->with('key')->andReturn('first'); + $firstConnection->expects('release'); + $secondConnection = $this->mockConnection(); + $secondConnection->expects('multi')->andReturn(m::mock(PhpRedis::class)); + $secondConnection->expects('discard'); + $first = $this->createRedis($firstConnection); + $second = $this->createRedis($secondConnection); + + $first->multi(); + $second->multi(); + $second->discardContextConnection(); + + $this->assertSame('first', $first->get('key')); + } + + public function testProxiesForTheSamePoolManagerAndNameSharePins(): void + { + $connection = $this->mockConnection(); + $connection->expects('multi')->andReturn(m::mock(PhpRedis::class)); + $connection->expects('get')->with('key')->andReturn('value'); + $connection->expects('release'); + $pool = m::mock(RedisPool::class); + $pool->expects('borrow')->andReturn($connection); + $pool->shouldReceive('getOptions')->andReturn(PoolOptions::fromArray([])); + $poolManager = m::mock(PoolManager::class)->makePartial(); + $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); + $first = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); + $second = new RedisProxy($poolManager, 'default', $this->sentinelFactory()); + + $first->multi(); + + $this->assertSame('value', $second->get('key')); + } + + public function testDeferredCleanupReleasesEachPoolManagersPinnedConnection(): void + { + $firstConnection = $this->mockConnection(); + $firstConnection->expects('multi')->andReturn(m::mock(PhpRedis::class)); + $firstConnection->expects('release'); + $secondConnection = $this->mockConnection(); + $secondConnection->expects('multi')->andReturn(m::mock(PhpRedis::class)); + $secondConnection->expects('release'); + $first = $this->createRedis($firstConnection); + $second = $this->createRedis($secondConnection); + + $coroutineId = go(static function () use ($first, $second): void { + $first->multi(); + $second->multi(); + }); + + Coroutine::join([$coroutineId], 1.0); + + $this->assertFalse(Coroutine::exists($coroutineId)); } /** @@ -2144,18 +2195,26 @@ private function createRedis(m\MockInterface|RedisConnection $connection): Redis $pool->shouldReceive('borrow')->andReturn($connection); $pool->shouldReceive('getOptions')->andReturn(PoolOptions::fromArray([])); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); return new RedisProxy($poolManager, 'default', $this->sentinelFactory()); } + /** + * Get the coroutine context key of the given proxy's pinned connection. + */ + private function contextKey(RedisProxy $redis): string + { + return (fn (): string => $this->getContextKey())->call($redis); + } + /** * Create a Redis proxy that returns the given subscriber. */ private function createRedisWithSubscriber(Subscriber $subscriber): RedisProxy { - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldNotReceive('pool'); return new class($poolManager, 'default', $this->sentinelFactory(), $subscriber) extends RedisProxy { @@ -2230,7 +2289,7 @@ private function createCountingRedis( $pool->shouldReceive('borrow')->andReturn(...$connections); $pool->shouldReceive('getOptions')->andReturn(PoolOptions::fromArray([])); - $poolManager = m::mock(PoolManager::class); + $poolManager = m::mock(PoolManager::class)->makePartial(); $poolManager->shouldReceive('pool')->with('default')->andReturn($pool); return new RedisProxyReleaseCountingStub( @@ -2371,9 +2430,7 @@ class RedisProxyReleaseCountingStub extends RedisProxy public function releaseContextConnection(): void { - $hasContextConnection = CoroutineContext::has( - self::CONNECTION_CONTEXT_PREFIX . $this->getName() - ); + $hasContextConnection = CoroutineContext::has($this->getContextKey()); parent::releaseContextConnection(); diff --git a/tests/Reverb/PackageMetadataTest.php b/tests/Reverb/PackageMetadataTest.php index 44bd5761f3..9494146f09 100644 --- a/tests/Reverb/PackageMetadataTest.php +++ b/tests/Reverb/PackageMetadataTest.php @@ -25,6 +25,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void foreach ([ 'ext-swoole', + 'hypervel/attributes', 'hypervel/bus', 'hypervel/collections', 'hypervel/console', diff --git a/tests/Saloon/PackageMetadataTest.php b/tests/Saloon/PackageMetadataTest.php index 8e65e6fc9d..8259e9ff4d 100644 --- a/tests/Saloon/PackageMetadataTest.php +++ b/tests/Saloon/PackageMetadataTest.php @@ -30,6 +30,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'hypervel/guzzle', 'guzzlehttp/psr7', 'guzzlehttp/uri-template', + 'hypervel/attributes', 'hypervel/cache', 'hypervel/collections', 'hypervel/conditionable', @@ -37,6 +38,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'hypervel/container', 'hypervel/contracts', 'hypervel/coroutine', + 'hypervel/database', 'hypervel/events', 'hypervel/filesystem', 'hypervel/foundation', diff --git a/tests/Sanctum/GuardTest.php b/tests/Sanctum/GuardTest.php index ff15d7add8..f1861f7610 100644 --- a/tests/Sanctum/GuardTest.php +++ b/tests/Sanctum/GuardTest.php @@ -546,6 +546,25 @@ function () use ($firstGuard, $secondUser): mixed { $this->assertSame($firstUser, $firstGuard->user()); } + public function testNewGuardWithTheSameNameStartsWithoutThePreviousGuardsUsers(): void + { + [$tokenUser, $token, $plainToken] = $this->createUserWithToken(); + $provider = $this->app->make('auth')->createUserProvider('users'); + $this->assertNotNull($provider); + RequestContext::set(Request::create('/', server: [ + 'HTTP_AUTHORIZATION' => 'Bearer ' . $plainToken, + ])); + $previousGuard = new SanctumGuard('sanctum', $provider, $this->app, []); + $this->assertTrue($tokenUser->is($previousGuard->user())); + $previousGuard->setUser($tokenUser); + $token->delete(); + + $guard = new SanctumGuard('sanctum', $provider, $this->app, []); + + $this->assertFalse($guard->hasUser()); + $this->assertNull($guard->user()); + } + public function testAuthContextKeysIncludeOnlyDurableExplicitState(): void { $provider = $this->app->make('auth')->createUserProvider('users'); @@ -556,7 +575,7 @@ public function testAuthContextKeysIncludeOnlyDurableExplicitState(): void ])); $this->assertSame([ - '__auth.guards.sanctum.user.explicit', + (fn (): string => $this->getExplicitUserContextKey())->call($guard), ], $guard->getAuthContextKeys()); } diff --git a/tests/Scout/PackageMetadataTest.php b/tests/Scout/PackageMetadataTest.php index 2db2df8417..f821860c95 100644 --- a/tests/Scout/PackageMetadataTest.php +++ b/tests/Scout/PackageMetadataTest.php @@ -31,7 +31,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void foreach ([ 'ext-filter', - 'hypervel/guzzle', + 'hypervel/attributes', 'hypervel/collections', 'hypervel/conditionable', 'hypervel/config', @@ -42,6 +42,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'hypervel/coroutine', 'hypervel/database', 'hypervel/foundation', + 'hypervel/guzzle', 'hypervel/macroable', 'hypervel/pagination', 'hypervel/queue', diff --git a/tests/Session/PackageMetadataTest.php b/tests/Session/PackageMetadataTest.php index 739b48f0c5..86bc4df8a2 100644 --- a/tests/Session/PackageMetadataTest.php +++ b/tests/Session/PackageMetadataTest.php @@ -27,6 +27,7 @@ public function testRuntimePackageRequirementsAreDeclared(): void 'ext-ctype', 'ext-mbstring', 'ext-session', + 'hypervel/attributes', 'hypervel/auth', 'hypervel/cache', 'hypervel/collections', diff --git a/tests/Support/SupportFacadesQueueTest.php b/tests/Support/SupportFacadesQueueTest.php index f1dd42d4c7..ef75601037 100644 --- a/tests/Support/SupportFacadesQueueTest.php +++ b/tests/Support/SupportFacadesQueueTest.php @@ -8,12 +8,14 @@ use Hypervel\Container\Container; use Hypervel\Contracts\Queue\Factory as QueueContract; use Hypervel\Queue\QueueManager; +use Hypervel\Queue\Worker; use Hypervel\Support\Facades\Facade; use Hypervel\Support\Facades\Queue; use Hypervel\Support\Testing\Fakes\QueueFake; use Hypervel\Tests\TestCase; use Mockery as m; use Mockery\MockInterface; +use ReflectionProperty; class SupportFacadesQueueTest extends TestCase { @@ -74,15 +76,6 @@ public function testFakeExceptFor(): void $this->assertSame($this->queueManager, Queue::getFacadeRoot()); } - public function testFakeExceptForSwapsQueueManager(): void - { - Queue::fakeExceptFor(function (): void { - $this->assertInstanceOf(QueueFake::class, Queue::getFacadeRoot()); - }, []); - - $this->assertSame($this->queueManager, Queue::getFacadeRoot()); - } - public function testFakeExceptForReturnValue(): void { $result = Queue::fakeExceptFor(function (): string { @@ -100,6 +93,20 @@ public function testFakeForReturnValue(): void $this->assertSame('test-result', $result); } + + public function testPopUsingRegistersAndRemovesWorkerCallback(): void + { + $callbacks = new ReflectionProperty(Worker::class, 'popCallbacks'); + $callback = static fn (): null => null; + + Queue::popUsing('myworker', $callback); + + $this->assertSame(['myworker' => $callback], $callbacks->getValue()); + + Queue::popUsing('myworker', null); + + $this->assertSame([], $callbacks->getValue()); + } } class QueueJobStub diff --git a/tests/Telescope/PackageMetadataTest.php b/tests/Telescope/PackageMetadataTest.php index 7bbcc8c182..095a1aaf8f 100644 --- a/tests/Telescope/PackageMetadataTest.php +++ b/tests/Telescope/PackageMetadataTest.php @@ -31,6 +31,7 @@ public function testDirectDependenciesAndProviderAreDeclared(): void ); $hypervelDependencies = [ + 'hypervel/attributes', 'hypervel/auth', 'hypervel/broadcasting', 'hypervel/bus', @@ -44,6 +45,7 @@ public function testDirectDependenciesAndProviderAreDeclared(): void 'hypervel/database', 'hypervel/di', 'hypervel/events', + 'hypervel/filesystem', 'hypervel/foundation', 'hypervel/http', 'hypervel/http-server', @@ -52,6 +54,8 @@ public function testDirectDependenciesAndProviderAreDeclared(): void 'hypervel/notifications', 'hypervel/queue', 'hypervel/redis', + 'hypervel/reflection', + 'hypervel/sentinel', 'hypervel/support', 'hypervel/view', ]; diff --git a/tests/Validation/PackageMetadataTest.php b/tests/Validation/PackageMetadataTest.php index 8963f6cb8b..d7169b3003 100644 --- a/tests/Validation/PackageMetadataTest.php +++ b/tests/Validation/PackageMetadataTest.php @@ -28,6 +28,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void 'ext-mbstring', 'brick/math', 'egulias/email-validator', + 'hypervel/attributes', 'hypervel/collections', 'hypervel/conditionable', 'hypervel/console', diff --git a/tests/WebSocketServer/PackageMetadataTest.php b/tests/WebSocketServer/PackageMetadataTest.php index 35f0e0304e..36d29fbc4e 100644 --- a/tests/WebSocketServer/PackageMetadataTest.php +++ b/tests/WebSocketServer/PackageMetadataTest.php @@ -25,6 +25,7 @@ public function testDirectRuntimeDependenciesAreDeclared(): void foreach ([ 'ext-swoole', + 'hypervel/attributes', 'hypervel/collections', 'hypervel/context', 'hypervel/contracts',