diff --git a/AGENTS.md b/AGENTS.md
index 62db70899..bd7a2e6ab 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -136,6 +136,7 @@ The Working rules and the Avoid overengineering rules apply to all work in this
- **Use one source of truth** — Put all user documentation in `src/docs/`. Package READMEs are intentionally minimal, not a second documentation surface, and must not duplicate user documentation.
- **Write user documentation in Laravel-docs prose** — Use the simple, direct, human-friendly style of first-party Laravel documentation. Prefer natural explanations and examples over implementation language; avoid internal jargon, stiff wording, and needless detail.
+- **Keep the Laravel porting guide current and focused** — Whenever a framework change introduces, changes, or removes a public API, feature, configuration surface, or supported integration in a way that a Laravel application or package porter genuinely must account for, update `src/docs/porting-from-laravel.md` in the same change. Hard boot or runtime failures, silent semantic differences, and commonly used framework surfaces normally qualify. Internal implementation differences, performance work that preserves the public contract, incidental source drift, package-specific details, and narrow edge cases that do not change normal porting decisions do not. The guide is a high-signal starting context for humans and LLMs, not an exhaustive framework diff or dumping ground. Treat its context size as a design constraint: keep additions concise and action-oriented, link to the canonical feature documentation instead of duplicating its detail, and remove stale or duplicated guidance whenever editing the guide.
#### Package READMEs
@@ -194,14 +195,14 @@ Build complete, long-term solutions, not MVPs or local workarounds. A broad chan
- **Use `Sleep::usleep()` / `Sleep::sleep()` for delays in source code** — `Sleep` is fakeable in tests. Use raw `sleep()` / `usleep()` only where real time must pass, such as test harnesses and external-process polling.
- **Use `xxh128` for internal non-cryptographic hashing** — cache and context keys, content checksums, and change detection. It is faster than `sha256`, which is reserved for trust boundaries: stored credential digests, signatures, and anything an attacker gains by forging. Seed it when the hashed value comes from user input, as `SwooleStore` does for its physical table keys.
- **Use immutable dates by default** — Hypervel defaults to `Hypervel\Support\CarbonImmutable`, including where Laravel uses mutable Carbon. Create public or application-configurable dates through the `Date` facade or date helpers, and use exact `CarbonImmutable` for framework-owned internal or held values. Type configurable Carbon boundaries as `CarbonInterface` and native or third-party boundaries as `DateTimeInterface`. Capture the return value of every date modifier whose result must persist. Use `Hypervel\Support\Carbon` only for explicit mutable opt-out or conversion behavior.
-- **Use typed config getters and avoid duplicate defaults** — prefer `$config->string()`, `$config->integer()`, `$config->float()`, `$config->boolean()`, and `$config->array()` over `$config->get()` for values that cannot be null. Framework and package defaults are shallow-merged with application config. `mergeableOptions()` is only for named groups such as connections or stores: application entries replace matching defaults, while other default entries remain. Other nested arrays are replaced as a whole. Keep a fallback when a setting inside one of those replaced arrays is intentionally optional.
+- **Use typed config getters and avoid duplicate defaults** — prefer `$config->string()`, `$config->integer()`, `$config->float()`, `$config->boolean()`, and `$config->array()` over `$config->get()` for values that cannot be null. Do not pass a code-level fallback when the framework or package config defines the key; missing or misspelled keys should fail loudly instead of silently using a second default. Framework and package defaults are shallow-merged with application config. `mergeableOptions()` is only for named groups such as connections or stores: application entries replace matching defaults, while other default entries remain. Other nested arrays are replaced as a whole. Keep a fallback when a setting inside one of those replaced arrays is intentionally optional.
- **Env var naming** — Ported config keeps upstream names. New Hypervel-specific settings should use the established prefix for the package or subsystem that owns the value (`SERVER_`, `CACHE_`, `REDIS_`, etc.). Determine ownership semantically, not from the config filename: aggregate files such as `app.php` contain multiple domains, and `APP_` is for genuinely application-wide settings. If a value mirrors another config key, reuse that key's environment variable instead of defining a duplicate.
- **Use `resolve...Using` for Hypervel-owned config resolvers** — prefer this naming for callbacks that resolve config-derived values, unless an established Laravel domain convention already exists, such as `redirectUsing()`.
- **Always use American English spelling** — E.g., "behavior" vs "behaviour", "utilize" vs "utilise".
## Container
-Hypervel's container keeps Laravel's API surface — `bind()`, `singleton()`, `scoped()`, `instance()`, aliases, contextual bindings — with resolution adapted for long-lived Swoole workers. `make()` and `get()` resolve identically; `get()` is just the PSR-compliant exception wrapper. Use `make()`, and use it instead of array access too: `offsetGet()` always returns `mixed`, while `make()` carries class-string generics phpstan can follow, `make()` can take parameters, and `$app[$key] = $value` is a hidden `bind()`. Converting `$app['...']` in ported code to `make()` is an approved modernization (see Policy under Porting Packages). `Container::getInstance()` auto-creates via `??= new static()`, so it always returns a container.
+Hypervel's container keeps Laravel's named API surface — `bind()`, `singleton()`, `scoped()`, `instance()`, aliases, contextual bindings — with resolution adapted for long-lived Swoole workers. Container ArrayAccess and dynamic service properties are intentionally unsupported. `make()` and `get()` resolve identically; `get()` is the PSR-compliant exception wrapper. `Container::getInstance()` auto-creates via `??= new static()`, so it always returns a container.
### Resolution semantics vs Laravel
@@ -726,7 +727,7 @@ Full PHPStan runs through `composer fix` at checkpoints. During implementation,
When porting Laravel packages, whether first-party or third-party, keep them as close to 1:1 with upstream as possible so future changes are easy to merge. The exceptions are:
- Modernizing PHP types, including native parameter, return, property, and class-constant types, plus other appropriate PHP 8.4+ features, strict types, and strict comparisons
- Converting mutable Laravel date construction to Hypervel's immutable date conventions, typing configurable factory output as `CarbonInterface`, and capturing date-modifier return values
-- Converting container array access (`$app['events']`) to `make()`, and untyped `$config->get()` calls to the typed getters where the key isn't nullable (see Container and the typed-getter rule under Development Conventions)
+- Converting container array access (`$app['events']`) and dynamic service-property access (`$app->events`) in ported code to named container methods, and untyped `$config->get()` calls to typed getters where the key isn't nullable (see Container and the typed-getter rule under Development Conventions)
- Adding Laravel-style title docblocks to methods (not classes — see Development Conventions)
- For ported Laravel packages: making them coroutine-safe, adding Swoole performance enhancements (e.g., static property caching), making them pass PHPStan
- Not porting upstream framework-specific integrations that only make sense in the source framework (for example packages, drivers) unless Hypervel intentionally has an equivalent surface
@@ -879,6 +880,7 @@ Tests for these features should be **removed** (not commented out) without askin
- **Databases:** SQL Server, MongoDB, DynamoDB — Hypervel only supports MySQL, MariaDB, PostgreSQL, and SQLite
- **Cache drivers:** Memcached, DynamoDB, MongoDB
- **Dynamic connections:** `DB::build()`, `DB::connectUsing()` — incompatible with Swoole connection pooling
+- **Container access:** ArrayAccess and dynamic service properties
This list is exhaustive. Any other missing functionality requires investigation and reporting per When to Stop and Report.
@@ -893,5 +895,5 @@ This list is exhaustive. Any other missing functionality requires investigation
7. Fix mock types (PDO, QueryBuilder, Grammar, etc.)
8. Add `->andReturnSelf()` to chained method mocks
9. Use a test-specific namespace only when helper classes have generic, collision-prone names — already-specific helper names do not need extra namespace ceremony.
-10. Remove tests for unsupported features (SQL Server/MongoDB/DynamoDB databases, Memcached/DynamoDB/MongoDB cache, dynamic connections)
+10. Remove tests only for the approved unsupported features listed above
11. Run tests and fix any remaining type errors
diff --git a/README.md b/README.md
index 8b8bddbd5..f26485141 100644
--- a/README.md
+++ b/README.md
@@ -13,40 +13,103 @@
>
> Hypervel 0.4 is not ready for use yet. APIs, behavior, configuration, and package internals may change unexpectedly while the rewrite is still in progress.
>
+> The published documentation at [hypervel.org/docs](https://hypervel.org/docs) currently covers Hypervel 0.3. If you are experimenting with this branch, use the [in-progress 0.4 documentation](https://github.com/hypervel/components/tree/0.4/src/docs). If you are coming from Laravel, begin with the [porting guide](https://github.com/hypervel/components/blob/0.4/src/docs/porting-from-laravel.md).
+>
> Please do not use this branch for projects until a beta release is tagged. If you are experimenting or testing the rewrite, bug reports and feedback are very welcome.
-## Introduction
+## About Hypervel
+
+> [!NOTE]
+> This repository contains the core components of the Hypervel framework. If you want to create a Hypervel application, visit the [Hypervel application repository](https://github.com/hypervel/hypervel).
+
+Hypervel is a modern, opinionated PHP framework built for Swoole. It runs applications in long-lived workers and uses coroutines to handle many requests, jobs, and connections concurrently.
+
+When one coroutine is waiting on a database query, cache lookup, queue operation, file access, or HTTP request, the worker can keep serving other requests and jobs instead of sitting idle. You write ordinary sequential code, and the runtime yields to other work while yours waits.
+
+Hypervel is built for traditional web applications, APIs, microservices, real-time services, background workers, and other applications that spend meaningful time waiting on external systems.
+
+## Framework Features
+
+Hypervel includes the features expected from a modern full-stack framework:
+
+- Fast, expressive [routing](https://github.com/hypervel/components/blob/0.4/src/docs/routing.md) and middleware.
+- A powerful [dependency injection container](https://github.com/hypervel/components/blob/0.4/src/docs/container.md) and service provider system.
+- [Eloquent ORM](https://github.com/hypervel/components/blob/0.4/src/docs/eloquent.md), schema building, and [database migrations](https://github.com/hypervel/components/blob/0.4/src/docs/migrations.md).
+- Multiple [session](https://github.com/hypervel/components/blob/0.4/src/docs/session.md) and [cache](https://github.com/hypervel/components/blob/0.4/src/docs/cache.md) stores.
+- [Background jobs](https://github.com/hypervel/components/blob/0.4/src/docs/queues.md), job batching, and [task scheduling](https://github.com/hypervel/components/blob/0.4/src/docs/scheduling.md).
+- Real-time [event broadcasting](https://github.com/hypervel/components/blob/0.4/src/docs/broadcasting.md) and [WebSocket support](https://github.com/hypervel/components/blob/0.4/src/docs/websockets.md).
+- [Authentication](https://github.com/hypervel/components/blob/0.4/src/docs/authentication.md), [authorization](https://github.com/hypervel/components/blob/0.4/src/docs/authorization.md), [validation](https://github.com/hypervel/components/blob/0.4/src/docs/validation.md), [notifications](https://github.com/hypervel/components/blob/0.4/src/docs/notifications.md), [mail](https://github.com/hypervel/components/blob/0.4/src/docs/mail.md), and [filesystem storage](https://github.com/hypervel/components/blob/0.4/src/docs/filesystem.md).
+- [Blade templates](https://github.com/hypervel/components/blob/0.4/src/docs/blade.md) and [Vite](https://github.com/hypervel/components/blob/0.4/src/docs/vite.md) integration for full-stack applications.
+- First-class [coroutines](https://github.com/hypervel/components/blob/0.4/src/docs/coroutines.md) and [concurrent HTTP requests](https://github.com/hypervel/components/blob/0.4/src/docs/http-client.md#concurrent-requests).
+- Persistent [database](https://github.com/hypervel/components/blob/0.4/src/docs/database.md#connection-pooling) and [Redis](https://github.com/hypervel/components/blob/0.4/src/docs/redis.md#connection-pooling) connection pools.
+- Coroutine-aware [testing](https://github.com/hypervel/components/blob/0.4/src/docs/testing.md), with [Testbench](https://github.com/hypervel/components/blob/0.4/src/docs/testbench.md) for package development.
+- [gRPC](https://github.com/hypervel/components/blob/0.4/src/docs/grpc.md) and [custom server processes](https://github.com/hypervel/components/blob/0.4/src/docs/server-processes.md).
-> Note: This repository contains the core code of the Hypervel framework. If you want to build an application using Hypervel, visit the [Hypervel repository](https://github.com/hypervel/hypervel).
+## Laravel Compatibility
-**Hypervel** is a Laravel-style PHP framework with native coroutine support for ultra-high performance.
+Hypervel aims for Laravel API compatibility wherever it fits. However, Hypervel is not a Laravel clone or drop-in replacement. Many Hypervel components are ports of Laravel packages, adapted for Hypervel's asynchronous runtime, performance requirements, and coroutine safety, but the framework itself has its own architecture, features, supported integrations, and direction.
-Hypervel ports many core components from Laravel while maintaining familiar usage patterns, making it instantly accessible to Laravel developers. The framework combines the elegant and expressive development experience of Laravel with the powerful performance benefits of coroutine-based programming. If you're a Laravel developer, you'll feel right at home with this framework, requiring minimal learning curve.
+Moving an existing Laravel application or package to Hypervel is a deliberate port, not a namespace replacement. The [porting guide](https://github.com/hypervel/components/blob/0.4/src/docs/porting-from-laravel.md) explains what needs to change and why.
-This is an ideal choice for building microservices, API gateways, and high-concurrency applications where traditional PHP frameworks often encounter performance constraints.
+## Project Direction
-## Why Hypervel?
+Hypervel will continue to track and port Laravel features where they fit, while building features designed for Hypervel's runtime. The dedicated [rate limiter](https://github.com/hypervel/components/blob/0.4/src/docs/rate-limiting.md), [dual-mode Redis cache tags](https://github.com/hypervel/components/blob/0.4/src/docs/cache.md#redis-tag-modes), [layered cache stores](https://github.com/hypervel/components/blob/0.4/src/docs/cache.md#building-cache-stacks), and [Redis session system with user-session management](https://github.com/hypervel/components/blob/0.4/src/docs/session.md#managing-user-sessions) are examples of that direction.
-While Laravel Octane impressively enhances your Laravel application's performance, it's crucial to understand the nature of modern web applications. In most cases, the majority of latency stems from I/O operations, such as file operations, database queries, and API requests.
+First-party ClickHouse support and built-in integration with [SonicStack](https://sonicstack.io) are also planned. SonicStack is Hypervel's deployment platform and is how we plan to fund ongoing framework development.
-However, Laravel doesn't support coroutines - the entire framework is designed for a blocking I/O environment. Applications heavily dependent on I/O operations will still face performance bottlenecks. Consider this scenario:
+## Requirements
-Imagine building an AI-powered chatbot where each conversation API takes 3-5 seconds to respond. With 10 workers in Laravel Octane receiving 10 concurrent requests, all workers would be blocked until these requests complete.
+Hypervel 0.4 requires PHP 8.4 or later and Swoole 6.2.2 or later. Hypervel's Redis integrations use the PhpRedis extension 6.1 or later; Predis is not supported.
-> You can see [benchmark comparison](https://hypervel.org/docs/introduction.html#benchmark) between Laravel Octane and Hypervel
+See the [installation documentation](https://github.com/hypervel/components/blob/0.4/src/docs/installation.md#requirements) for the complete list of required PHP extensions and setup instructions.
-Even with Laravel Octane's improvements, your application's concurrent request handling capacity remains constrained by I/O operation duration. Hypervel addresses this limitation through coroutines, enabling efficient handling of concurrent I/O operations without blocking workers. This approach significantly enhances performance and concurrency for I/O-intensive applications.
+## This Repository
-> See [this issue](https://github.com/laravel/octane/issues/765) for more discussions.
+This monorepo contains Hypervel's core framework components and first-party packages. Components are developed and tested together here, then published as separate Composer packages. Framework changes and pull requests should be submitted to this repository rather than the split package repositories.
## Documentation
-[https://hypervel.org/docs](https://hypervel.org/docs)
+The complete Hypervel documentation is available at [hypervel.org/docs](https://hypervel.org/docs).
+
+Hypervel's documentation follows the structure and style of Laravel's documentation, and portions are adapted from it. Our thanks to the Laravel community.
+
+## Contributing
+
+Thank you for considering contributing to Hypervel. The [contribution guide](https://github.com/hypervel/components/blob/0.4/src/docs/contributions.md) explains which changes are accepted, how to run the required checks, and how to prepare a pull request.
+
+For support questions, ideas, and feature requests, please use [GitHub Discussions](https://github.com/hypervel/components/discussions).
+
+## Code of Conduct
+
+Please review and follow Hypervel's [Code of Conduct](https://github.com/hypervel/components/blob/0.4/src/docs/contributions.md#code-of-conduct) when participating in the community.
+
+## Security Vulnerabilities
-Hypervel provides comprehensive and user-friendly documentation that allows you to quickly get started. From this documentation, you can learn how to use various components in Hypervel and understand the differences between this framework and Laravel.
+If you discover a security vulnerability in Hypervel, please report it privately by emailing Albert Chen at [albert@hypervel.org](mailto:albert@hypervel.org). Security vulnerabilities will be addressed promptly.
-> Most of the content in this documentation is referenced from the official Laravel documentation. We appreciate the Laravel community's contributions.
+Please do not report security vulnerabilities through public GitHub issues or discussions.
## License
-The Hypervel framework is open-sourced software licensed under the [MIT](https://opensource.org/licenses/MIT) license.
+The Hypervel framework is open-sourced software licensed under the [MIT license](https://github.com/hypervel/components/blob/0.4/LICENSE.md).
+
+## Created by
+
+
diff --git a/composer.json b/composer.json
index 4d143d456..92ef37308 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/components",
"type": "library",
- "description": "The components for Hypervel framework.",
+ "description": "The Hypervel Framework.",
"license": "MIT",
"keywords": [
"php",
diff --git a/docs/plans/2026-08-08-1956-server-reloader-and-worker-configuration-refresh.md b/docs/plans/2026-08-08-1956-server-reloader-and-worker-configuration-refresh.md
new file mode 100644
index 000000000..7e17d8b84
--- /dev/null
+++ b/docs/plans/2026-08-08-1956-server-reloader-and-worker-configuration-refresh.md
@@ -0,0 +1,424 @@
+# Server Reloader and Worker Configuration Refresh
+
+## Goal
+
+Make `server:reload` and programmatic server reloads start event and task workers from the latest environment and configuration without retaining config-derived objects from the master process. Each service provider refreshes the worker state it owns through one small lifecycle contract. Retained framework objects keep their identity when other live services hold them; replaceable cached objects are forgotten and rebuilt lazily.
+
+The public surface stays Laravel-shaped:
+
+- `ServerReloader::reload()` is an injectable service for programmatic reloads.
+- `server:reload` is a thin console adapter over that service.
+- Providers implement `ReloadsConfiguration::reloadConfiguration()` when they own worker state derived from configuration.
+- Manager reset methods use established plural names such as `forgetDrivers()`, `forgetConnections()`, and `forgetDisks()`.
+
+The refresh runs once per replacement worker before it accepts work. It adds no request-path work.
+
+## Verified Lifecycle Facts
+
+- The server application is registered and booted before Swoole forks workers. Workers inherit the container, registered providers, resolved singleton objects, callbacks, and cached manager state.
+- `WorkerStartCallback` dispatches `BeforeWorkerStart` before worker-type events, startup logging, `AfterWorkerStart`, and the worker-start coordinator barrier.
+- `ReloadDotenvAndConfig` already handles `BeforeWorkerStart`. It reloads dotenv, rebuilds configuration through `LoadConfiguration`, repopulates the existing config repository through `replaceItems()`, and replays `ConfigMutationTracker` mutations.
+- Config repository identity is preserved, so services retaining the repository see its new contents. Objects that copied config values into properties or resolved drivers in the master keep that stale state unless their owning provider updates or forgets them.
+- `Application::getProviders($type)` filters the existing ordered provider list; it does not resolve new providers.
+- Provider order is:
+ 1. base Event, Log, Context, and Routing providers;
+ 2. `Hypervel\` providers explicitly listed in `app.providers`, preserving their order;
+ 3. discovered package providers sorted by descending `ServiceProvider::$priority`, preserving ties;
+ 4. remaining application providers, preserving their order.
+- Listing a discovered `Hypervel\` provider in `app.providers` moves it into group 2 because the later `unique()` keeps the first occurrence. A discovered provider hook must therefore not depend on another discovered provider's relative position.
+- Swoole reloads event workers with `SIGUSR1` and task workers with `SIGUSR2`. Custom server processes, queue workers, the scheduler, and Horizon processes are separate process lifecycles.
+- A synchronous request on a registered HTTP connection during master bootstrap stores a live cURL handler on the auto-singleton HTTP factory. `BeforeServerFork` must clear that pure resource cache so the manager and every worker inherit presets but build their own handlers. Bare and asynchronous requests do not use this cache.
+- Laravel boots providers per PHP process. Hypervel must instead separate master bootstrap from worker configuration refresh while keeping Laravel-style provider ownership.
+
+## Anti-Overengineering Rules
+
+> This audit is not permission to add defensive machinery for every imaginable failure. Do not add an abstraction, state machine, retry loop, configurable timeout, registry, mutex, context slot, cache, or compatibility API merely because it sounds robust.
+>
+> Complexity must pay for itself with at least one of:
+>
+> - a demonstrated failure;
+> - a complete source trace proving a realistic vulnerable schedule;
+> - a clear general capability with real consumers and owner approval;
+> - deletion of greater or riskier complexity elsewhere.
+>
+> Typical Laravel lifecycle semantics define the supported contract. A package that intentionally relies on model events, middleware, listeners, transactions, or another documented mechanism is not defective merely because userland can explicitly bypass that mechanism. Do not build a parallel enforcement path for `withoutEvents()`, raw database writes, disabled middleware, direct transport access, or comparable deliberate bypasses unless the public contract explicitly promises behavior through that bypass.
+>
+> Underengineering is equally a failure. Fix every verified defect completely at its lowest owning boundary, never with a partial fix or a local patch over a broken shared contract, and always surface meaningful evidence-backed improvements rather than dropping them to avoid effort. Restraint applies to speculative machinery and cosmetic change, not to complete fixes or worthwhile opportunities.
+>
+> Do not treat an upstream difference as a bug without tracing it. Do not treat upstream parity as proof of correctness. A real Hypervel defect remains a defect when Laravel, Hyperf, Symfony, or an SDK has the same hole.
+>
+> The audit categories are discovery lenses, not boundaries around what may be corrected. Any genuine issue discovered while auditing, implementing, testing, or reviewing must be investigated, assigned to its lowest owning boundary, and taken through the applicable consensus, implementation, validation, review, and approval workflow—even when it is outside the current package, initial taxonomy, or changed diff. Do not dismiss a verified issue as unrelated or defer it merely to preserve package order. This rule applies only after the evidence threshold is met; it does not turn speculative concerns, deliberate bypasses, unsupported use, or contract violations into work.
+
+## Configuration Reload Contract
+
+Add the contract to Foundation contracts, where provider lifecycle behavior belongs:
+
+```php
+namespace Hypervel\Contracts\Foundation;
+
+interface ReloadsConfiguration
+{
+ public function reloadConfiguration(): void;
+}
+```
+
+Registered service providers implement the contract directly. `ReloadDotenvAndConfig` invokes them after the existing repository has been repopulated and recorded config mutations have been replayed:
+
+```php
+protected function reloadConfig(): void
+{
+ $config = $this->rebuildConfigRepository();
+
+ $this->configMutationTracker->replay($config);
+
+ foreach ($this->container->getProviders(ReloadsConfiguration::class) as $provider) {
+ $provider->reloadConfiguration();
+ }
+}
+```
+
+Rules:
+
+- Hooks are synchronous and fail fast. A broken refreshed configuration must prevent the replacement worker from becoming ready.
+- The contract and every public implementation carry a `Boot-only.` warning stating that request-time use mutates shared worker state while concurrent coroutines may still hold the previous objects.
+- Do not add retries, rollback, event hierarchies, priorities, a hook registry, or a dependency graph.
+- Hooks may rely on the documented provider group order, but optional package hooks must not depend on another discovered provider's position.
+- Hooks update inherited config snapshots. Work that must happen for every newly started worker after all configuration is current remains on `AfterWorkerStart`.
+- Auth and Sanctum cache validation remains on `AfterWorkerStart`. Moving it into Auth's hook would validate against a cache store that Cache's later hook may still replace.
+- Cache serializable-class finalization remains on `AfterWorkerStart` for the same completed-worker-bootstrap reason.
+- Invalid refreshed config may put replacement workers into a restart loop until the configuration is fixed and reload is attempted again. The master continues running its existing image. Document this behavior instead of masking it.
+
+### Replay derived configuration as operations
+
+`ConfigMutationTracker` supports both recorded values and closures reevaluated against the rebuilt repository. Use the existing `applyAndRecord()` shape for pure derived configuration. In Fortify, Sentry, and Horizon, move every config read and write in the derivation into one `static` closure with no captures and use this shared comment:
+
+> Derived config can depend on the worker environment, so replay the operation after config reload rather than its master result.
+
+- Fortify recomputes its four derived `passkeys.*` values from current `fortify.passkeys.*`, `app.url`, and `app.key`. Keep `Passkeys::ignoreRoutes()` and the redirect callback outside the closure as master-installed state.
+- Sentry recomputes its two missing log-channel defaults from the current channel map and log level. Explicit application channel configuration must remain untouched.
+- Horizon recomputes an empty `horizon.name` from current `app.name`; an explicit name remains untouched.
+
+Resolve the concrete config repository and do not add cached-configuration guards. These operations are idempotent and perform no file I/O. Keep value snapshots for `app.providers` and Reverb/gRPC `server.servers`: those values describe providers or server topology already installed in the master, so recomputing them only in replacement workers would make config disagree with the running process. Add a concise source comment at each of those three `set()` calls so a future change does not convert the snapshot into a replayed operation.
+
+## Object Identity and Resolution Rules
+
+Each hook must apply these rules:
+
+- Forget an object only when no framework-owned live object retains it.
+- Mutate an object in place when another live framework service holds it.
+- Preserve user-registered manager creators, extensions, callbacks, named definitions, and routes unless they are themselves built from refreshed config.
+- Guard the canonical bound abstract with `resolved()` before resolving an optional object. Do not guard only a concrete class used as a string binding target.
+- A string-concrete binding can make `resolved(Concrete::class)` true without caching a direct concrete instance. Resolving that concrete may create an unused auto-singleton. Never use that false-positive path.
+- Do not eagerly resolve services during refresh.
+- Narrow before mutating only where the matrix specifies it: `StdoutLoggerInterface`, `HubInterface`, the Telescope repository contracts, and the `translator`, `view`, `blade.compiler`, and `jwt` keys, whose implementations an application may plausibly supply. Every other hook calls its canonical key directly, string or concrete, without a type check: both the `forget*` family on `cache`, `queue`, `auth`, `mail.manager`, and `filesystem`, and in-place mutation of framework plumbing such as `redirect` and `cookie`.
+- A first-party fake bound over a canonical manager key must satisfy the reset API invoked by worker refresh. When the fake wraps the real manager, its reset must delegate while preserving recorded fake state.
+- Public methods that clear worker-held registries or mutate worker configuration must use the exact `Boot-only.` or `Boot or tests only.` warning and name the shared-state race caused by request-time use.
+
+## Programmatic Server Reload
+
+Add injectable `Hypervel\Server\ServerReloader` with:
+
+```php
+public function reload(): void;
+```
+
+Behavior:
+
+1. Read the current `server.settings.pid_file` through the config repository.
+2. Read the PID through the low-level `Hypervel\Filesystem\Filesystem` service. The disk-oriented Filesystem contract is the wrong boundary: its `get()` accepts disk-relative paths and may return `null`, while the server PID path is a local absolute path and the existing concrete service throws `FileNotFoundException`.
+3. Reject non-numeric or non-positive PID contents with the server package `InvalidArgumentException`.
+4. Send `SIGUSR1` for event workers.
+5. Send `SIGUSR2` only when `server.settings.task_worker_num` is greater than zero.
+6. Throw `ServerException` naming the failed signal and worker class when a signal cannot be sent.
+7. Let the existing filesystem exception report an unreadable PID file.
+
+Use constructor injection for the config contract and the low-level concrete Filesystem service. Keep signal delivery behind one protected method so unit tests can record success and failure without signaling a real process.
+
+Leave the concrete service unbound so Hypervel's normal auto-singleton behavior applies. Do not add a facade, container alias, or interface for a class with one implementation.
+
+`ServerReloadCommand` becomes a thin adapter: print the start message, call `ServerReloader::reload()`, map the service's filesystem, invalid-PID, and signal exceptions to command failure with their messages, and print `Done.` on success. Remove its direct config, filesystem, and signal ownership. Do not add progress callbacks merely to interleave an extra task-worker message.
+
+Keep the existing `ReloadsWorkers` trait separate. Its silent best-effort `SIGUSR1` behavior serves a different internal cleanup path and deliberately does not claim strict task-worker reload semantics. Do not merge it into the service or add PID liveness checks, polling, locks, retries, readiness tracking, or stale-PID recovery.
+
+## Container Correction
+
+`Container::forgetInstance()` must canonicalize aliases before clearing cached instance, scoped, and auto-singleton state:
+
+```php
+public function forgetInstance(string $abstract): void
+{
+ $this->forgetCachedInstances($this->getAlias($abstract));
+}
+```
+
+Keep `forgetCachedInstances()` operating on an already canonical key. Add a regression proving that forgetting through an alias clears the canonical cached object and the next resolution builds a new instance.
+
+## Swoole Cache Table Safety
+
+Cache and Rate Limiter Swoole tables are process topology: they must be created in the master before Swoole forks workers. Rate Limiter already seals its `TableManager` after `BeforeServerStart` initialization and rejects a later unknown table. Cache currently creates an unknown table lazily in the calling worker, which silently gives each worker private cache state after a reload introduces a new table.
+
+Give `SwooleTableManager` the same minimal invariant:
+
+- add a `sealed` flag;
+- return existing states before checking it;
+- throw `LogicException("Swoole cache table [{$name}] was not initialized before the server fork.")` when a missing state is requested after sealing;
+- add idempotent `seal(): void` with the exact `Boot-only.` warning explaining the cross-worker split;
+- call `seal()` after table creation on every `CreateSwooleTable` dispatch, even when no Swoole cache store is configured. `BeforeServerStart` fires once per configured port, so later dispatches must reuse existing states and reseal without failing.
+
+Do not add table recreation, config diffing, worker coordination, or another lifecycle. This converts a silent data split into the same explicit restart requirement the rate limiter already enforces.
+
+## Shared Reset and Mutation APIs
+
+Add the smallest plural reset or setter that matches each existing manager/class responsibility:
+
+The plural manager reset methods return `static`, matching existing Laravel-style fluent resets such as `forgetDrivers()`, `forgetGuards()`, and `forgetMailers()`. Translator's cache-only `forgetLoadedGroups()` follows its existing void mutator convention.
+
+| Owner | API | Required behavior |
+|---|---|---|
+| Password | `forgetBrokers()` | Clear resolved brokers while preserving broker construction behavior. |
+| Cache | `forgetDrivers()` | Clear resolved cache drivers while preserving custom creators and the shared serialization policy. |
+| `MultipleInstanceManager` | `forgetInstances()` | Clear resolved named instances while preserving creators. Used by Concurrency and Rate Limiter. |
+| Filesystem | `forgetDisks()` | Clear resolved disks while preserving custom creators. |
+| HTTP client | `forgetConnectionHandlers()` | Clear inherited transport handlers while preserving registered connection presets. |
+| Log | `forgetChannels()` | Clear resolved channels while preserving custom creators and shared context. |
+| Queue | `forgetConnections()` | Clear resolved queue connections while preserving connectors and callbacks owned elsewhere. |
+| URL generator | `setAssetRoot(?string)` | Update the retained generator's asset root at worker boot. |
+| URL generator | existing `setRequest()` | Change its warning from `Tests only.` to `Boot or tests only.` because Routing legitimately refreshes the fallback request at worker boot. |
+| View compiler | `reloadConfiguration(...)` | Update the retained compiler's five config-derived fields without losing application-registered compilation behavior. |
+| Translator | `setBaseLocale(string)` | Update the retained worker-wide base locale without changing a coroutine-local override. |
+| Translator | `forgetLoadedGroups()` | Clear file-loaded groups while preserving lines registered through `addLines()`. |
+| Redirector | existing `setSession()` | Add a `Boot-only.` warning because Session refreshes the retained Redirector. |
+| Telescope database repository | `setConnection(string)` | Update the connection used by the retained repository. |
+| Telescope database repository | `setChunkSize(?int)` | Apply the constructor's existing falsy-to-default rule. |
+
+Foundation retains the source-aware CLI or HTML dumper created during master bootstrap. Explicit `VAR_DUMPER_FORMAT=cli|html` temporarily removes that variable only while installing the matching Hypervel handler, then restores it in `finally`; `server` and TCP formats remain Symfony-owned. Both dumper `register()` methods return the constructed instance, and the shared dump-source concern exposes `setCompiledViewPath(string)` so reload mutates the retained dumper instead of replacing the global handler. This preserves Telescope's wrapper around that handler. Dumper selection itself remains restart-owned.
+
+Keep editor-link resolution in its own shared concern. Exception renderer frames need only editor links; they must not inherit dump-source configuration and worker-lifetime resolver APIs merely to reuse that logic.
+
+Extract `DatabaseEntriesRepository::DEFAULT_CHUNK_SIZE = 1000`; initialize the property and setter fallback from the same constant, and make the constructor delegate to the two setters.
+
+For the View compiler, keep only `Filesystem` promoted. Declare the five config-derived fields as normal properties and make the constructor delegate to one validated boot-only method:
+
+```php
+public function reloadConfiguration(
+ string $cachePath,
+ string $basePath,
+ bool $shouldCache,
+ string $compiledExtension,
+ bool $shouldCheckTimestamps,
+): void;
+```
+
+Keep the existing non-empty cache-path validation before assignment. Leave this concrete lifecycle method off `CompilerInterface`, whose rendering contract remains `getCompiledPath()`, `isExpired()`, and `compile()`.
+
+### Translator base-locale correction
+
+The translator constructor currently calls coroutine-local `setLocale()` after property promotion has already assigned the same base locale. This leftover Laravel assignment seeds no request coroutine and masks later base-locale changes in the current or non-coroutine context.
+
+Correct the ownership model:
+
+- Declare `protected string $locale` as a normal property; keep only the loader promoted.
+- Make the constructor call `setBaseLocale()` so that setter is the single assignment path.
+- Extract `assertValidLocale()` and call it from both `setLocale()` and `setBaseLocale()`.
+- Call the same validator from `setFallback()` so invalid refreshed configuration fails before the replacement worker becomes ready.
+- Keep the `/`, `\`, `.`, and `..` path checks unchanged.
+- Move the cross-file validation comment to the shared validator and update `FileLoader`'s matching comment.
+- `setBaseLocale()` changes only the property. An existing coroutine-local `setLocale()` override continues to win.
+- Keep `setBaseLocale()` off the Translation contract. It is a framework concrete lifecycle operation, not a requirement for application-supplied translators.
+- Update the Translation README's existing Laravel-difference sentence to name `setBaseLocale()` as the boot-only counterpart. Do not add framework-lifecycle API detail to the application localization guide.
+
+Keep lines registered through the boot-only `addLines()` API separate from loader results. Store them as an ordered operation list indexed by namespace, group, and locale. A list is required because parent and child dot-path writes must replay in their original order; a keyed map changes `Arr::set()` results when a parent is overwritten between child writes. `addLines()` records each operation and applies it immediately only when that group is already loaded. `load()` reads the loader first, then replays that group's operations. Do not load eagerly from `addLines()`: later-booting providers may still add loader paths or namespace hints.
+
+Add `forgetLoadedGroups(): void` with a `Boot or tests only.` warning. It clears only loader results; registered operations remain for the next load. Keep `setLoaded()` as the exact loaded-cache setter. Do not deduplicate registered operations: boot-only use is bounded, and deduplication would have to reproduce parent/child ordering semantics.
+
+## Provider Refresh Matrix
+
+### Core and default providers
+
+| Provider | Refresh behavior |
+|---|---|
+| Foundation | Reapply `app.timezone`; update the retained source-aware dumper with current `view.compiled`; clear maintenance-mode manager drivers; flush the boot-reachable `WorkerCachedMaintenanceMode` snapshot; forget `MaintenanceModeContract`. A provider may populate that snapshot by calling `Application::isDownForMaintenance()` before the fork, and an interval of zero would otherwise retain it forever. Add the standard `Boot or tests only.` warning to `flushCache()`. |
+| Routing | Preserve routes, middleware, and the URL generator. Replace the fallback request from current `app.url`, set the asset root, and call `forceHttps()` with the current boolean so both enabling and disabling apply. Preserve Redirector and ResponseFactory identities. |
+| Session | Clear Session manager drivers and forget `session.store`. If canonical `redirect` is resolved, update the retained Redirector with the refreshed store. ResponseFactory keeps its retained Redirector reference. |
+| Auth | Clear resolved guards. Cache validation remains on `AfterWorkerStart`. |
+| Password | Clear resolved brokers. |
+| Broadcast | Clear manager drivers and forget the cached default broadcaster contract. |
+| Bus | Forget both `BatchRepository` and `DatabaseBatchRepository`. The outer singleton resolves and separately caches the same config-built concrete, so clearing only one key leaves stale batching configuration reachable through the other. No framework master-boot retainer requires in-place mutation. |
+| Cache | Clear manager drivers and forget `cache.store`. Preserve custom creators and the shared `SerializableClassPolicy`; finalization remains on `AfterWorkerStart`. Swoole cache-table definitions remain restart-owned, and the sealed table manager rejects a newly configured table after fork. |
+| Concurrency | Clear resolved manager instances. Preserve custom creators. |
+| Rate Limiter | Clear resolved manager instances. Preserve custom creators, named limiters, store policy, and key scopes. Swoole rate-limiter table definitions remain restart-owned; its existing sealed manager already rejects a newly configured table after fork. |
+| Cookie | If the canonical cookie service is resolved, mutate the retained `CookieJar` through `setDefaultPathAndDomain()` with current session config. Middleware and the session handler retain this object. |
+| Database | Keep existing pre-refresh resource cleanup. After config rebuild, forget `db.resolver` and the auto-singleton `ConnectionResolver`. |
+| Encryption | Reset the Serializable Closure secret and forget the encrypter. No framework master-boot object requires retaining the old encrypter identity. |
+| Filesystem | Clear disks and forget `filesystem.disk`. |
+| Hashing | Clear drivers and forget `hash.driver`. |
+| HTTP | On `BeforeServerFork`, clear connection handlers on an already resolved HTTP factory. Preserve registered presets, middleware, fakes, and other factory state; workers rebuild process-local handlers lazily. |
+| Log | Clear channels and refresh the retained stdout logger in place. Guard and resolve `StdoutLoggerInterface`, then mutate only the framework `StdoutLogger`. Remove the hard-coded stdout refresh from `WorkerStartCallback`. |
+| Mail | Clear mailers and forget Markdown. `MailFake::forgetMailers()` also clears the wrapped real manager while preserving recorded fake state. |
+| Notifications | Clear ChannelManager drivers and forget MailChannel. `NotificationFake` satisfies the manager reset as a no-op because it resolves no drivers, preserving recorded fake state. |
+| Object Pool | On `BeforeServerFork`, flush an already resolved `PoolManager`; keep the existing recycler on `AfterWorkerStart`. Do not add a worker-start flush: the master manager is empty after the fork-time flush, and a second flush could close a pool created by an earlier worker-start listener. `ObjectPool::destroyObject()` already contains user cleanup failures, so do not add exception aggregation to `PoolManager::flush()`. |
+| Queue | Clear connections and forget `queue.connection` and `queue.failer`. `BackgroundConnector` and `DeferredConnector` install exception reporting when they construct their queue, so rebuilt connections need no eager restoration. `QueueFake::forgetConnections()` also clears its wrapped real manager while preserving recorded fake state. |
+| Translation | If canonical `translator` is resolved and is the framework Translator, set base locale and fallback from config, then call `forgetLoadedGroups()`. Preserve registered lines, loader paths/namespaces, extensions, callbacks, selector, and stringable handlers. |
+| View | Preserve Factory, FileViewFinder, Blade compiler, EngineResolver, and resolved engine identities. Update the retained finder paths and flush only its lookup cache. Update the retained framework compiler's config fields through `Compiler::reloadConfiguration()` while preserving directives, conditions, tags, echo format, precompilers, components, and other registrations. Clear `CompilerEngine`'s boot-reachable compile-check map so a view rendered during provider boot cannot bypass refreshed cache or timestamp policy. Do not forget the compiler or engine: normal engine eviction rebuilds around the same compiler singleton, while Sentry's resolver rebuilds a decorator around its captured old engine. |
+
+Foundation reapplies the timezone after mutation replay because `LoadConfiguration` sets it before replay, and a recorded boot mutation may then change `app.timezone`. It does not reapply `mb_internal_encoding`; that value is deliberately fixed to UTF-8 rather than config-derived.
+
+No configuration hook is added to these base/default providers:
+
+- Event and Context register process-wide infrastructure with no config-derived snapshot.
+- Console, Engine, Form Request, and Pipeline register commands, callbacks, or stateless factories without copying configuration.
+- HTTP's fallback request binding reads the preserved config repository when it resolves. Its named-connection handlers are discarded before the server fork rather than through a configuration hook.
+- Pagination's boot-installed resolvers read request context or the retained container when invoked.
+- Redis retains its config repository and pool factory, reads connection configuration on demand, and already discards process connections before worker start.
+- Server and Server Process own master-installed server/process topology, not reloadable worker configuration.
+- Signal reads `signal.handlers` only when its listener starts after configuration refresh.
+- Validation retains the Translator and Database manager identities; their owning providers refresh those objects or their dependent state in place.
+
+### Optional providers
+
+| Provider | Refresh behavior |
+|---|---|
+| JWT | `ClaimFactory::reloadConfiguration()` rereads issuer and subject-lock settings. `JwtManager::reloadConfiguration()` clears drivers and validations, rereads blacklist enablement, and resolves the current Blacklist only when enabled. The provider updates a resolved ClaimFactory, forgets resolved Parser and Blacklist objects, then refreshes a resolved manager. Constructors delegate to these boot-only methods after their required base initialization. Preserve custom driver creators. |
+| Permission | If resolved, call `PermissionRegistrar::initializeCache()`. |
+| Reverb | Clear resolved `ApplicationManager` drivers and forget an already resolved `WebhookBatchBuffer`. Preserve `ServerProviderManager`; server topology remains restart-owned. |
+| Saloon | Re-register the current configured HTTP connection after configuration replay. This replaces same-name options and their cached handler, registers a changed name, and preserves application-owned presets under previous names. Keep console resources on the initial boot path only. |
+| Scout | If `EngineManager` is resolved, call `forgetEngines()` while preserving custom creators. Forget resolved Algolia, Meilisearch, and Typesense client bindings so rebuilt engines use current client configuration. |
+| Sentry | Extract the existing Hub binding's client construction and integration setup into one protected `createClient(): ClientInterface` method. Use it both for initial Hub construction and reload. Guard canonical `HubInterface`, mutate only the framework `Hub` through `bindClient()`, preserve the same global `SentrySdk` Hub, and forget BacktraceHelper after the client swap. Its log-channel defaults are already recomputed during config mutation replay. Listener/decorator topology and package enablement remain restart-owned. |
+| Socialite | If resolved, clear `SocialiteManager` drivers while preserving custom creators. |
+| Inertia | Forget resolved `inertia.view-finder`. Do not flush request-scoped gateway state from a worker hook. |
+| Telescope | Preserve repository objects retained by watchers, controllers, and `Telescope::$store`. Guard and mutate only `EntriesRepository`, `ClearableRepository`, and `PrunableRepository`; update connection and chunk size. Never guard or resolve `DatabaseEntriesRepository::class`, which can construct an unused fourth instance. Convert its two contextual config callbacks to typed getters. |
+
+No configuration hook is added to these discovered providers:
+
+- Sanctum, whose cache validation remains on `AfterWorkerStart`;
+- Fortify, whose derived Passkeys values are handled during config mutation replay while route and action topology remains restart-owned;
+- gRPC, whose dedicated listener configuration and captured request/response limits form one master-installed server topology and require restart together;
+- Horizon, whose derived default name is handled during config mutation replay while static options and supervisor topology live in separate long-running processes;
+- Nested Set and WebSocket Server, which register config-independent macros or event listeners;
+- Passkeys, whose package config merge and any Fortify derivation are replayed while routes and bindings remain restart-owned;
+- Tinker, Watcher, and Wayfinder, which register console-only services;
+- Testbench and Testing, which are development-only providers and own no server-worker configuration snapshot.
+
+## Provider Source Cleanup Required by the Changes
+
+- Convert all three Queue provider `$app[...]` reads to `make()` inline.
+- Convert all three View provider `$app[...]` reads to `make()` inline.
+- Do not add `@var` annotations or restructure the closures. These canonical string keys still return `mixed`; the conversion is consistency with the container convention, not a PHPStan improvement.
+- Add `ext-posix` to `src/foundation/composer.json`, because Foundation's reload trait directly calls `posix_kill`. Root and Server metadata already require it.
+
+## Restart Boundary
+
+Worker configuration refresh updates inherited config-backed state for replacement event and task workers. It does not rebuild master-owned topology:
+
+- listening ports and Swoole settings;
+- event/task worker counts and callback registration;
+- routes, middleware, event listeners, package enablement, and config-gated bindings or boot registrations;
+- custom server-process definitions;
+- Cache and Rate Limiter Swoole table definitions;
+- gRPC, Reverb, Sentry, Telescope watcher, Fortify, and Horizon topology;
+- preloaded or changed PHP code.
+- dump output format selected through `VAR_DUMPER_FORMAT`.
+
+Changing those requires a full server or process restart. Queue workers, the scheduler, Horizon, and custom server processes are not signaled by `ServerReloader` and must be restarted through their own lifecycle controls.
+
+## Documentation
+
+Update `src/docs/providers.md` in Laravel-docs prose:
+
+- Correct all three claims that providers register or boot at worker startup. Explain that the server application registers and boots before forking, and workers inherit that state.
+- Preserve the accurate conclusion that deferred providers provide no useful optimization because registration cost is amortized, while correcting the lifecycle explanation.
+- Add a concise `ReloadsConfiguration` section with an application-provider example. State that application providers run after framework and discovered package providers.
+- Explain the difference between master bootstrap, worker configuration refresh, and per-worker startup events.
+- Link to the deployment reload section with `/docs/{{version}}/...` syntax.
+
+Update `src/docs/deployment.md` in the same style:
+
+- Document injecting `ServerReloader` and calling `reload()`.
+- Explain that `server:reload` and the service replace event workers and configured task workers.
+- Explain which config-derived services refresh, which topology changes need a restart, and which other long-running process types must be restarted separately.
+- Name Cache and Rate Limiter Swoole table definitions as restart-owned, and explain that the first use of a newly configured table after reload fails explicitly because shared tables can only be created before the server fork.
+- Explain the invalid-config restart-loop behavior and recovery: fix configuration, then reload again.
+
+Update the HTTP client's Connections section with one short paragraph: connection presets registered only during `boot()` retain those options across worker reloads. Providers whose presets come from reloadable configuration must implement `ReloadsConfiguration` and re-register them. Link to the providers guide and state that application hooks run after package hooks, so applications remain the final authority for shared connection names when they re-register them.
+
+Update the Queue guide's existing `background` and `deferred` connection paragraph to state that uncaught exceptions are reported through the application's exception handler.
+
+Remove the completed service-provider reload item from `docs/todo.md`. Do not duplicate these explanations in package READMEs.
+
+## Tests
+
+### Server and lifecycle orchestration
+
+- Add `tests/Server/ServerReloaderTest.php` for PID-file reads, invalid PID contents, event-only reload, event-plus-task reload, each signal failure, exception messages, and the protected signal seam.
+- Simplify `ServerReloadCommandTest` to adapter behavior: start/success output, service call, and failure status/rendering.
+- Extend `ReloadDotenvAndConfigTest` to prove the exact sequence: dotenv/config rebuild, config mutation replay, then ordered provider hooks.
+- Cover provider filtering, ordered calls, fail-fast behavior, and no resolution of unrelated services.
+- Prove Fortify, Sentry, and Horizon replay their full derivations from rebuilt config without captures; include Sentry's explicit-channel preservation and current-level fallback branches.
+- Prove an invalid refreshed configuration prevents the worker-start path from completing.
+- Extend `WorkerStartCallbackTest` to prove stdout configuration is provider-owned and the event order remains correct.
+- Prove explicit CLI/HTML formats install Hypervel's source-aware handler while restoring `VAR_DUMPER_FORMAT`, and prove Foundation reload mutates the retained dumper without removing Telescope's wrapper.
+
+### Shared APIs and identity
+
+- Add focused unit coverage for every new plural reset method, including preservation of custom creators and callbacks.
+- Prove `forgetConnectionHandlers()` preserves registered HTTP presets while rebuilding handlers, and prove the HTTP provider clears only an already resolved factory on `BeforeServerFork`.
+- Add the container alias-forget regression.
+- Add Cache table-manager coverage proving known tables remain available after sealing, unknown tables fail with the fork-specific exception, and `CreateSwooleTable` initializes then seals safely across two `BeforeServerStart` dispatches.
+- Add URL generator tests for refreshed request, asset root, and both `forceHttps(true)` and `forceHttps(false)` paths while retaining identity.
+- Prove Session refresh preserves Redirector and ResponseFactory identities while replacing the session store.
+- Prove Cookie refresh mutates the retained CookieJar used by existing middleware/session handler objects.
+- Prove Translation construction creates no coroutine override, base refresh changes the fallback value, an explicit request override still wins, locale and fallback setters reject invalid locales, loaded groups clear, and extensions/callbacks remain.
+- Prove pre-load `addLines()` registrations preserve unrelated loader lines and override matching lines. Prove parent/child/repeated-parent operations replay in call order, and default, namespaced, and JSON-group registrations survive refresh while new loader results are read.
+- Strengthen the existing Translation coroutine isolation assertion and extend the existing invalid-locale test rather than creating duplicate test files.
+- Prove View preserves Factory, finder, compiler, EngineResolver, and resolved engine identities while updating finder paths and compiler configuration. Preserve finder hints/extensions and Blade registrations. Seed the compile-check map with a pre-fork render, change `view.cache` or enable timestamp checks, and prove the next render uses the refreshed policy; do not use a changed compiled path or relative hash because a missing target file self-heals without proving the reset.
+- Prove Telescope updates the three retained repository views without constructing a direct DatabaseEntriesRepository auto-singleton.
+- Prove Sentry preserves Hub identity and binds a fresh client.
+- Prove JWT refresh order and state: ClaimFactory update, Parser/Blacklist replacement, manager driver/validation reset, blacklist enablement, and creator preservation.
+
+### Provider behavior
+
+Add or extend provider tests for every matrix row. Headline regressions must include:
+
+- cache resolved in the master then changed before worker start;
+- database resolver default changed after master resolution;
+- rebuilt background/deferred Queue connections carry exception reporting from their connectors;
+- Queue, Mail, and Notification fakes remain bound with their recorded state after refresh, while Queue and Mail also reset their wrapped real managers;
+- Object Pool flushes before fork and no worker-start flush is added;
+- Log refreshes the canonical stdout logger before later hooks can log;
+- Auth and Sanctum validation still run only after all configuration hooks;
+- optional-provider resolved guards do not instantiate unused services.
+- Saloon refreshes same-name options and handlers, registers a changed name without removing an application-owned old-name preset, and rejects invalid refreshed options before mutating the registered preset.
+
+Run each changed test file immediately. After each coherent package slice, run its focused test group. At the final checkpoint run `composer fix`, then trace all changed callers/callees and retained-object relationships during self-review.
+
+## Implementation Order
+
+1. Add and test the Foundation contract and container alias correction.
+2. Add and test `ServerReloader`, then reduce the command to an adapter.
+3. Add shared manager reset/mutation APIs and their focused tests one file at a time.
+4. Correct Translator base-locale ownership and tests before adding the Translation provider hook.
+5. Correct derived-config mutation recording, then extend `ReloadDotenvAndConfig` to call provider hooks and prove ordering/failure semantics.
+6. Implement core/default provider hooks in provider order. Seal Cache's Swoole table manager in the Cache slice. Clear resolved HTTP connection handlers before fork. Add the Log hook and remove `WorkerStartCallback`'s stdout special case in the same slice, then verify its event-order test immediately.
+7. Implement optional provider hooks, including Saloon's shared boot/reload registration path, while preserving resolved guards and object identity.
+8. Complete the Queue/View container-access cleanup.
+9. Update Foundation package metadata.
+10. Update provider/deployment docs, Translation README, and remove the completed todo entry.
+11. Run focused cross-provider regressions, then `composer fix` once.
+12. Perform a full self-review for stale paths, object-retention mistakes, accidental eager resolution, Laravel API compatibility, request-path overhead, and overengineering before code review.
+
+## Completion Criteria
+
+- Every replacement event/task worker rebuilds dotenv/config, replays tracked mutations, and refreshes every registered provider implementing `ReloadsConfiguration` before readiness.
+- Programmatic and CLI reloads share one strict `ServerReloader` implementation.
+- Config-derived objects resolved before fork either rebuild lazily or update in place according to their real retainers.
+- Registered HTTP connection presets survive the fork while every process builds its own transport handlers.
+- Cache and Rate Limiter reject Swoole table definitions that were not initialized before the server fork instead of creating process-private state.
+- Pure derived config is reevaluated from current worker inputs; recorded snapshots remain only for master-installed provider and server topology.
+- No optional hook constructs a service merely to refresh it.
+- Request-scoped state, custom manager extensions, routes, callbacks, and retained object identities remain intact where required.
+- Auth/Sanctum validation and cache policy finalization run after configuration refresh.
+- Reload failures are explicit and fail fast; no retry, rollback, polling, registry, or readiness state machine is added.
+- No supported Laravel API is removed or narrowed. New APIs use Laravel-style names and boot-only warnings.
+- The feature adds work only during worker startup and explicit reload commands, with no request hot-path overhead.
+- Documentation accurately distinguishes reloadable worker configuration from restart-owned process topology.
+- Changed test files, focused package suites, `composer fix`, self-review, and peer code review are green.
diff --git a/docs/plans/2026-08-14-1721-container-named-api-and-array-access-removal.md b/docs/plans/2026-08-14-1721-container-named-api-and-array-access-removal.md
new file mode 100644
index 000000000..ce2935ae8
--- /dev/null
+++ b/docs/plans/2026-08-14-1721-container-named-api-and-array-access-removal.md
@@ -0,0 +1,385 @@
+# Container named API and ArrayAccess removal plan
+
+## Status and objective
+
+Design consensus and peer plan review are complete; `claude-array-access` signed off after three review passes. Implementation is authorized and in progress.
+
+Hypervel 0.4 will intentionally omit Laravel's container `ArrayAccess` API and the equivalent dynamic service properties. Container registration, lookup, and lifecycle changes will use named methods throughout the framework and its tests. The change also tightens the facade application boundary to the container contract, fixes temporary override cleanup, and records the public incompatibility where users and future Laravel ports will find it.
+
+The owner approved the exact test deletion and consolidation list, applied the protected `AGENTS.md` update after peer signoff, and approved removing the inherited scalar `concurrency.driver` compatibility path.
+
+## Final design
+
+### Public contract
+
+- `Hypervel\Contracts\Container\Container` extends only PSR-11 `ContainerInterface`. `Hypervel\Contracts\Foundation\Application` inherits this change transitively.
+- `Hypervel\Container\Container` implements only the Hypervel container contract. Remove its `ArrayAccess` import, interface, four `offset*` methods, and `__get` / `__set` service accessors.
+- Do not add a deprecation shim, compatibility trait, helper, or custom error. Native PHP errors and warnings expose unsupported array and dynamic-property use without a compatibility fallback.
+- Do not add `forgetBinding()`. No production caller needs arbitrary binding deletion, and the removed `offsetUnset()` semantics are not a sound public operation: they clear selected binding, resolution, lifecycle, instance, and alias state while leaving tags, extenders, contextual bindings, rebound callbacks, and aliases that may target the canonical key.
+- Keep `make()` for Hypervel resolution and `get()` for PSR-11 callers. Use `bound()` for Hypervel binding checks and `has()` where the variable is intentionally PSR-11-shaped.
+
+The concrete class keeps a concise marker at the former offset-method location:
+
+```php
+// Hypervel intentionally omits container array and dynamic property access.
+// Use named methods so resolution and binding lifecycles remain explicit.
+```
+
+This is the source marker required for an intentionally omitted Laravel feature. `flushState()` remains the last method after the removed trailing offset and magic-method block.
+
+### Why this is the clean boundary
+
+- `ArrayAccess` is suitable for real map or collection objects; it is not deprecated or generally bad PHP. The problem is the container mapping: reads perform resolution, writes silently select transient binding semantics, and unset mutates several lifecycle stores.
+- `make()` carries Hypervel's conditional class-string generic return. `offsetGet(): mixed` loses that information and cannot accept resolution parameters.
+- `$app[$key] = $value` hides whether the caller intends a transient factory or a shared existing value. Named `bind()` and `instance()` make that lifecycle choice reviewable.
+- `$app->service` and `$app->service = $value` are even less discoverable and can be confused with declared properties.
+- PSR-11 exposes `get()` and `has()`. Current PHP-DI, Symfony DI, and League Container implementations do not implement `ArrayAccess`.
+- Laravel 13 retains ArrayAccess and dynamic properties on its concrete container, so documentation must not call them deprecated. Laravel's contract omits `ArrayAccess`, its current container guide documents named methods rather than array or dynamic-property access, and Laravel rejected adding `ArrayAccess` to the contract because it is a separate concern that is not required for a container.
+- Hypervel 0.4 is the correct hard-break point. A compatibility layer would allow ported and application code to keep introducing the syntax this design is meant to remove.
+
+### Named-method migration rules
+
+Audit every candidate; do not bulk replace it.
+
+| Existing use | Replacement |
+|---|---|
+| `$container[$abstract]` | `$container->make($abstract)` |
+| `isset($container[$abstract])` | `$container->bound($abstract)`, or `has()` for an intentionally PSR-11-typed value |
+| `$container[$abstract] = $closure` | `$container->bind($abstract, $closure)` |
+| `$container[$abstract] = $objectOrValue` | `$container->instance($abstract, $objectOrValue)` when the caller supplies one shared value; retain `bind(fn () => $value)` only when transient binding and rebinding behavior are intentional |
+| `unset($container[$abstract])` | Refactor around the caller's actual lifecycle. The verified temporary overrides use `forgetInstance()` |
+| `$container->service` | `$container->make('service')` |
+| `$container->service = $value` | Explicit `instance()` or `bind()` after reviewing lifetime intent |
+
+For config, resolve the repository explicitly. Use typed getters only when presence and type are guaranteed. Keep `get()` plus the existing fallback or type guard when current behavior accepts a missing or wrong-typed value. When `null` intentionally selects or disables behavior, preserve that meaning and ensure the null branch has a focused behavioral test.
+
+`offsetSet()` currently does fire rebound callbacks: `bind()` calls `registerBinding()`, which calls `rebound()` for an already-resolved abstract. Conversions that must preserve offset assignment semantics use `bind()`, not `instance()`.
+
+## Implementation
+
+### 1. Remove the API at its owning boundary
+
+Update one file at a time:
+
+1. `src/contracts/src/Container/Container.php`
+ - Remove the `ArrayAccess` import and parent interface.
+ - Keep the existing PSR and Hypervel named method surface unchanged.
+2. `src/container/src/Container.php`
+ - Remove the `ArrayAccess` import and implemented interface.
+ - Remove `offsetExists()`, `offsetGet()`, `offsetSet()`, `offsetUnset()`, `__get()`, and `__set()`.
+ - Add the concise intentional-removal marker at the former method block.
+ - Do not alter `bound()`, `resolved()`, `bind()`, `instance()`, `forgetInstance()`, alias handling, or cached-instance internals as part of the removal.
+
+The `isset(...) || array_key_exists(...)` pairs in `bound()`, `resolved()`, and `resolve()` remain. They deliberately support `instance($abstract, null)` while retaining an `isset()` fast path.
+
+### 2. Convert production consumers
+
+The verified production migration surface is grouped below. Each file must be read, changed, and checked separately. A name-independent receiver sweep found three caller shapes missed by the original app/container-name search: 21 `$this->hypervel[...]` reads across 13 console-command files, 10 `$this[...]` accesses in `Application`, and one local `$hypervel[...]` read in Testbench `UsesVendor`. The two other `$this[...]` source hits are inside the container magic accessors removed at the owning boundary.
+
+| Area | Files | Required conversion |
+|---|---|---|
+| Cache console | `src/cache/src/Console/ClearCommand.php` | Resolve events through `$this->hypervel->make()`. |
+| Concurrency | `src/concurrency/src/ConcurrencyManager.php` | Resolve config explicitly and apply the approved default/per-driver key correction described below. |
+| Console | `src/console/src/Concerns/ConfiguresPrompts.php`, `src/console/src/Concerns/CreatesMatchingTest.php` | Resolve validator and path services through `$this->hypervel->make()`. |
+| Database | `src/database/src/Capsule/Manager.php`, `src/database/src/Console/Migrations/MigrateCommand.php`, `src/database/src/Console/Migrations/RefreshCommand.php`, `src/database/src/Console/WipeCommand.php`, `src/database/src/DatabaseServiceProvider.php`, `src/database/src/Migrations/Migrator.php` | Use `make()` for config, database, filesystem, event, migration, and dispatcher services. Console commands use `$this->hypervel->make()`. Preserve config repository writes as config writes. |
+| Filesystem | `src/filesystem/src/FilesystemManager.php` | Resolve the URL service with `make()`. |
+| Foundation | `src/foundation/src/Application.php`, `src/foundation/src/Bootstrap/HandleExceptions.php`, `src/foundation/src/Console/ConfigCacheCommand.php`, `src/foundation/src/Console/EnvironmentCommand.php`, `src/foundation/src/Console/Kernel.php`, `src/foundation/src/Console/RouteCacheCommand.php`, `src/foundation/src/Console/RouteListCommand.php`, `src/foundation/src/Http/Kernel.php`, `src/foundation/src/Providers/FoundationServiceProvider.php`, `src/foundation/src/Support/Providers/RouteServiceProvider.php` | Replace config, event, environment, file, router, and URL-generator reads with named methods. Console commands use `$this->hypervel->make()`; `Application` follows the lifecycle audit below. |
+| Foundation testing | `src/foundation/src/Testing/Concerns/InteractsWithAuthentication.php`, `src/foundation/src/Testing/Concerns/InteractsWithSession.php`, `src/foundation/src/Testing/Concerns/MakesHttpRequests.php`, `src/foundation/src/Testing/DatabaseTruncation.php`, `src/foundation/src/Testing/WithConsoleEvents.php` | Use named service reads. Apply the temporary middleware override fix described below. |
+| Logging | `src/log/src/Context/ContextServiceProvider.php`, `src/log/src/LogManager.php` | Resolve events with `make()`. |
+| Queue | `src/queue/src/Console/ClearCommand.php`, `src/queue/src/Console/RetryCommand.php`, `src/queue/src/Console/WorkCommand.php`, `src/queue/src/QueueManager.php`, `src/queue/src/SyncQueue.php` | Resolve event, queue, and cache services with `make()`; console commands use `$this->hypervel->make()`. |
+| Sentry | `src/sentry/src/SentryServiceProvider.php` | Apply the typed user-configuration conversion below. |
+| Support | `src/support/src/ServiceProvider.php` | Replace dynamic config-property access using the behavior-preserving snippet below. |
+| Support facades | `src/support/src/Facades/Cookie.php`, `src/support/src/Facades/Date.php`, `src/support/src/Facades/Facade.php`, `src/support/src/Facades/Queue.php`, `src/support/src/Facades/Schema.php`, `src/support/src/Facades/Storage.php` | Use named container methods and apply the typed facade boundary described below. |
+| Testbench | `src/testbench/src/Attributes/UsesVendor.php`, `src/testbench/src/Concerns/HandlesDatabases.php`, `src/testbench/src/Concerns/InteractsWithPublishedFiles.php` | Resolve the cloned application's guaranteed vendor-symlink flag, events, and files with `make()`. Remove `UsesVendor`'s unreachable null fallback after `CreateVendorSymlink::handle()` registers the boolean instance. |
+| Testing | `src/testing/src/Concerns/TestCaches.php`, `src/testing/src/Concerns/TestViews.php`, `src/testing/src/PendingCommand.php` | Resolve config/compiler services by method and apply the temporary console-output override fix below. |
+| Validation | `src/validation/src/ValidationServiceProvider.php` | Replace offset reads with `make()` and the two-entry `isset` check with explicit `bound()` checks before resolving. |
+
+Keep these verified ordinary-array uses unchanged even though the variables resemble containers:
+
+- `src/reverb/src/ConfigApplicationProvider.php`
+- `src/server/src/ServerManager.php`
+- `src/coordinator/src/CoordinatorManager.php`
+- `src/di/src/Aop/AspectManager.php`
+- `src/foundation/src/PackageManifest.php` (`$hypervel` is the validated Composer `extra.hypervel` array)
+
+#### Console command application access
+
+`Command::$hypervel` is nullable during construction, but `Console\Application::addCommand()` and `freshCommandForRun()` inject it before command execution. `Command` and its subclasses already use the direct property throughout their execution paths, matching Laravel's command idiom. Convert offset reads directly to `$this->hypervel->make(...)`; do not introduce `getHypervel()` only for converted calls and create two styles in one method. When the same resolved service is reused, store that service in a local variable instead of aliasing the application property.
+
+#### Application self-access
+
+`Hypervel\Foundation\Application` extends the container, so its ten `$this[...]` uses are container access even though they have no receiver variable name. Convert event and environment reads to `$this->make(...)`. In `detectEnvironment()`, register the detected string as the shared application environment and preserve the return value:
+
+```php
+return $this->instance('env', (new EnvironmentDetector)->detect($callback, $args));
+```
+
+`instance()` returns the supplied value and expresses the environment's actual shared-value lifetime. It replaces the current hidden `bind('env', fn () => $value)` selected by offset assignment. Extend one existing `detectEnvironment()` integration case to assert that `environment()` returns the detected value; do not add a separate test method.
+
+#### Inherited concurrency configuration defect
+
+`ConcurrencyManager` inherits three incompatible uses of `concurrency.driver` from Laravel:
+
+- `getDefaultInstance()` reads `concurrency.driver` as a legacy scalar fallback after `concurrency.default`.
+- `setDefaultInstance()` writes both `concurrency.default` and scalar `concurrency.driver`.
+- `getInstanceConfig()` reads `concurrency.driver.{$name}` as a per-driver array.
+
+Writing the default can therefore replace the entire per-driver map. The shipped Hypervel and Laravel configs define only `concurrency.default`, and current Laravel contains the same collision. Laravel history shows the shipped configuration moving from `driver` to `default` in `50205f4a13`, followed by `15e0224750` (`support driver`) adding the dual scalar read/write; the earlier `fbef34c77b` index change also explains why per-driver configuration now occupies the singular `driver` key. Hypervel 0.4 has never shipped the legacy scalar key. `AGENTS.md` says not to port backwards-compatibility shims for versions or features Hypervel does not support, so removing this path follows existing porting policy rather than introducing a novel divergence.
+
+The owner approved the clean 0.4 behavior: use `concurrency.default` exclusively for the default, read it with the typed `string()` getter without duplicating the shipped config default in code, and make `setDefaultInstance()` write only that key. A missing or misspelled merged key must fail at the config boundary instead of silently selecting `coroutine`. Keep the existing `concurrency.driver.{$name}` location for per-driver configuration so fixing the collision does not invent a second schema change. Add a regression proving that changing the default does not overwrite configured per-driver data.
+
+#### Typed Sentry configuration
+
+`SentryServiceProvider::getUserConfig()` promises an array. Resolve config explicitly and use:
+
+```php
+return $this->app->make('config')->array(static::$abstract);
+```
+
+Remove the current `empty()` ternary. `register()` merges the package config before this method is called, so the root key is guaranteed and must not duplicate the package default in code. A missing or invalid non-array value now fails at the typed configuration boundary with `InvalidArgumentException` instead of being hidden or flowing to a return-type `TypeError`.
+
+#### View-path compatibility
+
+`ServiceProvider::loadViewsFrom()` currently skips a missing or non-array `view.paths` value. `Repository::array()` would throw and change that behavior. Use:
+
+```php
+$config = $this->app->make('config');
+
+if (is_array($viewPaths = $config->get('view.paths'))) {
+ foreach ($viewPaths as $viewPath) {
+```
+
+The single `is_array()` check covers the current missing-value case because `is_array(null)` is false.
+
+#### Temporary middleware overrides
+
+`withoutMiddleware()` installs either `middleware.disable` or selected fake middleware through `instance()`. Replace `withMiddleware()`'s unsets with `forgetInstance()`.
+
+This is a bug fix as well as an API conversion. An explicit binding survives `instance()`, but current `offsetUnset()` deletes it. `forgetInstance()` removes the fake and restores the original binding and lifecycle. It also deliberately preserves the abstract's `resolved()` history, so a later `bind()` or `extend()` may fire rebinding callbacks where destructive offset-unset cleanup did not.
+
+Add one regression to `tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php`: pre-bind middleware with behavior that cannot be recovered by zero-configuration construction, call `withoutMiddleware($class)` and `withMiddleware($class)`, then prove the original binding remains and resolves again. Keep the existing formerly-unbound and global-disable cases.
+
+#### Temporary console output
+
+`PendingCommand::mockConsoleOutput()` creates one mock object. Register it with `instance(OutputStyle::class, $mock)` and replace the `finally` cleanup with `forgetInstance(OutputStyle::class)`.
+
+Do not add a dedicated test. Existing `tests/Console/ArtisanCommandTest.php` console paths exercise the cleanup and assert that `OutputStyle::class` is no longer bound. `ContainerTest::testForgettingTemporaryInstanceRestoresScopedLifecycle` already proves that an instance override can be forgotten while the original scoped registration survives.
+
+### 3. Type the facade application boundary
+
+`Facade` stops accepting an untyped ArrayAccess duck type once resolution uses `make()`. Make the real requirement native and nullable:
+
+```php
+protected static ?ContainerContract $app = null;
+
+public static function getFacadeApplication(): ?ContainerContract;
+
+public static function setFacadeApplication(?ContainerContract $app): void;
+```
+
+`resolveFacadeInstance()` uses `static::$app->make($name)`. The existing truthy guard narrows the nullable property there.
+
+Eight nullable-boundary sites deliberately fail if no facade application has been set. Preserve that fail-fast behavior without guards or a throwing helper. At each site, copy `static::$app` or `static::getFacadeApplication()` to a local variable narrowed with `/** @var ContainerContract $app */`, then use the container:
+
+- `Facade.php`: `resolved()`
+- `Cookie.php`: `has()` and `get()`
+- `Schema.php`: `connection()`
+- `Storage.php`: `fake()`, `persistentFake()`, and `buildDiskConfiguration()`
+- `Queue.php`: the `QueueFake` constructor call in `fake()`
+
+Do not make `QueueFake` nullable. `Queue::fake()` already requires a facade root and therefore a container.
+
+`Date::resolveFacadeInstance()` replaces its nested app/offset `isset` with the equivalent `static::$app === null || ! static::$app->bound($name)` fallback condition.
+
+In `Storage::fake()` and `persistentFake()`, read the guaranteed default disk with the config repository's `string('filesystems.default')` getter. `buildDiskConfiguration()` must keep tolerant nested config behavior:
+
+```php
+$originalConfig = $app->make('config')->get("filesystems.disks.{$disk}") ?? [];
+```
+
+Do not use `array(..., [])`; the existing code does not eagerly reject a non-array value before reading its optional `throw` offset.
+
+Update `tests/Support/SupportFacadeTest.php` deliberately:
+
+- `ApplicationStub` extends concrete `Hypervel\Container\Container` instead of implementing `ArrayAccess`.
+- Replace the ArrayAccess-era `setAttributes()` helper with `setInstances()`, registering each supplied key through `instance()`.
+- `CountingApplicationStub` counts `make()` calls using the exact parent signature `public function make(string $abstract, array $parameters = []): mixed`, forwards `$parameters`, and exposes a clearly named count.
+- Keep the uncached-facade assertion: it must prove two container resolutions.
+- Add or retain coverage that `setFacadeApplication(null)` is supported.
+
+All other inspected facade setup callers pass a concrete Hypervel container, an Application contract implementation, or `null` and require no compatibility layer.
+
+### 4. Migrate tests without hiding lifecycle choices
+
+Generate the candidate list with broad `grep` searches across all `tests/`, then inspect it one file at a time. After changing each test file, immediately run that file before moving to the next.
+
+For tests where bracket syntax is only setup:
+
+- Replace existing object and scalar registrations with `instance()`. This applies to config repositories, environment strings, cache doubles, storage paths, boolean provider flags, and similar fixed values.
+- Replace closure registrations with `bind()`.
+- Replace reads with `make()` and existence checks with `bound()`.
+- Preserve config fallbacks and guards using the same typed-getter rule as source.
+- Add `: void` to every test method whose body is changed, as required by the repository's test typing rules.
+- Do not touch a value merely because its local variable is named `$app` or `$container`. For example, `tests/Integration/Http/RequestBindingTest.php` obtains a plain array from `config()->array('app')`; its `unset($app['url'])` remains unchanged. The reflected coordinator registry arrays in `tests/Coordinator/TimerTest.php` also remain arrays.
+- Keep the local `$hypervel = []` Composer-metadata array in `tests/Testing/PHPUnit/TestStateRegistrarsTest.php`; it is distinct from the cloned application named `$hypervel` in Testbench `UsesVendor`.
+- Convert the two cloned-application reads in `tests/Testbench/Foundation/Bootstrap/CreateVendorSymlinkTest.php` from `$application['TESTBENCH_VENDOR_SYMLINK']` to `make()`. They are real container reads, not Composer metadata arrays.
+
+Fixed-value assignment sites that need an explicit `instance()` audit include:
+
+- `tests/Support/SupportCapsuleManagerTraitTest.php`
+- `tests/Integration/Horizon/Feature/MonitorSupervisorMemoryTest.php`
+- `tests/Integration/Horizon/Feature/MonitorMasterSupervisorMemoryTest.php`
+- `tests/Integration/Foundation/FoundationServiceProvidersTest.php`
+- `tests/Integration/Encryption/KeyGenerateCommandTest.php`
+- `tests/Telescope/FeatureTestCase.php`
+- `tests/Cache/ClearCommandTest.php`
+- `tests/Log/LogManagerTest.php`
+- `tests/Foundation/FoundationHelpersTest.php`
+- `tests/Foundation/FoundationDevCommandsTest.php`
+- `tests/Foundation/Testing/DatabaseTruncationTest.php`
+- `tests/Testbench/Fixtures/Providers/ParentServiceProvider.php`
+- `tests/Testbench/Fixtures/Providers/ChildServiceProvider.php`
+
+Convert the four real magic config accesses in `tests/Support/SupportMaintenanceModeTest.php` to an explicitly resolved config repository. Do not change declared test properties such as `bootstrapFile`, `frameworkBootstrapCount`, or `waiterEntered`.
+
+The two queue payload fixtures, `tests/Integration/Queue/CustomPayloadTest.php` and `tests/Testbench/TestbenchTest.php`, should register the generated one-time password with `instance()` and clear it with `forgetInstance()`. Their purpose is to expose a stale static payload callback closing over a previous Application after its one-time entry has been cleared. The eager test-fixture value does not change that behavior.
+
+#### Container test changes
+
+The owner approved this exact list:
+
+| Current test | Change | Preserved coverage / marker |
+|---|---|---|
+| `ContainerTest::testArrayAccess` | Remove | Sole subject is the omitted interface. Leave `// REMOVED: Container ArrayAccess is intentionally unsupported; use named container methods.` at the matching upstream location. |
+| `ContainerTest::testUnsetRemoveBoundInstances` | Consolidate into `testForgetInstanceForgetsInstance` | Add `bound()` assertions before and after `forgetInstance()`. Leave a `REMOVED:` marker at the upstream method location naming the destination coverage. |
+| `ContainerTest::testBoundInstanceAndAliasCheckViaArrayAccess` | Keep under a named-method-oriented name | Replace `isset` with `bound()` for the instance and alias. Record the upstream-name mapping in the plan only; no removal marker. |
+| `ContainerTest::testOffsetUnsetClearsScopedInstance` | Remove | Hypervel-original duplicate of existing `testForgetInstanceForgetsScopedInstance`; no upstream marker. |
+| `ContainerTest::testOffsetUnsetClearsScopedLifecycleMarker` | Remove | Hypervel-original assertion for intentionally removed destructive unset behavior. `forgetInstance()` correctly preserves scoped lifecycle; an explicit non-scoped `bind()` changes lifecycle. No upstream marker. |
+| `ContainerTest::testContainerCanDynamicallySetService` | Remove | Sole subject is offset existence/set/get. Leave the same concise ArrayAccess `REMOVED:` marker at this separate upstream location. |
+
+This produces three `REMOVED:` marker comments across four accounted upstream methods. The renamed bound/alias test remains executable but will require manual matching if its upstream-named counterpart changes.
+
+In `tests/Container/ContainerExtendTest.php`, convert offset-assignment setup to explicit transient `bind()` closures. Keep the current unset/extender test as a `forgetExtenders()` test and remove only the obsolete unset step.
+
+### 5. Document the intentional difference
+
+#### Package README
+
+Update `src/container/README.md` in the required README order:
+
+- Keep the header and badge.
+- Add `Documentation: https://hypervel.org/docs/container` because the package has a meaningful public documentation page.
+- Add `Differences From Laravel` explaining that Hypervel omits container ArrayAccess and dynamic service properties, and directs users to `make()` / `get()`, `bound()` / `has()`, `bind()`, `instance()`, and `forgetInstance()` for temporary overrides. State that arbitrary binding removal is not exposed because registrations are worker-global boot-time state.
+- Add `Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Container` after the difference section, matching the component-tree form used by Laravel-derived package READMEs.
+
+Do not describe ArrayAccess as deprecated or claim Laravel discourages it. State the verified current facts: Laravel retains the concrete API, while its contract and current documentation use named methods.
+
+#### Porting guide
+
+Update `src/docs/porting-from-laravel.md` concisely at `Container Lifecycles`, where it currently says Hypervel's container has Laravel's public shape:
+
+- State that Hypervel intentionally supports named container methods but not Laravel's ArrayAccess or dynamic service properties.
+- Give compact porting mappings for read, existence, closure registration, fixed-instance registration, and temporary-instance cleanup.
+- Keep the lifecycle table and worker-lifetime explanation as the authoritative lifecycle guidance.
+- Do not add internal implementation detail, repository migration counts, edge-case history, or repeated rationale.
+
+#### Main container guide
+
+Update the introductory Laravel-comparison sentence in `src/docs/container.md`. It currently says the container's bindings and resolution helpers all behave like Laravel's, which becomes too broad after this public API removal. Keep the useful lifecycle comparison, but say that Hypervel follows Laravel's named binding and resolution APIs while documenting intentional public API differences in the porting guide. Do not duplicate the mappings or rationale here. No `src/docs` container example currently uses the removed syntax.
+
+#### Release overview
+
+Broaden the single intentional-differences clause in `src/docs/releases.md`'s `Laravel-Style Package Ports` section so it also says Hypervel may omit Laravel public APIs whose semantics do not suit Hypervel. Keep this at one high-level clause; do not add an API-specific subsection.
+
+Do not add an `src/docs/upgrade.md` entry. That guide is for Hypervel 0.3-to-0.4 applications, and the Hyperf container used by 0.3 did not implement ArrayAccess or the removed offset methods. Keep the separate `porting-from-laravel.md` entry for Laravel developers. Also leave `porting-from-laravel.md`'s statement about supported public binding methods unchanged; `bind()`, `singleton()`, `scoped()`, and provider `bindings` / `singletons` properties remain supported.
+
+#### Protected agent guide
+
+The owner approved and applied a deliberately minimal `AGENTS.md` update after peer signoff. It changes four conceptual locations so the guide never instructs future ports to use a removed API:
+
+1. `Container`: say Hypervel keeps Laravel's named API surface and intentionally omits array and dynamic service access.
+2. `Porting Packages > Policy`: require ported container array and dynamic-property access to use named methods.
+3. `Porting Laravel Tests > Approved unsupported features`: add container ArrayAccess and dynamic service properties to the exhaustive list so future tests whose sole subject is those APIs are removed with the required markers.
+4. `Development Conventions`: clarify in the existing typed-config rule that code must not supply a second fallback when framework or package config defines the key, so missing or misspelled keys fail loudly.
+
+The quick checklist now points to the exhaustive list instead of duplicating it. No method mapping, compatibility rationale, or arbitrary binding-removal guidance was added; those unneeded instructions would increase context and the risk of misinterpretation without addressing an observed agent failure.
+
+### 6. Keep verified generic behavior unchanged
+
+- `src/facade-documenter/facade.php::fulfillsBuiltinInterface()` remains generic. Its ArrayAccess filter still applies to real ArrayAccess facade targets and simply stops matching the App container; no generated App offset methods exist.
+- `src/testbench/src/Bootstrapper.php` keeps `static::$configuration?->offsetExists('hypervel')` and `static::$configuration['hypervel']`. That value implements `Hypervel\Testbench\Contracts\Config`, which intentionally extends ArrayAccess; it is not the application container.
+- Genuine ArrayAccess APIs elsewhere in Hypervel—collections, config repositories, HTTP responses, requests, data objects, views, cache tag wrappers, and similar map-like objects—remain supported and documented.
+- Preserve `_archive` unchanged. It is a parked historical snapshot scheduled for deletion after 0.4, not active framework source, and is excluded from migration and search gates.
+- Private package and application source had no real container array or dynamic-property callers after excluding vendor, generated, and reference-copy directories. Re-run that verification when implementing, but do not edit third-party or generated trees.
+
+## Testing and verification
+
+### Per-file cadence
+
+- After each changed test file: `./vendor/bin/phpunit --no-progress path/to/Test.php`.
+- Run `tests/Container/ContainerTest.php` immediately after its source/test edits, then `tests/Container/ContainerExtendTest.php`.
+- Run `tests/Support/SupportFacadeTest.php` after the facade boundary and test-double conversion.
+- Run `tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php` after the middleware cleanup fix.
+- Run `tests/Console/ArtisanCommandTest.php` after `PendingCommand` changes even if the test file itself does not change.
+- Run `tests/Foundation/FoundationApplicationTest.php` and `tests/Foundation/ApplicationRunningInConsoleTest.php` after converting `Application` self-access and extending the stored-environment assertion.
+- Run `tests/Console/ConfiguresPromptsTest.php`, `tests/Console/GeneratorCommandTest.php`, and each affected package's matching command tests after the console-command conversions.
+- Run `tests/Testbench/Attributes/UsesVendorTest.php` after converting the cloned application read.
+- Run `tests/Concurrency/ConcurrencyTest.php` immediately after the approved manager and regression-test changes.
+- Run the queue custom-payload and Testbench files immediately after each fixture edit.
+- Run `composer test:testbench` after the Testbench source/test slice.
+
+### Search gates
+
+Use broad Bash `grep` searches across the whole `src/` and `tests/` trees:
+
+- Confirm the Hypervel container contract and concrete class no longer import, extend, or implement `ArrayAccess` and define no `offset*`, `__get`, or `__set` methods.
+- Before implementation and again as the closing backstop, rank every bracket receiver without assuming container variable names:
+
+ ```bash
+ grep -rhoE -e '(\$this->[a-zA-Z_][a-zA-Z0-9_]*|static::\$[a-zA-Z_][a-zA-Z0-9_]*|\$[a-zA-Z_][a-zA-Z0-9_]*)\[' src tests --include='*.php' \
+ | sed -E 's/\[$//' | sort | uniq -c | sort -rn
+ ```
+
+ Inspect the complete ranked output for any receiver that is or may be a container. This discovery step is mandatory; the original fixed-name search missed both `$this->hypervel` and container self-access.
+- Search bracket access through the known container identifier family: `$this->app`, `$app`, `static::$app`, `$this->application`, `$application`, `$this->container`, `$container`, `$this->hypervel`, and `$hypervel`. Also search bare `$this[...]` across all source and tests; the current twelve hits are the ten `Application` callers plus the two container magic-accessor implementations, so none should remain after this change. Inspect every other remainder; only genuine array or non-container ArrayAccess values may remain. In source, the known ordinary-array files are the Reverb config provider, ServerManager, CoordinatorManager, AspectManager, and `PackageManifest`; Testbench `Bootstrapper::$configuration` is the known non-container ArrayAccess value.
+- Search direct `offsetExists/Get/Set/Unset` calls on app/container variables; none may remain.
+- Run a shape-based magic-property search across source and tests, including `app`, `application`, `container`, and `hypervel` identifiers:
+
+ ```bash
+ grep -RInP --include='*.php' '(?:->(?:app|application|container|hypervel)|\$(?:app|application|container|hypervel))->[a-z_][A-Za-z0-9_]*(?![A-Za-z0-9_]|\s*\()' src tests
+ ```
+
+ Inspect every hit. No container service property may remain; declared fixture or test-double properties such as `bootstrapFile`, `frameworkBootstrapCount`, `middlewarePriority`, `waiterEntered`, and parallel-runner tracking fields remain valid.
+- Search active `src/docs`, package READMEs, and the porting guide for stale claims or examples that say container array/dynamic access is supported; exclude `_archive` and historical plan files.
+- Repeat the same checks across `apps/`, `packages/hypervel/`, and `packages/hypervel-dev/`, excluding dependency, generated, temporary-reference, and storage trees; any real caller must be converted under the same rules.
+
+### Final checks
+
+After all targeted tests are green, run `composer fix` once from the worktree root. It runs formatting, both PHPStan configurations, the full parallel suite, Testbench package-mode tests, and the dogfood package tests.
+
+If `composer fix` fails:
+
+1. Investigate the exact failure and apply only verified fixes.
+2. Run the corrected targeted test/check.
+3. Inspect the current `fix` script and run the failed entry plus every remaining entry in order. Do not repeat an earlier passing entry unless the fix can affect it.
+4. Re-run the search gates after formatter or review fixes.
+
+## Implementation order
+
+1. Remove ArrayAccess from the contract and concrete container, including magic accessors and the required source marker.
+2. Convert production consumers one file at a time, including middleware/PendingCommand lifecycle fixes and facade typing fallout.
+3. Convert tests one file at a time, running each file immediately and leaving verified ordinary arrays unchanged.
+4. Update the container README, main container guide, porting guide, and release overview with targeted edits.
+5. Run focused suites, search gates, `composer test:testbench` for the Testbench slice, and one full `composer fix` checkpoint.
+6. Re-read this plan, inspect the final diff file by file for stale compatibility code or documentation, and request code review before handoff.
+
+## Primary references
+
+- Laravel 13 container documentation:
+- Laravel 13 concrete container:
+- Laravel 13 container contract:
+- Laravel discussion rejecting ArrayAccess on the contract:
+- PSR-11 specification and rationale: and
+- Current PHP-DI, Symfony DI, and League Container concrete sources, checked only to establish that method-only containers are normal modern PHP practice.
+
+Historical wording must stay narrow: ArrayAccess was present in the first tagged Illuminate container, while Laravel 3.2 used its own static IoC class without it. Do not call the API Pimple inheritance.
diff --git a/docs/todo.md b/docs/todo.md
index a3740b985..bcc7979c8 100644
--- a/docs/todo.md
+++ b/docs/todo.md
@@ -22,7 +22,6 @@
- Convert the remaining tests that extend `PHPUnit\Framework\TestCase` to `Hypervel\Tests\TestCase` as required by `AGENTS.md`, verifying each file individually under coroutine execution and opting out only when the test explicitly exercises coroutine transitions.
- Design a connection-owned service identity and capability API for external backends that packages can query without repeated hot-path probes. Redis/Valkey and database connections already expose fragments of this information in different forms; prefer lazy detection cached for the current connection or pool generation, with invalidation on reconnect and purge, over an eager process-global startup registry that performs unused I/O or survives a backend change. Start with concrete consumers and capability checks rather than a universal version-comparison abstraction.
-- Find a clean, simple framework-wide solution for configuration-dependent services resolved before worker configuration reload. `server:reload` refreshes the existing configuration repository, but objects that have already copied configuration into their own state remain stale. For example, `SentryServiceProvider` eagerly resolves a worker-lifetime Hub and client during boot, so DSN, environment, and sampling changes are not applied until a full restart; resolving `Cache::store('some-store')` from a service provider populates `CacheManager`'s store cache before reload, so changes to that store's driver, connection, prefix, or other captured configuration are likewise not applied. Define the reload contract, audit framework-owned eager resolutions and manager caches, and solve the lifecycle at their shared owning boundary instead of adding package-specific refresh hooks or application workarounds.
- Convert container array access to `make()` across `src/`. About 40 files use `$app['...']` (e.g. `LogManager`, `ViewServiceProvider`, `TranslationServiceProvider`), carried over from upstream Laravel. `offsetGet()` always returns `mixed`, while `make()` has class-string generics phpstan can follow, so the conversion makes static analysis strictly more useful. Approved modernization per the Porting Packages policy in `AGENTS.md`; new code already follows the rule.
- Investigate where requiring and directly using a PHP extension would make framework code significantly faster than its current pure-PHP implementation. The framework already declares bundled extensions it depends on, so the question is which hot paths are doing in PHP what a C extension does natively. The worked example is `ext-gmp` for identifier encoding: UUID and ULID string conversion and any base32/base58/base62 short-id work exceed 64 bits, so `ramsey/uuid` and `symfony/uid` convert them digit by digit in PHP, while `gmp_init()`/`gmp_strval()` do arbitrary-base conversion natively — a hand-rolled base-36 UUID conversion measured 14.6 µs against 0.5 µs for the GMP equivalent with byte-identical output. Anything that fits in a 64-bit int (snowflakes, timestamps, counters) needs no extension, and hashing, encryption, and signatures are already C. Measure `Str::uuid()`/`Str::ulid()` and the other candidates before adding a requirement, and weigh each new extension against installation cost.
- Convert untyped `$config->get()` calls across `src/` to the typed getters (`string()`, `integer()`, `float()`, `boolean()`, `array()`) without call-site defaults, for every key that isn't genuinely nullable. Defaults live in the merged config files — declare any key currently defaulted only at a call site in its package's config file as part of the conversion. Typed getters throw `InvalidArgumentException` naming the key on misconfiguration instead of letting a wrong type propagate silently, and give phpstan real return types. Bootstrap code that runs before config merging keeps its call-site defaults. Approved modernization per the Porting Packages policy in `AGENTS.md`; new code already follows the rule.
@@ -41,7 +40,8 @@
## Documentation
-- Re-run the introduction benchmarks against Hypervel 0.4 before publishing externally. The benchmark tables currently preserve the 0.3 results so the comparison is not lost during the docs port, but Hypervel 0.4's decoupled runtime should have fresh measurements before those numbers are treated as current.
+- Publish reproducible Hypervel 0.4 benchmarks on a dedicated documentation page before linking them from the introduction. Record the framework, PHP, Swoole, and dependency versions; use the same hardware and load-generation conditions for every runtime; publish the benchmark applications and configuration; and include the raw results, collection date, and limitations. Do not reuse the Hypervel 0.3 results as current data. Once the page is published, add it to `src/docs/documentation.md` and link to it from the introduction.
+- When the Hypervel 0.4 documentation is published, replace the versioned GitHub source links in both the `hypervel/components` and `hypervel/hypervel` READMEs with the corresponding hypervel.org documentation URLs. The documentation's `{{version}}` cross-links only resolve on the published site, so readers who follow the current links land on pages whose internal navigation is broken.
## Redis
diff --git a/src/api-client/composer.json b/src/api-client/composer.json
index f9159fde1..aaa85cbb0 100644
--- a/src/api-client/composer.json
+++ b/src/api-client/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/api-client",
- "description": "The api client package for Hypervel.",
+ "description": "Reusable API client integrations for Hypervel applications.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/auth/composer.json b/src/auth/composer.json
index 67e13b1cf..b3e9ca9fb 100644
--- a/src/auth/composer.json
+++ b/src/auth/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/auth",
- "description": "The auth package for Hypervel.",
+ "description": "The Hypervel Auth package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/auth/src/AuthServiceProvider.php b/src/auth/src/AuthServiceProvider.php
index 2d5bef537..b41c306ed 100755
--- a/src/auth/src/AuthServiceProvider.php
+++ b/src/auth/src/AuthServiceProvider.php
@@ -11,6 +11,7 @@
use Hypervel\Contracts\Auth\Access\Gate as GateContract;
use Hypervel\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Hypervel\Contracts\Config\Repository as ConfigRepository;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Core\Events\AfterWorkerStart;
use Hypervel\Database\Eloquent\Builder as EloquentBuilder;
use Hypervel\Database\Eloquent\Collection as EloquentCollection;
@@ -25,7 +26,7 @@
use function Hypervel\Support\enum_value;
-class AuthServiceProvider extends ServiceProvider
+class AuthServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
private const int MAX_QUERY_ATTRIBUTE_LENGTH = 63;
@@ -42,6 +43,19 @@ public function register(): void
$this->commands([ClearResetsCommand::class]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared resolved guards while
+ * concurrent coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('auth')) {
+ $this->app->make('auth')->forgetGuards();
+ }
+ }
+
/**
* Bootstrap the service provider.
*/
diff --git a/src/auth/src/Passwords/PasswordBrokerManager.php b/src/auth/src/Passwords/PasswordBrokerManager.php
index 583f1d0db..8dc2c8845 100644
--- a/src/auth/src/Passwords/PasswordBrokerManager.php
+++ b/src/auth/src/Passwords/PasswordBrokerManager.php
@@ -181,6 +181,19 @@ public function setDefaultDriver(UnitEnum|string $name): void
CoroutineContext::set(self::DEFAULT_BROKER_CONTEXT_KEY, $name);
}
+ /**
+ * Forget all resolved password brokers.
+ *
+ * Boot or tests only. Mutates the singleton's broker cache; concurrent
+ * coroutines may already hold a broker that next resolution will replace.
+ */
+ public function forgetBrokers(): static
+ {
+ $this->brokers = [];
+
+ return $this;
+ }
+
/**
* Refresh the event dispatcher on resolved brokers.
*
diff --git a/src/auth/src/Passwords/PasswordResetServiceProvider.php b/src/auth/src/Passwords/PasswordResetServiceProvider.php
index b76effbf2..3e179cf87 100755
--- a/src/auth/src/Passwords/PasswordResetServiceProvider.php
+++ b/src/auth/src/Passwords/PasswordResetServiceProvider.php
@@ -4,9 +4,10 @@
namespace Hypervel\Auth\Passwords;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Support\ServiceProvider;
-class PasswordResetServiceProvider extends ServiceProvider
+class PasswordResetServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -17,6 +18,19 @@ public function register(): void
$this->registerEventRebindHandler();
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared resolved brokers while
+ * concurrent coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('auth.password')) {
+ $this->app->make('auth.password')->forgetBrokers();
+ }
+ }
+
/**
* Register the password broker instance.
*/
diff --git a/src/boost/composer.json b/src/boost/composer.json
index 31e95b93b..b6546d79b 100644
--- a/src/boost/composer.json
+++ b/src/boost/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/boost",
"type": "library",
- "description": "The boost package for Hypervel.",
+ "description": "AI agent tools and guidelines for Hypervel applications.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/broadcasting/composer.json b/src/broadcasting/composer.json
index 9f94af07e..17b6d6715 100644
--- a/src/broadcasting/composer.json
+++ b/src/broadcasting/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/broadcasting",
"type": "library",
- "description": "The broadcasting package for Hypervel.",
+ "description": "The Hypervel Broadcasting package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/broadcasting/src/BroadcastServiceProvider.php b/src/broadcasting/src/BroadcastServiceProvider.php
index cd6718c9f..7775896c0 100644
--- a/src/broadcasting/src/BroadcastServiceProvider.php
+++ b/src/broadcasting/src/BroadcastServiceProvider.php
@@ -6,9 +6,10 @@
use Hypervel\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
use Hypervel\Contracts\Broadcasting\Factory as BroadcastingFactory;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Support\ServiceProvider;
-class BroadcastServiceProvider extends ServiceProvider
+class BroadcastServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -24,4 +25,19 @@ public function register(): void
BroadcastingFactory::class
);
}
+
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared broadcast connections while
+ * concurrent coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(BroadcastManager::class)) {
+ $this->app->make(BroadcastManager::class)->forgetDrivers();
+ }
+
+ $this->app->forgetInstance(BroadcasterContract::class);
+ }
}
diff --git a/src/bus/composer.json b/src/bus/composer.json
index 3216d16ac..b2185bb42 100644
--- a/src/bus/composer.json
+++ b/src/bus/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/bus",
- "description": "The bus package for Hypervel.",
+ "description": "The Hypervel Bus package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/bus/src/BusServiceProvider.php b/src/bus/src/BusServiceProvider.php
index c1b2199e7..7d539fd60 100644
--- a/src/bus/src/BusServiceProvider.php
+++ b/src/bus/src/BusServiceProvider.php
@@ -7,10 +7,11 @@
use Hypervel\Container\Container;
use Hypervel\Contracts\Bus\Dispatcher as DispatcherContract;
use Hypervel\Contracts\Bus\QueueingDispatcher as QueueingDispatcherContract;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Queue\Factory as QueueFactoryContract;
use Hypervel\Support\ServiceProvider;
-class BusServiceProvider extends ServiceProvider
+class BusServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -36,6 +37,18 @@ public function register(): void
);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use replaces shared batch repositories while
+ * concurrent coroutines may still hold the previous instances.
+ */
+ public function reloadConfiguration(): void
+ {
+ $this->app->forgetInstance(BatchRepository::class);
+ $this->app->forgetInstance(DatabaseBatchRepository::class);
+ }
+
/**
* Register the batch handling services.
*/
diff --git a/src/cache/composer.json b/src/cache/composer.json
index 02e0a9fcb..e43fb0965 100644
--- a/src/cache/composer.json
+++ b/src/cache/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/cache",
"type": "library",
- "description": "The cache package for Hypervel.",
+ "description": "The Hypervel Cache package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/cache/src/CacheManager.php b/src/cache/src/CacheManager.php
index f9635e456..1f72f4d89 100644
--- a/src/cache/src/CacheManager.php
+++ b/src/cache/src/CacheManager.php
@@ -471,6 +471,19 @@ public function forgetDriver(array|UnitEnum|string|null $name = null): static
return $this;
}
+ /**
+ * Forget all resolved cache drivers.
+ *
+ * Boot or tests only. Mutates the singleton's store cache; concurrent
+ * coroutines may already hold stores that next resolution will replace.
+ */
+ public function forgetDrivers(): static
+ {
+ $this->stores = [];
+
+ return $this;
+ }
+
/**
* Disconnect the given driver and remove from local cache.
*
diff --git a/src/cache/src/CacheServiceProvider.php b/src/cache/src/CacheServiceProvider.php
index 640578842..45019598d 100644
--- a/src/cache/src/CacheServiceProvider.php
+++ b/src/cache/src/CacheServiceProvider.php
@@ -13,11 +13,12 @@
use Hypervel\Cache\Listeners\RegisterSwooleMaintenanceTimers;
use Hypervel\Cache\Redis\Console\BenchmarkCommand;
use Hypervel\Cache\Redis\Console\DoctorCommand;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Core\Events\AfterWorkerStart;
use Hypervel\Core\Events\BeforeServerStart;
use Hypervel\Support\ServiceProvider;
-class CacheServiceProvider extends ServiceProvider
+class CacheServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -39,6 +40,21 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared cache stores while concurrent
+ * coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('cache')) {
+ $this->app->make('cache')->forgetDrivers();
+ }
+
+ $this->app->forgetInstance('cache.store');
+ }
+
/**
* Bootstrap the service provider.
*/
diff --git a/src/cache/src/Console/ClearCommand.php b/src/cache/src/Console/ClearCommand.php
index c36033305..19948814e 100644
--- a/src/cache/src/Console/ClearCommand.php
+++ b/src/cache/src/Console/ClearCommand.php
@@ -45,7 +45,9 @@ public function handle(): int
return $this->clearLocks();
}
- $this->hypervel['events']->dispatch(
+ $events = $this->hypervel->make('events');
+
+ $events->dispatch(
'cache:clearing',
[$this->argument('store'), $this->tags()]
);
@@ -61,7 +63,7 @@ public function handle(): int
return self::FAILURE;
}
- $this->hypervel['events']->dispatch(
+ $events->dispatch(
'cache:cleared',
[$this->argument('store'), $this->tags()]
);
diff --git a/src/cache/src/Listeners/CreateSwooleTable.php b/src/cache/src/Listeners/CreateSwooleTable.php
index f271f5f71..407225f01 100644
--- a/src/cache/src/Listeners/CreateSwooleTable.php
+++ b/src/cache/src/Listeners/CreateSwooleTable.php
@@ -14,8 +14,12 @@ class CreateSwooleTable extends BaseListener
*/
public function handle(BeforeServerStart $event): void
{
- $this->swooleStores()->each(function (array $config) {
- $this->container->make(SwooleTableManager::class)->get($config['table']);
+ $tables = $this->container->make(SwooleTableManager::class);
+
+ $this->swooleStores()->each(function (array $config) use ($tables): void {
+ $tables->get($config['table']);
});
+
+ $tables->seal();
}
}
diff --git a/src/cache/src/SwooleTableManager.php b/src/cache/src/SwooleTableManager.php
index 662ea1f2b..20be58791 100644
--- a/src/cache/src/SwooleTableManager.php
+++ b/src/cache/src/SwooleTableManager.php
@@ -6,6 +6,7 @@
use Hypervel\Contracts\Container\Container;
use InvalidArgumentException;
+use LogicException;
use Swoole\Table;
class SwooleTableManager
@@ -17,6 +18,8 @@ class SwooleTableManager
*/
protected array $states = [];
+ protected bool $sealed = false;
+
public function __construct(
protected Container $app
) {
@@ -55,7 +58,28 @@ public function createTable(int $rows, int $bytes, float $conflictProportion): S
*/
public function get(string $name): SwooleTableState
{
- return $this->states[$name] ??= $this->resolve($name);
+ if (isset($this->states[$name])) {
+ return $this->states[$name];
+ }
+
+ if ($this->sealed) {
+ throw new LogicException(
+ "Swoole cache table [{$name}] was not initialized before the server fork."
+ );
+ }
+
+ return $this->states[$name] = $this->resolve($name);
+ }
+
+ /**
+ * 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.
+ */
+ public function seal(): void
+ {
+ $this->sealed = true;
}
/**
diff --git a/src/collections/composer.json b/src/collections/composer.json
index 4f61b0bc6..eb45297c9 100644
--- a/src/collections/composer.json
+++ b/src/collections/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/collections",
"type": "library",
- "description": "The collections package for Hypervel.",
+ "description": "The Hypervel Collections package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/concurrency/composer.json b/src/concurrency/composer.json
index 6002558d5..c092be10c 100644
--- a/src/concurrency/composer.json
+++ b/src/concurrency/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/concurrency",
"type": "library",
- "description": "The concurrency package for Hypervel.",
+ "description": "The Hypervel Concurrency package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/concurrency/src/ConcurrencyManager.php b/src/concurrency/src/ConcurrencyManager.php
index 69c74a798..73048752b 100644
--- a/src/concurrency/src/ConcurrencyManager.php
+++ b/src/concurrency/src/ConcurrencyManager.php
@@ -60,9 +60,7 @@ public function createSyncDriver(): SyncDriver
*/
public function getDefaultInstance(): string
{
- return $this->app['config']['concurrency.default']
- ?? $this->app['config']['concurrency.driver']
- ?? 'coroutine';
+ return $this->config->string('concurrency.default');
}
/**
@@ -72,8 +70,7 @@ public function getDefaultInstance(): string
*/
public function setDefaultInstance(string $name): void
{
- $this->app['config']['concurrency.default'] = $name;
- $this->app['config']['concurrency.driver'] = $name;
+ $this->config->set('concurrency.default', $name);
}
/**
@@ -81,7 +78,7 @@ public function setDefaultInstance(string $name): void
*/
public function getInstanceConfig(string $name): array
{
- return $this->app['config']->get(
+ return $this->config->array(
'concurrency.driver.' . $name,
['driver' => $name],
);
diff --git a/src/concurrency/src/ConcurrencyServiceProvider.php b/src/concurrency/src/ConcurrencyServiceProvider.php
index 488858008..13eb490e1 100644
--- a/src/concurrency/src/ConcurrencyServiceProvider.php
+++ b/src/concurrency/src/ConcurrencyServiceProvider.php
@@ -4,9 +4,10 @@
namespace Hypervel\Concurrency;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Support\ServiceProvider;
-class ConcurrencyServiceProvider extends ServiceProvider
+class ConcurrencyServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -15,4 +16,17 @@ public function register(): void
{
$this->app->singleton(ConcurrencyManager::class, fn ($app) => new ConcurrencyManager($app));
}
+
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared concurrency drivers while
+ * concurrent coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(ConcurrencyManager::class)) {
+ $this->app->make(ConcurrencyManager::class)->forgetInstances();
+ }
+ }
}
diff --git a/src/config/composer.json b/src/config/composer.json
index 41ff94ace..b6627f75f 100644
--- a/src/config/composer.json
+++ b/src/config/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/config",
"type": "library",
- "description": "The config package for Hypervel.",
+ "description": "The Hypervel Config package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/console/composer.json b/src/console/composer.json
index df0a2fbaa..14071ed84 100644
--- a/src/console/composer.json
+++ b/src/console/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/console",
"type": "library",
- "description": "The console package for Hypervel.",
+ "description": "The Hypervel Console package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/console/src/Concerns/ConfiguresPrompts.php b/src/console/src/Concerns/ConfiguresPrompts.php
index 1784935b1..5d96d669a 100644
--- a/src/console/src/Concerns/ConfiguresPrompts.php
+++ b/src/console/src/Concerns/ConfiguresPrompts.php
@@ -219,7 +219,7 @@ protected function validatePrompt($value, $rules)
*/
protected function getPromptValidatorInstance($field, $value, $rules, array $messages = [], array $attributes = [])
{
- return $this->hypervel['validator']->make(
+ return $this->hypervel->make('validator')->make(
[$field => $value],
[$field => $rules],
empty($messages) ? $this->validationMessages() : $messages,
diff --git a/src/console/src/Concerns/CreatesMatchingTest.php b/src/console/src/Concerns/CreatesMatchingTest.php
index 87daf6171..523bbafe5 100644
--- a/src/console/src/Concerns/CreatesMatchingTest.php
+++ b/src/console/src/Concerns/CreatesMatchingTest.php
@@ -34,7 +34,7 @@ protected function handleTestCreation(string $path): bool
}
return $this->call('make:test', [
- 'name' => (new Stringable($path))->after($this->hypervel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/')->value(),
+ 'name' => (new Stringable($path))->after($this->hypervel->make('path'))->beforeLast('.php')->append('Test')->replace('\\', '/')->value(),
'--pest' => $this->option('pest'),
'--phpunit' => $this->option('phpunit'),
'--force' => $this->hasOption('force') && $this->option('force'),
diff --git a/src/container/README.md b/src/container/README.md
index 387836c11..0e7a341c0 100644
--- a/src/container/README.md
+++ b/src/container/README.md
@@ -1,4 +1,12 @@
Container for Hypervel
===
-[](https://deepwiki.com/hypervel/container)
\ No newline at end of file
+[](https://deepwiki.com/hypervel/container)
+
+Documentation: https://hypervel.org/docs/container
+
+## Differences From Laravel
+
+Hypervel supports Laravel's named container APIs, but not container ArrayAccess or dynamic service properties. Use `make()` / `get()`, `bound()` / `has()`, `bind()`, and `instance()`. For temporary instance overrides, use `forgetInstance()` to restore the original binding. Hypervel does not expose arbitrary binding removal because registrations are worker-wide boot-time state.
+
+Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Container
diff --git a/src/container/composer.json b/src/container/composer.json
index 10e0fc7a4..55f65552d 100644
--- a/src/container/composer.json
+++ b/src/container/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/container",
- "description": "The Container package for Hypervel.",
+ "description": "The Hypervel Container package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/container/src/Container.php b/src/container/src/Container.php
index c48bac3b3..20a164633 100755
--- a/src/container/src/Container.php
+++ b/src/container/src/Container.php
@@ -4,7 +4,6 @@
namespace Hypervel\Container;
-use ArrayAccess;
use Closure;
use Exception;
use Hypervel\Container\Attributes\Bind;
@@ -30,7 +29,7 @@
use Throwable;
use TypeError;
-class Container implements ArrayAccess, ContainerContract
+class Container implements ContainerContract
{
use ReflectsClosures;
@@ -711,9 +710,9 @@ public function extend(string $abstract, Closure $closure): void
/**
* Register an existing instance as shared in the container.
*
- * Tests only. Replaces a worker-lifetime shared instance; runtime use races
- * across coroutines and changes the object returned to every subsequent
- * resolver.
+ * Boot or tests only. Replaces a worker-lifetime shared instance; runtime
+ * use races across coroutines and changes the object returned to every
+ * subsequent resolver.
*
* @template TInstance of mixed
*
@@ -2268,7 +2267,7 @@ protected function forgetCachedInstances(string $abstract): void
*/
public function forgetInstance(string $abstract): void
{
- $this->forgetCachedInstances($abstract);
+ $this->forgetCachedInstances($this->getAlias($abstract));
}
/**
@@ -2379,62 +2378,6 @@ public static function flushState(): void
static::$buildRecipes = [];
}
- /**
- * Determine if a given offset exists.
- *
- * @param string $key
- */
- public function offsetExists($key): bool
- {
- return $this->bound($key);
- }
-
- /**
- * Get the value at a given offset.
- *
- * @param string $key
- */
- public function offsetGet($key): mixed
- {
- return $this->make($key);
- }
-
- /**
- * Set the value at a given offset.
- *
- * @param string $key
- * @param mixed $value
- */
- public function offsetSet($key, $value): void
- {
- $this->bind($key, $value instanceof Closure ? $value : fn () => $value);
- }
-
- /**
- * Unset the value at a given offset.
- *
- * @param string $key
- */
- public function offsetUnset($key): void
- {
- unset($this->bindings[$key], $this->resolved[$key], $this->scopedInstances[$key]);
-
- $this->dropStaleInstances($key);
- }
-
- /**
- * Dynamically access container services.
- */
- public function __get(string $key): mixed
- {
- return $this[$key];
- }
-
- /**
- * Dynamically set container services.
- */
- public function __set(string $key, mixed $value): void
- {
- $this[$key] = $value;
- }
+ // Hypervel intentionally omits container array and dynamic property access.
+ // Use named methods so resolution and binding lifecycles remain explicit.
}
diff --git a/src/contracts/composer.json b/src/contracts/composer.json
index d227909a8..bcefff1b9 100644
--- a/src/contracts/composer.json
+++ b/src/contracts/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/contracts",
"type": "library",
- "description": "The contracts package for Hypervel.",
+ "description": "The Hypervel Contracts package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/contracts/src/Container/Container.php b/src/contracts/src/Container/Container.php
index 7a88157a5..07c88666f 100644
--- a/src/contracts/src/Container/Container.php
+++ b/src/contracts/src/Container/Container.php
@@ -4,13 +4,12 @@
namespace Hypervel\Contracts\Container;
-use ArrayAccess;
use Closure;
use InvalidArgumentException;
use LogicException;
use Psr\Container\ContainerInterface;
-interface Container extends ArrayAccess, ContainerInterface
+interface Container extends ContainerInterface
{
/**
* @template TClass of object
diff --git a/src/contracts/src/Foundation/ReloadsConfiguration.php b/src/contracts/src/Foundation/ReloadsConfiguration.php
new file mode 100644
index 000000000..ea957aa62
--- /dev/null
+++ b/src/contracts/src/Foundation/ReloadsConfiguration.php
@@ -0,0 +1,16 @@
+app->resolved('cookie')) {
+ return;
+ }
+
+ $config = $this->app->make('config')->array('session');
+
+ /** @var CookieJar $cookie */
+ $cookie = $this->app->make('cookie');
+ $cookie->setDefaultPathAndDomain(
+ $config['path'],
+ $config['domain'],
+ $config['secure'],
+ $config['same_site'] ?? null,
+ );
+ }
}
diff --git a/src/coordinator/composer.json b/src/coordinator/composer.json
index 5402099b4..c835ff58b 100644
--- a/src/coordinator/composer.json
+++ b/src/coordinator/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/coordinator",
"type": "library",
- "description": "The Hypervel Coordinator package for coroutine coordination.",
+ "description": "Coroutine lifecycle coordination for Hypervel.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/core/src/Bootstrap/WorkerStartCallback.php b/src/core/src/Bootstrap/WorkerStartCallback.php
index ee99e53c9..2c0e39fe9 100644
--- a/src/core/src/Bootstrap/WorkerStartCallback.php
+++ b/src/core/src/Bootstrap/WorkerStartCallback.php
@@ -12,7 +12,6 @@
use Hypervel\Core\Events\BeforeWorkerStart;
use Hypervel\Core\Events\MainWorkerStart;
use Hypervel\Core\Events\OtherWorkerStart;
-use Hypervel\Core\Logger\StdoutLogger;
use Swoole\Server as SwooleServer;
class WorkerStartCallback
@@ -28,10 +27,6 @@ public function onWorkerStart(SwooleServer $server, int $workerId): void
{
$this->dispatcher->dispatch(new BeforeWorkerStart($server, $workerId));
- if ($this->logger instanceof StdoutLogger) {
- $this->logger->reloadConfiguration();
- }
-
if ($workerId === 0) {
$this->dispatcher->dispatch(new MainWorkerStart($server, $workerId));
} else {
diff --git a/src/coroutine/composer.json b/src/coroutine/composer.json
index 49f1ad238..ea3b37e02 100644
--- a/src/coroutine/composer.json
+++ b/src/coroutine/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/coroutine",
"type": "library",
- "description": "The coroutine package for Hypervel.",
+ "description": "Coroutine primitives for Hypervel applications.",
"license": "MIT",
"keywords": [
"php",
@@ -46,4 +46,4 @@
"dev-main": "0.4-dev"
}
}
-}
\ No newline at end of file
+}
diff --git a/src/database/composer.json b/src/database/composer.json
index 99ec2c213..da9826503 100644
--- a/src/database/composer.json
+++ b/src/database/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/database",
"type": "library",
- "description": "The database package for Hypervel.",
+ "description": "The Hypervel Database package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/database/src/Capsule/Manager.php b/src/database/src/Capsule/Manager.php
index 916a42bbf..6b9eab9b0 100644
--- a/src/database/src/Capsule/Manager.php
+++ b/src/database/src/Capsule/Manager.php
@@ -59,7 +59,9 @@ public function __construct(?ContainerContract $container = null)
*/
protected function setupDefaultConfiguration(): void
{
- $this->container['config']['database.default'] = 'default';
+ $configuration = $this->container->make('config');
+
+ $configuration['database.default'] = 'default';
}
/**
@@ -119,11 +121,12 @@ public function getConnection(?string $name = null): ConnectionInterface
*/
public function addConnection(array $config, string $name = 'default'): void
{
- $connections = $this->container['config']['database.connections'];
+ $configuration = $this->container->make('config');
+ $connections = $configuration['database.connections'] ?? [];
$connections[$name] = $config;
- $this->container['config']['database.connections'] = $connections;
+ $configuration['database.connections'] = $connections;
}
/**
@@ -158,7 +161,7 @@ public function getDatabaseManager(): DatabaseManager
public function getEventDispatcher(): ?Dispatcher
{
if ($this->container->bound('events')) {
- return $this->container['events'];
+ return $this->container->make('events');
}
return null;
diff --git a/src/database/src/Console/Migrations/MigrateCommand.php b/src/database/src/Console/Migrations/MigrateCommand.php
index bc416473d..6d7476987 100644
--- a/src/database/src/Console/Migrations/MigrateCommand.php
+++ b/src/database/src/Console/Migrations/MigrateCommand.php
@@ -222,7 +222,7 @@ protected function createMissingSqliteDatabase(string $path): bool
protected function createMissingMySqlOrPgsqlDatabase(Connection $connection): bool
{
$adminConfig = (new ConfigurationUrlParser)->parseConfiguration(
- $this->hypervel['config']->get("database.connections.{$connection->getName()}")
+ $this->hypervel->make('config')->get("database.connections.{$connection->getName()}")
);
if (($adminConfig['database'] ?? null) !== $connection->getDatabaseName()) {
diff --git a/src/database/src/Console/Migrations/RefreshCommand.php b/src/database/src/Console/Migrations/RefreshCommand.php
index 7d5501923..3ef45a9b2 100644
--- a/src/database/src/Console/Migrations/RefreshCommand.php
+++ b/src/database/src/Console/Migrations/RefreshCommand.php
@@ -67,7 +67,7 @@ public function handle(): int
]));
if ($this->hypervel->bound(Dispatcher::class)) {
- $this->hypervel[Dispatcher::class]->dispatch(
+ $this->hypervel->make(Dispatcher::class)->dispatch(
new DatabaseRefreshed($database, $this->needsSeeding())
);
}
diff --git a/src/database/src/Console/WipeCommand.php b/src/database/src/Console/WipeCommand.php
index 61fb55923..cb3b9bf61 100644
--- a/src/database/src/Console/WipeCommand.php
+++ b/src/database/src/Console/WipeCommand.php
@@ -67,7 +67,7 @@ public function handle(): int
*/
protected function dropAllTables(?string $database): void
{
- $this->hypervel['db']->connection($database)
+ $this->hypervel->make('db')->connection($database)
->getSchemaBuilder()
->dropAllTables();
}
@@ -77,7 +77,7 @@ protected function dropAllTables(?string $database): void
*/
protected function dropAllViews(?string $database): void
{
- $this->hypervel['db']->connection($database)
+ $this->hypervel->make('db')->connection($database)
->getSchemaBuilder()
->dropAllViews();
}
@@ -87,7 +87,7 @@ protected function dropAllViews(?string $database): void
*/
protected function dropAllTypes(?string $database): void
{
- $this->hypervel['db']->connection($database)
+ $this->hypervel->make('db')->connection($database)
->getSchemaBuilder()
->dropAllTypes();
}
@@ -103,7 +103,7 @@ protected function dropAllTypes(?string $database): void
*/
protected function flushDatabaseConnection(?string $database): void
{
- $this->hypervel['db']->purge($database);
+ $this->hypervel->make('db')->purge($database);
}
/**
diff --git a/src/database/src/DatabaseServiceProvider.php b/src/database/src/DatabaseServiceProvider.php
index 19bd4ae3e..8d2acd98b 100644
--- a/src/database/src/DatabaseServiceProvider.php
+++ b/src/database/src/DatabaseServiceProvider.php
@@ -7,6 +7,8 @@
use Faker\Factory as FakerFactory;
use Faker\Generator as FakerGenerator;
use Hypervel\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract;
+use Hypervel\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Queue\EntityResolver;
use Hypervel\Core\Events\BeforeServerFork;
use Hypervel\Core\Events\BeforeWorkerStart;
@@ -41,7 +43,7 @@
use Hypervel\Support\ServiceProvider;
use Swoole\Constant;
-class DatabaseServiceProvider extends ServiceProvider
+class DatabaseServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -55,28 +57,28 @@ public function register(): void
$this->app->singleton('db.resolver', fn ($app) => $app->make(ConnectionResolver::class));
$this->app->singleton('migration.repository', function ($app) {
- $migrations = $app['config']['database.migrations'];
+ $migrations = $app->make('config')->get('database.migrations');
$table = is_array($migrations)
? ($migrations['table'] ?? 'migrations')
: $migrations;
return new DatabaseMigrationRepository(
- $app['db'],
+ $app->make('db'),
$table,
);
});
$this->app->singleton('migrator', function ($app) {
return new Migrator(
- $app['migration.repository'],
- $app['db'],
- $app['files'],
+ $app->make('migration.repository'),
+ $app->make('db'),
+ $app->make('files'),
);
});
$this->app->singleton('migration.creator', function ($app) {
- return new MigrationCreator($app['files'], $app->basePath('stubs'));
+ return new MigrationCreator($app->make('files'), $app->basePath('stubs'));
});
$this->commands([
@@ -102,6 +104,18 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears the shared connection resolver while
+ * concurrent coroutines may still be using connections from it.
+ */
+ public function reloadConfiguration(): void
+ {
+ $this->app->forgetInstance('db.resolver');
+ $this->app->forgetInstance(ConnectionResolver::class);
+ }
+
/**
* Register the primary database bindings.
*/
@@ -112,16 +126,21 @@ protected function registerConnectionServices(): void
ConcurrencyErrorDetector::class,
);
+ $this->app->singletonIf(
+ LostConnectionDetectorContract::class,
+ LostConnectionDetector::class,
+ );
+
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
$this->app->singleton('db', function ($app) {
- return new DatabaseManager($app, $app['db.factory']);
+ return new DatabaseManager($app, $app->make('db.factory'));
});
$this->app->bind('db.connection', function ($app) {
- return $app['db']->connection();
+ return $app->make('db')->connection();
});
$this->app->singleton('db.schema', function () {
@@ -155,7 +174,7 @@ protected function registerFakerGenerator(): void
}
$this->app->scoped(FakerGenerator::class, function ($app, $parameters) {
- $locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US');
+ $locale = $parameters['locale'] ?? $app->make('config')->get('app.faker_locale', 'en_US');
return FakerFactory::create($locale);
});
diff --git a/src/database/src/Migrations/Migrator.php b/src/database/src/Migrations/Migrator.php
index 740d4b117..5ad919468 100755
--- a/src/database/src/Migrations/Migrator.php
+++ b/src/database/src/Migrations/Migrator.php
@@ -818,7 +818,7 @@ public function fireMigrationEvent(MigrationEventContract $event): void
$container = Container::getInstance();
if ($container->bound(Dispatcher::class)) {
- $container[Dispatcher::class]->dispatch($event);
+ $container->make(Dispatcher::class)->dispatch($event);
}
}
diff --git a/src/di/composer.json b/src/di/composer.json
index 7b61f2af3..056a7e298 100644
--- a/src/di/composer.json
+++ b/src/di/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/di",
"type": "library",
- "description": "The Hypervel DI package providing AOP (Aspect-Oriented Programming) and class map functionality.",
+ "description": "Dependency injection, aspect-oriented programming, and class-map support for Hypervel.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/docs/README.md b/src/docs/README.md
index da1bd7750..5ffd28bae 100644
--- a/src/docs/README.md
+++ b/src/docs/README.md
@@ -1,7 +1,8 @@
Hypervel Documentation
===
-You can find the online version of the Hypervel documentation at [https://hypervel.org/docs](https://hypervel.org/docs).
+> [!WARNING]
+> The documentation in this branch covers the unreleased Hypervel 0.4 rewrite. The published documentation at [hypervel.org/docs](https://hypervel.org/docs) currently covers Hypervel 0.3. APIs and behavior described in this branch may change before Hypervel 0.4 is released.
## Contribution Guidelines
diff --git a/src/docs/container.md b/src/docs/container.md
index 53dc4765e..ff1aba0b1 100644
--- a/src/docs/container.md
+++ b/src/docs/container.md
@@ -63,7 +63,7 @@ class PodcastController extends Controller
In this example, the `PodcastController` needs to retrieve podcasts from a data source such as Apple Music. So, we will **inject** a service that is able to retrieve podcasts. Since the service is injected, we are able to easily "mock", or create a dummy implementation of the `AppleMusic` service when testing our application.
-Hypervel's container is similar to Laravel's but resolves under a long-running Swoole worker. The bindings, attributes, and resolution helpers all behave like Laravel's, but instance caching is more aggressive and the per-request lifecycle is keyed to a coroutine rather than a fresh PHP process. See [Resolution Lifecycles](#resolution-lifecycles) for the behaviors that differ.
+Hypervel's container follows Laravel's named binding and resolution APIs while running inside a long-lived Swoole worker. Intentional public API differences are covered in the [porting guide](/docs/{{version}}/porting-from-laravel#container-lifecycles). Instance caching is more aggressive, and the per-request lifecycle is keyed to a coroutine rather than a fresh PHP process. See [Resolution Lifecycles](#resolution-lifecycles) for the lifecycle differences.
### Zero Configuration Resolution
diff --git a/src/docs/coroutine-context.md b/src/docs/coroutine-context.md
index 163b768d5..3188ca149 100644
--- a/src/docs/coroutine-context.md
+++ b/src/docs/coroutine-context.md
@@ -24,6 +24,7 @@
- [Request Context](#request-context)
- [Parent Coroutine Context](#parent-coroutine-context)
- [Common Pitfalls](#common-pitfalls)
+- [Credits](#credits)
## Introduction
@@ -466,3 +467,8 @@ Values set outside a coroutine are stored in the shared non-coroutine context. T
Values stored in one coroutine are not visible inside another unless you copy them. Use `Coroutine::fork`, `go(..., copyContext: true)`, `parallel(..., copyContext: true)`, or `CoroutineContext::copyFrom(...)` when a child needs values from its parent.
Objects remain shared when context is copied unless they implement `ReplicableContext`. Avoid copying mutable request-specific objects when shared changes would be unsafe.
+
+
+## Credits
+
+Hypervel Coroutine Context began as a port of [Hyperf Context](https://github.com/hyperf/context) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/coroutines.md b/src/docs/coroutines.md
index bce8f0c93..27e4a68c5 100644
--- a/src/docs/coroutines.md
+++ b/src/docs/coroutines.md
@@ -28,6 +28,7 @@
- [Lockers](#lockers)
- [Advanced Coroutine APIs](#advanced-coroutine-apis)
- [Common Pitfalls](#common-pitfalls)
+- [Credits](#credits)
## Introduction
@@ -824,3 +825,8 @@ Use `Coroutine::defer()` for cleanup that belongs to one coroutine. Use [`Hyperv
Prefer the [Concurrency facade](/docs/{{version}}/concurrency) or the `parallel` helper when the parent coroutine needs results or exceptions from child coroutines. Use `go`, `co`, `Coroutine::create`, or `Concurrent` when a child may run independently and its exceptions may be reported instead of returned to the parent.
Swoole can make most stream-based I/O operations yield to other coroutines while they wait. Some PHP extensions cannot be hooked and will block the entire worker process. For CPU-intensive work or extensions that cannot yield, you should run the work in a separate process.
+
+
+## Credits
+
+Hypervel Coroutine began as a port of [Hyperf Coroutine](https://github.com/hyperf/coroutine) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/deployment.md b/src/docs/deployment.md
index ddab4560a..95d53b3b1 100644
--- a/src/docs/deployment.md
+++ b/src/docs/deployment.md
@@ -213,23 +213,45 @@ This command precompiles all your Blade views so they are not compiled on demand
> [!NOTE]
> When deploying to [SonicStack](https://sonicstack.io), it is not necessary to use the `reload` command, as gracefully reloading all services is handled automatically.
-After deploying a new version of your application, any long-running services such as the Hypervel server (which serves both HTTP and Hypervel Reverb), queue workers, and scheduler should be reloaded / restarted to use the new code. Hypervel provides a single `reload` Artisan command that will signal these services:
+Long-running services do not automatically see changes made by a deployment. Hypervel provides a `reload` Artisan command that reloads the server's event and task workers, signals queue workers to restart, and interrupts the scheduler:
```shell
php artisan reload
```
-The `reload` command gracefully reloads the Hypervel server and signals queue workers and the scheduler to restart. If you are not using [SonicStack](https://sonicstack.io), you should manually configure a process monitor that can detect when your reloadable processes exit and automatically restart them.
+If you are not using [SonicStack](https://sonicstack.io), you should configure a process monitor to restart services such as queue workers when they exit.
-To reload only the Hypervel server, you may use the `server:reload` command. This command reloads the server's event workers and any configured task workers:
+To reload only the Hypervel server, you may use the `server:reload` command:
```shell
php artisan server:reload
```
-The command will fail if the configured PID file cannot be read, does not contain a valid process ID, or the reload signal cannot be delivered.
+This command replaces the server's event workers and any configured task workers. Before a replacement worker begins accepting work, Hypervel reloads the environment and configuration, then refreshes framework services that were created from the previous configuration. This includes resolved cache stores, database connections, queue connections, log channels, filesystem disks, and other services owned by framework and package providers.
-Neither `reload` nor `server:reload` restarts custom server processes. Restart the server when server-process code or configuration changes.
+You may also inject the `ServerReloader` service when a deployment tool or application command needs to reload the server programmatically:
+
+```php
+use Hypervel\Server\ServerReloader;
+
+/**
+ * Execute the console command.
+ */
+public function handle(ServerReloader $serverReloader): void
+{
+ // Perform deployment work...
+
+ $serverReloader->reload();
+}
+```
+
+The command and service will fail if the configured PID file cannot be read, does not contain a valid process ID, or a reload signal cannot be delivered.
+
+Some changes belong to the server's master process and require a full restart. These include listening ports, Swoole settings, worker counts, routes, middleware, event listeners, package enablement, custom server-process definitions, the dump output format, and changed or preloaded PHP code. Cache and Rate Limiter Swoole table definitions also require a restart because shared tables can only be created before workers are forked. If a newly configured table is used after a reload, Hypervel will fail explicitly instead of creating a separate table inside one worker.
+
+The server reload does not restart queue workers, the scheduler, Horizon, or custom server processes. Use the `reload` command for queue workers and the scheduler. Restart Horizon and custom server processes separately.
+
+If refreshed configuration is invalid, replacement workers will fail before they become ready and may continue restarting until the configuration is corrected. Fix the configuration, then run `server:reload` again. Hypervel does not hide the error or continue with partially refreshed worker state.
## Debug Mode
diff --git a/src/docs/http-client.md b/src/docs/http-client.md
index 05fc8b3bb..cb81d8ee5 100644
--- a/src/docs/http-client.md
+++ b/src/docs/http-client.md
@@ -899,6 +899,8 @@ Each pending request constructs one cookie jar and reuses it only across that re
Calling `registerConnection()` or `setConnectionConfig()` again invalidates the connection's cached low-level handler. Subsequent requests lazily create a handler from the new configuration, while in-flight requests safely finish on their existing handler reference.
+Connections registered only in the `boot` method keep their original options when the server is reloaded. If a connection uses configuration that may change, implement `ReloadsConfiguration` on the provider and register the connection again from its `reloadConfiguration` method. Application provider hooks run after framework and package hooks, so your application remains the final authority for a shared connection name when it re-registers that connection. To learn more, see [reloading worker configuration](/docs/{{version}}/providers#reloading-worker-configuration).
+
## Macros
diff --git a/src/docs/introduction.md b/src/docs/introduction.md
index 4018bc85b..286409b35 100644
--- a/src/docs/introduction.md
+++ b/src/docs/introduction.md
@@ -2,150 +2,91 @@
- [What Is Hypervel?](#what-is-hypervel)
- [Why Hypervel?](#why-hypervel)
-- [Built for Async I/O](#built-for-async-io)
-- [Familiar, Productive APIs](#familiar-productive-apis)
-- [Long-Running Workers](#long-running-workers)
-- [Performance Benchmarks](#performance-benchmarks)
- - [Simple API Endpoint](#simple-api-endpoint)
- - [Simulated I/O Wait](#simulated-io-wait)
+ - [Built for Concurrent I/O](#built-for-concurrent-io)
+ - [A Full-Stack Framework](#a-full-stack-framework)
+- [Long-Running Applications](#long-running-applications)
+- [Laravel Compatibility](#laravel-compatibility)
+- [Hypervel's Direction](#hypervels-direction)
- [Next Steps](#next-steps)
+ - [New Applications](#new-applications)
+ - [Existing Laravel Applications](#existing-laravel-applications)
+ - [Package Development](#package-development)
## What Is Hypervel?
-Hypervel is a Laravel-style async PHP framework powered by Swoole coroutines. It provides Laravel's expressive, familiar APIs while running on a high-performance, non-blocking runtime built for HTTP servers, queues, scheduled jobs, WebSockets, and I/O-heavy applications.
+Hypervel is a modern, opinionated PHP framework built for Swoole. Applications run in long-lived workers and use coroutines to handle many requests, jobs, and connections concurrently.
-Hypervel is designed for applications that need the productivity of a modern full-stack PHP framework and the throughput of an async runtime. It is a great fit for traditional web applications, API gateways, microservices, real-time applications, background workers, and services that spend meaningful time waiting on databases, caches, queues, HTTP APIs, or other external systems.
+When one coroutine is waiting on a database query, cache lookup, queue operation, file access, or HTTP request, the worker can continue doing other work instead of remaining idle.
+
+It is a full-stack framework for traditional web applications, APIs, microservices, real-time services, background workers, and other applications that spend meaningful time waiting on external systems.
## Why Hypervel?
-Many modern applications spend much of their time waiting on I/O. A request might query a database, call Redis, talk to another HTTP API, write to storage, dispatch jobs, or broadcast WebSocket messages before it can return a response.
-
-In a traditional blocking runtime, the worker handling that request waits while each I/O operation completes. Running more processes can help, but concurrency is still bounded by worker count and server resources.
-
-Hypervel is built around Swoole coroutines. When one coroutine is waiting on I/O, the worker can continue running other coroutines instead of sitting idle. This lets Hypervel handle high-concurrency workloads efficiently while keeping application code expressive and familiar.
-
-
-## Built for Async I/O
-
-Consider an AI-powered chat application where each upstream model request takes three to five seconds to respond. In a blocking runtime, every worker handling one of those requests remains occupied until the upstream service responds.
-
-In Hypervel, those waiting periods do not have to block the whole worker. The runtime can continue serving other requests, processing jobs, or handling WebSocket traffic while coroutines wait for I/O to complete.
-
-This is the core advantage of Hypervel's runtime model: the framework is built for applications where network and storage latency dominate the total request time.
+Many applications spend a significant portion of each request waiting on input and output. A request may query a database, call Redis, communicate with another HTTP service, write to storage, dispatch a job, or broadcast a WebSocket message before it can return a response.
-
-## Familiar, Productive APIs
+Hypervel is designed to use that waiting time efficiently. Its coroutine runtime allows a worker to run other coroutines while supported I/O operations are in progress, without requiring you to organize ordinary application code around callbacks or manually manage an event loop.
-Hypervel provides Laravel's expressive APIs for routing, middleware, controllers, service providers, configuration, queues, events, notifications, validation, Eloquent, Blade, Inertia, testing, and more.
+
+### Built for Concurrent I/O
-That means you can build Hypervel applications using familiar framework patterns while targeting a coroutine-first runtime. Hypervel's internals are refactored for long-running workers and coroutine safety, while the application-facing APIs remain productive and easy to read.
+In a traditional blocking runtime, a worker remains occupied while an I/O operation completes. Additional worker processes can increase concurrency, but each process has its own memory and can only handle one blocking operation at a time.
-
-## Long-Running Workers
+Consider an endpoint that waits one second for an upstream service. In a blocking runtime with eight workers that each handle one request at a time, only about eight of these requests can complete each second before additional requests begin waiting for a worker. In Hypervel, Swoole coroutines allow those waits to overlap within each worker, so the number of worker processes does not impose the same limit on concurrent I/O. When a coroutine reaches an operation that can yield, Swoole may pause that coroutine and resume another one. The original coroutine continues from the same point when its operation is ready.
-Hypervel applications run inside long-lived Swoole workers. This avoids rebuilding the entire framework for every request and allows Hypervel to keep useful framework state in memory between requests.
+Coroutines are especially useful for applications that make frequent database, Redis, HTTP, filesystem, queue, or timer calls. However, coroutines do not make CPU-intensive PHP code run in parallel. This work, along with PHP extensions that Swoole cannot hook, should run in a separate process. To learn more about these considerations, consult the [coroutine documentation](/docs/{{version}}/coroutines#common-pitfalls).
-Because workers are long-lived, application code should avoid storing request-specific state in global variables, static properties, or singletons. Request-specific state should be passed through the current request flow or stored in [context](/docs/{{version}}/context) (or the lower-level [coroutine context](/docs/{{version}}/coroutine-context)).
+
+### A Full-Stack Framework
-
-## Performance Benchmarks
+Hypervel provides the features expected from a modern full-stack framework, including [routing](/docs/{{version}}/routing), [middleware](/docs/{{version}}/middleware), [dependency injection](/docs/{{version}}/container), [Eloquent](/docs/{{version}}/eloquent), [database migrations](/docs/{{version}}/migrations), [sessions](/docs/{{version}}/session), [caching](/docs/{{version}}/cache), [queues](/docs/{{version}}/queues), [scheduling](/docs/{{version}}/scheduling), [broadcasting](/docs/{{version}}/broadcasting), [authentication](/docs/{{version}}/authentication), [validation](/docs/{{version}}/validation), [Blade templates](/docs/{{version}}/blade), and [testing](/docs/{{version}}/testing).
-The benchmarks below compare Hypervel to Laravel Octane for a simple API endpoint and an I/O-bound endpoint that waits for one second before responding. They are intended to show how Hypervel behaves under both raw request handling and coroutine-friendly I/O wait workloads.
+Hypervel also includes [coroutine APIs](/docs/{{version}}/coroutines), the [Concurrency facade](/docs/{{version}}/concurrency), persistent [database](/docs/{{version}}/database#connection-pooling) and [Redis](/docs/{{version}}/redis#connection-pooling) connection pools, [WebSocket servers](/docs/{{version}}/websockets), [gRPC services](/docs/{{version}}/grpc), and [custom server processes](/docs/{{version}}/server-processes).
-The benchmark tests cover two scenarios:
+
+## Long-Running Applications
-
+Hypervel boots the application before the Swoole server begins handling requests. The application, service providers, singletons, facades, configuration repository, and other framework state remain in memory and are reused by future requests handled by the same worker. Service providers are registered and booted during application startup, not once for every request.
-- A simple API endpoint that responds with `hello world`.
-- A simulated I/O wait endpoint that sleeps for one second before responding with `hello world`.
+This lifecycle avoids rebuilding the framework for every request and allows connections and other resources to be reused efficiently. For example, Hypervel's database and Redis integrations reuse established connections from worker-level pools instead of opening a new connection for every operation.
-
+Since multiple requests or jobs may run concurrently inside the same worker, request- or job-specific mutable state must not be stored in global variables, static properties, service providers, or shared singletons. Keep that state in method parameters, the current request when handling HTTP, [context](/docs/{{version}}/context), or the lower-level [coroutine context](/docs/{{version}}/coroutine-context).
-The worker count was configured to match the number of CPU cores by default.
+For a complete overview of application startup and request handling, consult the [request lifecycle documentation](/docs/{{version}}/lifecycle).
-Test environment:
+
+## Laravel Compatibility
-| Resource | Value |
-| --- | --- |
-| Hardware | Apple M1 Pro 2021 |
-| CPU | 8 cores |
-| RAM | 16 GB |
+Hypervel aims for Laravel API compatibility wherever it fits. However, Hypervel is not a Laravel clone or drop-in replacement. Many Hypervel components are ports of Laravel packages, adapted for Hypervel's asynchronous runtime, performance requirements, and coroutine safety, but the framework itself has its own architecture, features, supported integrations, and direction.
-
-### Simple API Endpoint
+Moving an existing Laravel application or package to Hypervel is a deliberate port, not a namespace replacement. Hypervel differs in its runtime, service lifecycles, supported drivers, package structure, and some public APIs. The [porting guide](/docs/{{version}}/porting-from-laravel) explains the differences that commonly affect application and package code.
-| Runtime | Workers | Requests / second | Average latency | Transfer / second |
-| --- | ---: | ---: | ---: | ---: |
-| Laravel Octane | 8 | 8,230.97 | 15.93ms | 1.69MB |
-| Hypervel | 8 | 96,562.80 | 7.66ms | 15.10MB |
+Hypervel actively monitors Laravel changes and ports compatible additions when they make sense for Hypervel's architecture. If your application or package depends on a recent Laravel API that is not available, check the current Hypervel source and raise the concrete use case with the maintainers.
-Laravel Octane:
+
+## Hypervel's Direction
-```text
-Running 10s test @ http://127.0.0.1:8000/api
- 4 threads and 100 connections
- Thread Stats Avg Stdev Max +/- Stdev
- Latency 15.93ms 16.86ms 155.82ms 87.02%
- Req/Sec 2.07k 420.46 3.10k 66.00%
- 82661 requests in 10.04s, 16.95MB read
-Requests/sec: 8230.97
-Transfer/sec: 1.69MB
-```
+When a different design is better suited to long-running workers, coroutines, or Hypervel's performance requirements, Hypervel uses it. The dedicated [rate limiter](/docs/{{version}}/rate-limiting), [dual-mode Redis cache tags](/docs/{{version}}/cache#redis-tag-modes), [layered cache stores](/docs/{{version}}/cache#building-cache-stacks), and [Redis session system with user-session management](/docs/{{version}}/session#managing-user-sessions) are examples of features developed for Hypervel. The framework will continue tracking and porting Laravel features where they fit while also developing its own features and integrations.
-Hypervel:
+First-party ClickHouse support and built-in integration with [SonicStack](https://sonicstack.io) are also planned. SonicStack is Hypervel's fully managed deployment platform, built specifically for running and maintaining Hypervel applications. It is also how we plan to fund ongoing framework development.
-```text
-Running 10s test @ http://127.0.0.1:8000/api
- 4 threads and 100 connections
- Thread Stats Avg Stdev Max +/- Stdev
- Latency 7.66ms 17.85ms 249.92ms 90.25%
- Req/Sec 24.42k 10.47k 54.37k 68.53%
- 971692 requests in 10.06s, 151.98MB read
-Requests/sec: 96562.80
-Transfer/sec: 15.10MB
-```
-
-
-### Simulated I/O Wait
-
-| Runtime | Workers | Requests / second | Average latency | Transfer / second |
-| --- | ---: | ---: | ---: | ---: |
-| Laravel Octane | 8 | 7.92 | 1.03s | 1.66KB |
-| Hypervel | 8 | 10,842.71 | 1.02s | 1.96MB |
+
+## Next Steps
-Laravel Octane:
+Whether you are starting a new application, moving an existing Laravel project, or developing a reusable package, the following guides are a good place to start.
-```text
-Running 10s test @ http://127.0.0.1:8000/api
- 4 threads and 100 connections
- Thread Stats Avg Stdev Max +/- Stdev
- Latency 1.03s 184.92us 1.03s 87.50%
- Req/Sec 1.52 1.29 5.00 54.84%
- 80 requests in 10.10s, 16.80KB read
- Socket errors: connect 0, read 0, write 0, timeout 72
-Requests/sec: 7.92
-Transfer/sec: 1.66KB
-```
+
+### New Applications
-Hypervel:
+If you are creating a new Hypervel application, begin with the [installation guide](/docs/{{version}}/installation). You may then want to explore the [directory structure](/docs/{{version}}/structure), [configuration](/docs/{{version}}/configuration), [request lifecycle](/docs/{{version}}/lifecycle), [coroutines](/docs/{{version}}/coroutines), and [deployment](/docs/{{version}}/deployment) documentation.
-```text
-Running 10s test @ http://10.10.4.12:8000/api
- 16 threads and 15000 connections
- Thread Stats Avg Stdev Max +/- Stdev
- Latency 1.02s 64.72ms 1.87s 93.62%
- Req/Sec 1.16k 1.68k 9.15k 87.59%
- 109401 requests in 10.09s, 19.82MB read
-Requests/sec: 10842.71
-Transfer/sec: 1.96MB
-```
+
+### Existing Laravel Applications
-> [!NOTE]
-> The Hypervel I/O wait benchmark was run with `wrk` on another machine so that `wrk` could use enough resources to keep more connections open during the test.
+If you are moving an existing Laravel application to Hypervel, begin with the [porting guide](/docs/{{version}}/porting-from-laravel). Create a fresh Hypervel application and port the code deliberately instead of treating the migration as a namespace replacement.
-
-## Next Steps
+
+### Package Development
-To start building with Hypervel, read the [installation](/docs/{{version}}/installation), [request lifecycle](/docs/{{version}}/lifecycle), [configuration](/docs/{{version}}/configuration), [coroutines](/docs/{{version}}/coroutines), and [deployment](/docs/{{version}}/deployment) documentation.
+If you are developing a Hypervel package, read the [package development](/docs/{{version}}/packages) and [Testbench](/docs/{{version}}/testbench) documentation. These guides explain package discovery, service providers, configuration, and testing packages inside a Hypervel application. If you are porting an existing Laravel package, read the [porting guide](/docs/{{version}}/porting-from-laravel) first.
diff --git a/src/docs/jwt.md b/src/docs/jwt.md
index cc6e78122..e83b2a14e 100644
--- a/src/docs/jwt.md
+++ b/src/docs/jwt.md
@@ -24,11 +24,12 @@
- [Guard Methods](#guard-methods)
- [Exceptions](#exceptions)
- [Differences From php-open-source-saver/jwt-auth](#differences-from-php-open-source-saver-jwt-auth)
+- [Credits](#credits)
## Introduction
-Hypervel JWT provides stateless bearer token authentication using Hypervel's authentication guard system. It is based on the `php-open-source-saver/jwt-auth` package and adapted for Hypervel's long-lived Swoole workers and coroutine runtime.
+Hypervel JWT provides stateless bearer token authentication using Hypervel's authentication guard system.
JWT authentication is useful when your application needs signed tokens that can be sent with API, mobile, or service-to-service requests. If you need first-party SPA session authentication or database-backed personal access tokens, consider [Sanctum](/docs/{{version}}/sanctum) instead.
@@ -593,7 +594,7 @@ Common exceptions include:
## Differences From php-open-source-saver/jwt-auth
-Hypervel JWT is based on `php-open-source-saver/jwt-auth`, but its internals are adapted for Hypervel:
+Hypervel JWT differs from `php-open-source-saver/jwt-auth` in several ways:
@@ -606,3 +607,8 @@ Hypervel JWT is based on `php-open-source-saver/jwt-auth`, but its internals are
- The `show_black_list_exception` option is not included; JWT exceptions fail normally.
+
+
+## Credits
+
+Hypervel JWT began as a port of [PHP Open Source Saver JWT Auth](https://github.com/PHP-Open-Source-Saver/jwt-auth) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/nested-set.md b/src/docs/nested-set.md
index d91a749ac..e06fe4541 100644
--- a/src/docs/nested-set.md
+++ b/src/docs/nested-set.md
@@ -28,6 +28,7 @@
- [Soft Deleting Nodes](#soft-deleting-nodes)
- [Rendering Trees](#rendering-trees)
- [Performance](#performance)
+- [Credits](#credits)
## Introduction
@@ -36,8 +37,6 @@ Hypervel's nested set package provides tools for storing hierarchical data in a
Nested sets store each node with left and right boundary columns. This makes ancestor and descendant reads efficient, while inserts and moves update the affected boundary ranges.
-The package is based on Aimeos's maintained `laravel-nestedset` package and adapted for Hypervel's Eloquent implementation.
-
## Installation
@@ -896,3 +895,8 @@ If a model observer vetoes a mutation and it returns `false`, throw from the tra
Concurrent writers to the same table and nested set scope must also be serialized by your application. The package does not add an implicit distributed lock or network call.
The schema helpers create separate indexes for right-bound scans, left-bound scans, and parent lookups. Scope columns prefix each index, which keeps each scoped tree's reads isolated and substantially reduces ancestor, descendant, child, and sibling query work. These indexes add a bounded cost to structural writes; this favors the read-heavy workloads nested sets are designed for. Add a depth index only when your application frequently filters large trees by depth.
+
+
+## Credits
+
+Hypervel Nested Set began as a port of [Aimeos Laravel Nested Set](https://github.com/aimeos/laravel-nestedset) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/permission.md b/src/docs/permission.md
index 86bcf08c0..645b784bb 100644
--- a/src/docs/permission.md
+++ b/src/docs/permission.md
@@ -57,13 +57,14 @@
- [Performance](#performance)
- [Exceptions](#exceptions)
- [Differences From Spatie Laravel Permission](#differences-from-spatie-laravel-permission)
+- [Credits](#credits)
## Introduction
Hypervel's permission package provides role-based access control for Eloquent models. A permission represents one ability, such as `edit articles`. A role is a named group of permissions, such as `editor`. You may assign roles and permissions to users or other models, then check access by role, direct permission, or permission inherited through a role.
-The package is based on Spatie's `laravel-permission` package and adapted for Hypervel. It also supports denied permissions, which explicitly reject an ability even when the model receives the same permission directly or through a role.
+The package also supports denied permissions, which explicitly reject an ability even when the model receives the same permission directly or through a role.
## Installation
@@ -1514,3 +1515,8 @@ Partition registration and isolation failures use focused exceptions:
- Hypervel adds opt-in generic row partitioning through `PermissionRegistrar::resolvePartitionUsing(...)`. It scopes model lifecycle operations, every package relation and pivot, queries, commands, cache identities, and invalidation without depending on any partition domain.
- Hypervel's cache config uses `expiration_seconds` and separate named cache keys so role, model-role, model-permission, and assignment-token caches can be invalidated independently.
- Undefined `permission.cache.store` values fail fast through Hypervel's cache manager instead of silently falling back to an array store.
+
+
+## Credits
+
+Hypervel Permission began as a port of [Spatie Laravel Permission](https://github.com/spatie/laravel-permission) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md
index fe7540070..55b50364b 100644
--- a/src/docs/porting-from-laravel.md
+++ b/src/docs/porting-from-laravel.md
@@ -4,9 +4,12 @@
- [Why Laravel Code Needs Porting](#why-laravel-code-needs-porting)
- [Porting Workflow](#porting-workflow)
- [Namespaces and Dependencies](#namespaces-and-dependencies)
+ - [Composer Dependencies](#composer-dependencies)
- [Common Namespace Replacements](#common-namespace-replacements)
- [Contracts](#contracts)
+ - [Missing Equivalents](#missing-equivalents)
- [Type Declarations](#type-declarations)
+ - [Inherited Properties](#inherited-properties)
- [Service Providers](#service-providers)
- [Registering Bindings](#registering-bindings)
- [Bootstrapping Services](#bootstrapping-services)
@@ -16,9 +19,19 @@
- [Request-Specific State](#request-specific-state)
- [Worker-Lifetime State](#worker-lifetime-state)
- [Container Lifecycles](#container-lifecycles)
+ - [Coroutine-Aware Dependencies](#coroutine-aware-dependencies)
- [Configuration](#configuration)
- [Other API Differences](#other-api-differences)
+ - [HTTP Client and Concurrency](#http-client-and-concurrency)
+ - [Rate Limiting](#rate-limiting)
+ - [Pagination](#pagination)
+ - [Dates](#dates)
- [Database, Cache, Sessions, and Queues](#database-cache-sessions-and-queues)
+ - [Database](#database)
+ - [Redis](#redis)
+ - [Cache](#cache)
+ - [Sessions](#sessions)
+ - [Queues](#queues)
- [Testing Ports](#testing-ports)
- [Application Tests](#application-tests)
- [Package Tests](#package-tests)
@@ -30,16 +43,20 @@
## Introduction
-Laravel code is often straightforward to port to Hypervel, but it should not be copied into a Hypervel application or package without review. Hypervel intentionally follows Laravel's public APIs wherever that makes sense, while running on long-lived Swoole workers that may handle many concurrent requests and jobs in the same PHP process.
+Hypervel is an independent, opinionated Swoole framework. It aims for Laravel API compatibility whenever those APIs fit its coroutine-first architecture, but it is not a Laravel port or a drop-in replacement. Hypervel deliberately differs in its runtime, service lifecycles, supported drivers, package structure, and some public APIs.
-This guide explains how to port Laravel application code and Laravel packages to Hypervel. It focuses on the parts that usually matter during a port: namespaces, service providers, configuration, tests, and coroutine safety.
+Laravel code is often straightforward to port, but it should not be copied into a Hypervel application or package without review. This guide explains how to port Laravel application code and Laravel packages to Hypervel. It focuses on the parts that usually matter during a port: dependencies, namespaces, service providers, configuration, tests, and coroutine safety.
+
+Do not use a complete class-by-class diff between Laravel and Hypervel as a migration plan. Begin with the code you are actually porting, identify the framework features and integrations it uses, and verify each of those against the documentation and source for your target Hypervel version.
+
+Hypervel actively monitors upstream Laravel changes and ports compatible additions when they make sense for Hypervel's architecture. If an application or package depends on a recent Laravel API that is not available, verify the current source and raise the concrete use case with the maintainers rather than assuming that every difference is permanent or accidental.
If you are building a new Hypervel package from scratch, you should also read the [package development documentation](/docs/{{version}}/packages). If you are testing a package, read the [Testbench documentation](/docs/{{version}}/testbench).
## Why Laravel Code Needs Porting
-Laravel is stateful PHP framework, where each request starts with a fresh PHP runtime and ends by destroying all in-memory state. Hypervel is a stateful framework that runs inside long-lived Swoole workers. A worker boots the application once, keeps it in memory, and serves many requests and jobs over its lifetime.
+Traditional Laravel applications commonly run under a request-isolated PHP lifecycle, where each request starts with a fresh application runtime and ends by discarding its in-memory state. Hypervel is designed around long-lived Swoole workers. An HTTP or queue worker keeps the application and its shared services in memory while serving many requests or jobs over its lifetime.
This difference means request-specific state must not be stored on shared objects. For example, consider the shape of Laravel's `SessionGuard`: the guard caches the authenticated user on an instance property so repeated `user()` calls during a single request are fast. In a PHP-FPM request lifecycle, that instance disappears at the end of the request. In Hypervel, a singleton guard may live for the worker lifetime, so the cached user must be isolated per coroutine instead of stored directly on the shared object.
@@ -54,16 +71,16 @@ The same issue appears in translators, managers, middleware, repositories, event
When porting Laravel code to Hypervel, work through the code in this order:
-1. Replace `Illuminate` namespaces with the matching `Hypervel` namespaces.
-2. Replace unsupported Laravel features with Hypervel-supported alternatives.
-3. Convert Laravel service providers to Hypervel service providers.
-4. Review singleton, static, and manager state for coroutine safety.
-5. Modernize type declarations where the Hypervel equivalent is stricter than Laravel.
-6. Verify the types against runtime behavior and tests.
-7. Port the relevant Laravel tests and add Hypervel-specific tests for coroutine isolation when needed.
-8. Run the test suite and static analysis for the project or package you are porting.
+1. Choose the Hypervel version you are targeting and use the documentation and source for that version.
+2. Inventory the Laravel code using the [porting checklist](#porting-checklist). Record its framework APIs, Composer dependencies, service providers, drivers, configuration, long-lived state, external I/O, and tests before changing the code.
+3. For an application, create a fresh Hypervel application and move application code into it. For a package, begin by updating its Composer dependencies and package discovery metadata.
+4. Replace `Illuminate` imports with verified `Hypervel` equivalents. Confirm that each replacement exists and provides the behavior the code expects.
+5. Replace unsupported integrations and APIs with documented Hypervel features, then port service providers and configuration.
+6. Review inherited property and method declarations against their Hypervel parents and traits.
+7. Review singleton, static, manager, and pooled-resource usage for coroutine safety.
+8. Port the relevant tests and add coroutine-isolation tests where needed. Confirm that no `Illuminate` imports remain, then run the test suite and static analysis.
-The goal is not to make code look different for its own sake. Keep Laravel behavior and method names where Hypervel supports them. Change the implementation only where Hypervel's runtime, supported drivers, or package structure requires it.
+The goal is not to make code look different for its own sake. Keep Laravel behavior and method names where Hypervel supports them. Change the implementation where Hypervel's runtime, supported drivers, public APIs, or package structure requires it.
## Namespaces and Dependencies
@@ -72,6 +89,15 @@ Most Laravel framework classes map directly from `Illuminate\...` to `Hypervel\.
When porting imports, update the import list first, then read the class again and verify that each replacement exists and has the behavior the code expects. Some Laravel packages depend on optional Laravel-only packages or drivers that Hypervel does not support.
+
+### Composer Dependencies
+
+For applications, use the `composer.json` file from a fresh Hypervel application as your starting point. Do not copy a Laravel application's framework dependencies, Composer scripts, or bootstrap files over the Hypervel skeleton.
+
+For packages, replace `laravel/framework` and individual `illuminate/*` requirements with the Hypervel components the package actually uses. Replace `orchestra/testbench` with `hypervel/testbench` for package tests. If a third-party dependency requires Laravel or Illuminate components, use a Hypervel-compatible version or port that integration; do not retain Illuminate packages merely to fill missing framework classes.
+
+Laravel package discovery metadata under `extra.laravel` does not register providers in Hypervel. Move Hypervel provider discovery to `extra.hypervel.providers` as described in the [package development documentation](/docs/{{version}}/packages#package-discovery).
+
### Common Namespace Replacements
@@ -82,6 +108,8 @@ The following replacements cover the most common Laravel framework dependencies:
| `Illuminate\Auth\...` | `Hypervel\Auth\...` |
| `Illuminate\Broadcasting\...` | `Hypervel\Broadcasting\...` |
| `Illuminate\Bus\...` | `Hypervel\Bus\...` |
+| `Illuminate\Cache\RateLimiter` | `Hypervel\RateLimiter\RateLimiter` |
+| `Illuminate\Cache\RateLimiting\Limit` | `Hypervel\RateLimiter\Limit` |
| `Illuminate\Cache\...` | `Hypervel\Cache\...` |
| `Illuminate\Console\...` | `Hypervel\Console\...` |
| `Illuminate\Container\...` | `Hypervel\Container\...` |
@@ -105,7 +133,7 @@ The following replacements cover the most common Laravel framework dependencies:
| `Illuminate\Validation\...` | `Hypervel\Validation\...` |
| `Illuminate\View\...` | `Hypervel\View\...` |
-Not every class has a one-for-one replacement. If a Laravel class belongs to a package Hypervel does not support, remove that integration or replace it with a Hypervel-supported feature.
+The rate limiter is an important exception to the general cache namespace replacement. Its namespace and API are discussed in the [rate limiting](#rate-limiting) section of this guide.
### Contracts
@@ -121,6 +149,13 @@ use Hypervel\Contracts\Support\Arrayable;
Some packages also define package-local contracts, such as `Hypervel\Permission\Contracts\Role` or `Hypervel\Scout\Contracts\SearchableInterface`. When porting a package, prefer the contract namespace used by the Hypervel package you are integrating with.
+
+### Missing Equivalents
+
+Not every Laravel class has a one-for-one replacement. If a class or method is absent, first check the relevant Hypervel documentation and current source for the supported approach. If there is no equivalent, remove the integration or raise the concrete use case with the maintainers.
+
+Do not recreate missing Laravel framework internals or add local classes under `Hypervel` namespaces merely to make a mechanical namespace replacement pass. An intentional adapter around a public contract may be appropriate for an application-owned or third-party integration, but it should adapt that integration to Hypervel's documented API instead of imitating missing framework internals.
+
## Type Declarations
@@ -130,6 +165,64 @@ For example, a Laravel method may document a parameter as `string` while interna
Declare strict types at the top of each file and use native parameter, property, and return types wherever the type is known. Keep PHPDoc for useful descriptions, generics, complex array shapes, `@throws` annotations, and cases PHP cannot express natively.
+
+### Inherited Properties
+
+Hypervel parent classes and traits use native property types where Laravel may use PHPDoc. PHP requires a child class or composed trait property to be compatible with the inherited declaration, so copying an untyped Laravel property may cause a fatal error before the application boots.
+
+For example, a Laravel model may declare its table and fillable attributes without native types:
+
+```php
+class Post extends Model
+{
+ protected $table = 'posts';
+
+ protected $fillable = ['title', 'body'];
+}
+```
+
+The corresponding Hypervel model properties must retain the native types declared by `Hypervel\Database\Eloquent\Model` and its traits:
+
+```php
+laravel`, `getLaravel()`, and `setLaravel()` members with `$this->hypervel`, `getHypervel()`, and `setHypervel()`.
+
+Models and commands are common examples, but they are not an exhaustive list. Audit properties declared by mailables, form requests, queueable jobs, and any other class that extends a Hypervel class or composes a Hypervel trait. Inspect the current parent class and every composed trait before adding or retaining a property declaration.
+
+Some typed properties have no default value. For example, a Hypervel mailable's `$markdown`, `$view`, and `$textView` properties must not be read directly before they have been initialized. Use `isset()` or `??` when testing an optional value, or assign a valid string before reading it.
+
## Service Providers
@@ -138,6 +231,8 @@ Hypervel service providers use the same public shape as Laravel service provider
```php
### Conditional Providers
@@ -224,7 +319,7 @@ public function isEnabled(): bool
### Deferred Providers
-Laravel's `DeferrableProvider` interface is not useful in Hypervel's long-running worker model. Providers are registered once during worker boot and then stay in memory. When porting a Laravel provider that implements `DeferrableProvider`, remove the interface and the `provides` method.
+Laravel's `DeferrableProvider` interface is not useful in Hypervel's long-running worker model. Providers are registered once during application bootstrap and then remain available to the long-lived runtime. When porting a Laravel provider that implements `DeferrableProvider`, remove the interface and the `provides` method.
## Coroutine Safety
@@ -288,7 +383,19 @@ If a public method mutates static or singleton-held state, make sure that state
### Container Lifecycles
-Hypervel's container has the same public shape as Laravel's container, but its lifecycle is adapted for Swoole:
+Hypervel follows Laravel's named container APIs, but intentionally does not support container ArrayAccess or dynamic service properties. Convert those calls while porting:
+
+| Laravel | Hypervel |
+|---|---|
+| `$app['events']` or `$app->events` | `$app->make('events')` |
+| `isset($app['events'])` | `$app->bound('events')` |
+| `$app['service'] = fn ($app) => ...` or `$app->service = fn ($app) => ...` | `$app->bind('service', fn ($app) => ...)` |
+| `$app['service'] = $service` or `$app->service = $service` | `$app->instance('service', $service)` |
+| Remove a temporary instance override | `$app->forgetInstance('service')` |
+
+Use `get()` and `has()` instead when working through the PSR-11 container interface. Hypervel does not expose arbitrary binding removal; `forgetInstance()` clears a temporary instance so the original binding can resolve again.
+
+Container lifecycles are adapted for Swoole:
| Need | Method |
|---|---|
@@ -298,7 +405,17 @@ Hypervel's container has the same public shape as Laravel's container, but its l
| Fresh instance at the call site | `build()` or `buildWith()` |
| Resolve using bindings and lifecycle rules | `make()` |
-Unbound concrete classes are automatically cached for the worker lifetime after their first resolution. This is a performance optimization for stateless services. If a class captures per-call values in its constructor, do not rely on zero-configuration resolution for that class; register it with `bind()` or resolve it with `build()`.
+> [!WARNING]
+> Unbound concrete classes are automatically cached for the worker lifetime after their first resolution. If an unbound class captures the current user, tenant, request, or other mutable per-request data in its constructor, ordinary tests may pass while concurrent requests receive another request's state. Register the class with `bind()` for a fresh instance, use `scoped()` for one instance per request or job coroutine, or construct a fresh instance with `build()`.
+
+
+### Coroutine-Aware Dependencies
+
+Hypervel's framework clients are designed to cooperate with Swoole coroutines, but a third-party library or PHP extension may perform blocking I/O. Swoole can hook many stream-based operations, while an extension that cannot yield will block the entire worker process until its work finishes.
+
+Before porting an integration that performs network, filesystem, subprocess, or other external I/O, verify that its client is safe to use with Swoole's coroutine hooks. Prefer a coroutine-aware client. For unusual work that requires an unhookable extension or full process isolation, use the `process` concurrency driver described in the [concurrency documentation](/docs/{{version}}/concurrency#choosing-a-driver).
+
+Pooled database and Redis resources belong to the coroutine or callback that borrowed them. Do not retain a checked-out low-level connection on a singleton or static property, and do not use it after its owning callback or coroutine ends. Use the manager or facade for each operation and the documented callback APIs, such as `Redis::withConnection()`, when several operations must share one borrowed connection. See [database connection pooling](/docs/{{version}}/database#connection-pooling) and [holding a pooled Redis connection](/docs/{{version}}/redis#holding-a-pooled-connection).
## Configuration
@@ -339,25 +456,83 @@ Application code should keep request-specific values in the request, session, co
## Other API Differences
-Most Laravel APIs have direct Hypervel equivalents under the `Hypervel` namespace. When an API is absent, it is usually because Hypervel's coroutine runtime provides a simpler primitive.
+Many Laravel APIs have direct Hypervel equivalents under the `Hypervel` namespace. The following differences commonly require more than a namespace replacement.
+
+
+### HTTP Client and Concurrency
For concurrent HTTP requests, replace Laravel's `Http::pool` and `Http::batch` patterns with Hypervel's coroutine helpers, typically `parallel` from `Hypervel\Coroutine`. See the [HTTP client documentation](/docs/{{version}}/http-client#concurrent-requests) for examples.
+Hypervel's `Concurrency` facade provides `coroutine`, `process`, and `sync` drivers. Laravel's `fork` driver is not available because coroutines are Hypervel's native lightweight execution model. Use the default `coroutine` driver for normal concurrent application work and reserve `process` for work that requires operating system process isolation. See the [concurrency documentation](/docs/{{version}}/concurrency#choosing-a-driver).
+
+
+### Rate Limiting
+
+Laravel's `Illuminate\Cache\RateLimiter` maps to `Hypervel\RateLimiter\RateLimiter`, not `Hypervel\Cache\RateLimiter`. Likewise, `Illuminate\Cache\RateLimiting\Limit` becomes `Hypervel\RateLimiter\Limit`. Two-argument calls to `RateLimiter::for($name, $callback)` port unchanged, including named route and queue limiters.
+
+The lower-level API is intentionally different. Hypervel uses admission policies such as `Limit`, `SlidingWindow`, and `LeakyBucket` with operations including `consume`, `inspect`, `attempt`, and `clear`. Laravel's counter methods, including `tooManyAttempts`, `hit`, `remaining`, `availableIn`, `resetAttempts`, and `retriesLeft`, are not available. Although `attempt` and `clear` exist in both frameworks, their signatures and behavior differ; do not port those calls by name alone.
+
+When porting custom throttling code, rebuild it using Hypervel's policy API described in the [rate limiting documentation](/docs/{{version}}/rate-limiting). Replace Laravel's `RateLimitedWithRedis` and `ThrottlesExceptionsWithRedis` queue middleware with `Hypervel\Queue\Middleware\RateLimited` or `ThrottlesExceptions` and select the Redis limiter store using `store('redis')`.
+
+
+### Pagination
+
+Hypervel ships Tailwind pagination views. The `Paginator::useBootstrap()`, `useBootstrapFour()`, and `useBootstrapFive()` methods are not available, so remove those calls from ported service providers. If the application does not use Tailwind, publish or create pagination views and select them using `Paginator::defaultView()` and `defaultSimpleView()`. See the [pagination documentation](/docs/{{version}}/pagination#customizing-the-pagination-view).
+
+
+### Dates
+
+Hypervel's date factory, `now()` and `today()` helpers, ordinary Eloquent date casts, and request date casts return `Hypervel\Support\CarbonImmutable` by default. Assign the result of date modifiers when the changed value must be retained:
+
+```php
+$expiresAt = $expiresAt->addMinutes(5);
+```
+
+Review concrete `Hypervel\Support\Carbon` type declarations that receive factory-created values. Use `Carbon\CarbonInterface` at boundaries that may receive mutable or immutable dates, or `CarbonImmutable` when immutability is required. An application that deliberately requires mutable dates may configure the date factory during application boot. See the [date and time documentation](/docs/{{version}}/helpers#dates).
+
## Database, Cache, Sessions, and Queues
-Hypervel supports a smaller set of drivers than Laravel. When porting code, replace unsupported drivers rather than documenting or configuring them. For the full configuration surface, see the [database](/docs/{{version}}/database), [cache](/docs/{{version}}/cache), [session](/docs/{{version}}/session), and [queue](/docs/{{version}}/queues) documentation.
+Hypervel's drivers are designed around its Swoole runtime and do not mirror every Laravel driver. Begin with the configuration files from a fresh Hypervel application and move the required connection values into them; do not copy Laravel configuration files wholesale.
+
+
+### Database
+
+Hypervel supports MySQL, MariaDB, PostgreSQL, and SQLite database connections. SQL Server, MongoDB, and DynamoDB database integrations are not supported.
+
+Database connections are persistent, pooled worker resources. Define every connection in `config/database.php` before the application boots. Dynamic connection creation through `DB::build()` and `DB::connectUsing()` is not supported. Review pool sizing and any database session state against the [database documentation](/docs/{{version}}/database#connection-pooling).
+
+
+### Redis
+
+Hypervel's Redis integration uses the PhpRedis extension exclusively. Its default `config/database.php` file does not contain a `client` option or `REDIS_CLIENT` environment variable. Remove those Laravel settings when porting configuration. A copied `client` option with any value other than `phpredis` is rejected; Predis is not supported.
+
+Laravel's top-level `database.redis.clusters` configuration is also rejected. Configure Redis Cluster by adding a `cluster` array to a named Redis connection. See the [Redis configuration](/docs/{{version}}/redis#configuration) and [cluster documentation](/docs/{{version}}/redis#clusters).
+
+
+### Cache
-Common differences include:
+Hypervel provides Redis, database, file, filesystem storage, Swoole table, session, stack, failover, array, worker-array, and null cache stores. Memcached, APC / APCu, DynamoDB, and MongoDB cache stores are not supported.
-- Database connections are pooled worker-level resources. Define connections in `config/database.php` before the worker boots. Dynamic connection creation via `DB::build()` and `DB::connectUsing()` is not supported.
-- Hypervel supports MySQL, MariaDB, PostgreSQL, and SQLite database connections. SQL Server, MongoDB, and DynamoDB database integrations are not supported.
-- Cache stores include Redis, relational databases, file storage, Swoole tables, session storage, stack / failover stores, and the `array` and `null` stores. Memcached, DynamoDB, and MongoDB cache stores are not supported.
-- `Cache::memo()` may wrap any cache store with per-coroutine memoization. It is accessed at runtime rather than configured as a separate store.
-- Session storage should use a driver supported by Hypervel. Redis is recommended for maximum performance and scalability when sessions need to be shared across workers or servers.
-- Queue connections include `database`, `redis`, `sqs`, `beanstalkd`, `failover`, `sync`, `background`, `deferred`, and `null`. The `background` and `deferred` drivers run work inside the current worker process and are not durable external queues.
+For local in-memory caching, use the [Swoole table cache](/docs/{{version}}/cache#swoole-table-cache). A Swoole table is shared by the workers on one application node. For applications running across several nodes, the [stack cache](/docs/{{version}}/cache#building-cache-stacks) may combine a short-lived Swoole L1 cache with a shared Redis L2 cache. `Cache::memo()` may also wrap a store with per-coroutine memoization at runtime.
-When a Laravel package offers optional support for unsupported drivers, remove those integrations from the Hypervel port unless the package can safely provide them through a separate optional dependency.
+The Redis cache store supports two tag modes. The default `all` mode follows Laravel's classic tagged-cache behavior. In `any` mode, tags are invalidation indexes: retrieve values by their plain keys, and flushing any one assigned tag removes the value. Review the [Redis tag mode documentation](/docs/{{version}}/cache#redis-tag-modes) before changing `REDIS_CACHE_TAG_MODE`.
+
+
+### Sessions
+
+Hypervel's persistent application session drivers are `file`, `cookie`, `database`, and `redis`. The non-persistent `array` and `null` drivers are available for testing. Redis sessions are stored directly in Redis and may select a named Redis connection using `SESSION_CONNECTION`.
+
+Laravel's Memcached, APC / APCu, DynamoDB, and generic cache-backed session configurations do not port. Hypervel does not provide Laravel's cache session handler or `SESSION_STORE` setting. Select one of Hypervel's session drivers and review its requirements in the [session documentation](/docs/{{version}}/session).
+
+
+### Queues
+
+Queue connections include `database`, `redis`, `sqs`, `beanstalkd`, `failover`, `sync`, `background`, `deferred`, and `null`. The `background` and `deferred` drivers run work inside the current worker process and are not durable external queues.
+
+Hypervel stores job batches in a relational database. Laravel's DynamoDB batch repository and DynamoDB failed-job provider are not available. Supported failed-job drivers are `database`, `database-uuids`, `file`, and `null`. See the [queue documentation](/docs/{{version}}/queues) for connection and worker configuration.
+
+When a Laravel package offers optional support for an unsupported driver, remove that integration from the Hypervel port unless the package can safely provide it through a separate optional dependency.
## Testing Ports
@@ -383,6 +558,8 @@ Package feature tests should use `Hypervel\Testbench\TestCase`. Testbench boots
```php
## Porting Applications
-When porting an application, start from a fresh Hypervel application skeleton and move code over intentionally. Hypervel 0.4 has a familiar application structure, but it is not a drop-in replacement for a Laravel `public/index.php` application.
+When porting an application, start from a fresh Hypervel application skeleton and move code over intentionally. Hypervel has a familiar application structure, but it is not a drop-in replacement for a Laravel `public/index.php` application.
+
+Do not replace the Hypervel skeleton's `composer.json`, `bootstrap/app.php`, `config` directory, or `.env.example` with their Laravel counterparts. Move application providers into `bootstrap/providers.php`, move routes into Hypervel's `routes` files, and configure middleware through the Hypervel `bootstrap/app.php` file. Transfer environment values into the corresponding Hypervel configuration keys instead of copying the Laravel environment file unchanged.
+
+Hypervel runs its Swoole HTTP server using `php artisan serve` and does not use `public/index.php` as its HTTP entry point. Review the [deployment documentation](/docs/{{version}}/deployment) before adapting web server or process-monitor configuration.
-Common application changes include:
+Hypervel uses Vite for frontend assets and does not provide Laravel Mix or the `mix()` helper. Keep or migrate assets to the Vite integration described in the [Vite documentation](/docs/{{version}}/vite).
-- Register application service providers in `bootstrap/providers.php`.
-- Move routing into Hypervel's `routes` files and middleware configuration into `bootstrap/app.php`.
-- Replace `Illuminate` imports with `Hypervel` imports.
-- Replace unsupported database, cache, session, queue, mail, or filesystem drivers.
-- Review all singleton services, static properties, middleware, and manager classes for request-specific state.
-- Use `php artisan serve` to run the Swoole server. Hypervel does not use `public/index.php` as the HTTP entry point.
-- Update tests to use Hypervel's coroutine-aware test case when they touch framework services.
+Hypervel does not include support for Laravel Cloud, which is built for Laravel applications. For a managed platform built for Hypervel applications and their long-running services, use [SonicStack](https://sonicstack.io), the Hypervel team's deployment platform. See [Deploying With SonicStack](/docs/{{version}}/deployment#deploying-with-sonicstack).
Treat configuration as boot-time state. If Laravel code changes config values during a request to model the current tenant, locale, guard, or request, move that state to context or a scoped service.
@@ -476,13 +651,22 @@ If the Laravel package ships tests, port the relevant tests with the package. If
When reviewing a Laravel port, confirm the following:
-- All `Illuminate` imports have been replaced with the correct `Hypervel` imports.
-- Service providers extend `Hypervel\Support\ServiceProvider`.
-- Package providers are registered through `extra.hypervel.providers`; application providers are registered in `bootstrap/providers.php`.
-- Request-specific state is not stored on static properties, singleton services, service providers, or managers.
-- Per-request values use context, coroutine context, scoped bindings, or fresh objects.
+- The target Hypervel version is explicit, and APIs have been checked against that version's documentation and source.
+- Applications begin with a fresh Hypervel skeleton; Laravel bootstrap, configuration, Composer scripts, and environment files have not replaced the Hypervel files.
+- `laravel/framework`, `illuminate/*`, Orchestra Testbench, and Laravel-only third-party dependencies have been removed or replaced with the required Hypervel packages.
+- Package providers use `extra.hypervel.providers`, while application providers are registered in `bootstrap/providers.php`.
+- Every `Illuminate` import has been replaced with an existing `Hypervel` import that provides the expected behavior; missing framework APIs have not been recreated as compatibility shims.
+- Inherited methods and properties match the native declarations on the current Hypervel parent classes and composed traits.
+- Code that receives framework-created dates handles immutable Carbon instances correctly.
+- Service providers extend `Hypervel\Support\ServiceProvider`, keep bindings in `register`, and do not use `DeferrableProvider`.
+- Request-specific state is not stored on static properties, singleton services, service providers, managers, or unbound concrete services.
+- Per-request values use context, coroutine context, scoped bindings, or fresh objects, while static caches contain only worker-safe immutable data.
- Runtime configuration mutation has been removed or replaced with request-scoped state.
-- Unsupported drivers and Laravel-only integrations have been removed or replaced.
-- Tests use the correct Hypervel or Testbench base class.
-- Coroutine isolation is tested for shared services that store per-request state.
-- Static caches only contain worker-safe immutable data and can be reset in tests when needed.
+- Third-party I/O and PHP extensions are coroutine-aware or deliberately isolated in a separate process.
+- Checked-out pooled resources, such as low-level database or Redis connections, do not escape their documented callback or coroutine lifetime.
+- Database, cache, session, queue, mail, and filesystem integrations use drivers supported by Hypervel.
+- Redis configuration uses PhpRedis and named-connection cluster settings instead of Laravel's client selector or top-level clusters array.
+- Custom rate limiting uses Hypervel's policy API; HTTP pools and batches use coroutine concurrency; unsupported pagination selectors have been removed.
+- Application frontend assets use Vite, and deployment configuration targets a Hypervel-compatible server or platform.
+- Tests use the correct Hypervel or Testbench base class and cover the behavior being ported.
+- Shared services that store per-request state have tests proving isolation between concurrent coroutines.
diff --git a/src/docs/providers.md b/src/docs/providers.md
index 7d7b73b46..58a7dc112 100644
--- a/src/docs/providers.md
+++ b/src/docs/providers.md
@@ -6,6 +6,7 @@
- [Merging Configuration](#merging-configuration)
- [The Boot Method](#the-boot-method)
- [Service Providers and Long-Running Workers](#service-providers-and-long-running-workers)
+ - [Reloading Worker Configuration](#reloading-worker-configuration)
- [Conditionally Loading Providers](#conditionally-loading-providers)
- [Advanced Provider APIs](#advanced-provider-apis)
- [Registering Providers](#registering-providers)
@@ -18,7 +19,7 @@ Service providers are the central place of all Hypervel application bootstrappin
But, what do we mean by "bootstrapped"? In general, we mean **registering** things, including registering service container bindings, event listeners, middleware, and even routes. Service providers are the central place to configure your application.
-Hypervel uses dozens of service providers internally to bootstrap its core services, such as the mailer, queue, cache, and others. In an HTTP server context, service providers are registered and booted when the Swoole worker starts. They are not registered and booted again for every request handled by that worker.
+Hypervel uses dozens of service providers internally to bootstrap its core services, such as the mailer, queue, cache, and others. In an HTTP server context, service providers are registered and booted before the Swoole server forks its workers. The workers inherit the registered services, listeners, middleware, routes, and other application state. Providers are not registered or booted again for each worker or request.
In a typical Hypervel application, user-defined service providers are registered in the `bootstrap/providers.php` file. In the following documentation, you will learn how to write your own service providers and register them with your Hypervel application.
@@ -200,10 +201,47 @@ public function boot(ResponseFactory $response): void
### Service Providers and Long-Running Workers
-Hypervel applications run in long-lived Swoole worker processes. This means service providers are registered and booted once when the worker starts, and any provider properties or singleton state may be reused by every request handled by that worker.
+Hypervel applications run in long-lived Swoole worker processes. The server application registers and boots its service providers before these workers are forked, so provider properties and singleton state are inherited and may be reused by every request handled by a worker.
For this reason, you should not store request-specific state, such as the current user, tenant, request, or response, on a service provider property or singleton service. Store request-specific state in the request, [context](/docs/{{version}}/context), or the lower-level [coroutine context](/docs/{{version}}/coroutine-context).
+
+### Reloading Worker Configuration
+
+When the server is reloaded, Hypervel reloads the environment and configuration before each replacement worker begins accepting work. Framework and package providers then refresh the long-lived services they own that were created from the previous configuration.
+
+If your application provider creates a long-lived service from configuration, it may implement the `ReloadsConfiguration` contract. For example, the following provider discards a resolved connection so the next resolution uses the current configuration:
+
+```php
+app->forgetInstance(Connection::class);
+ }
+}
+```
+
+If another shared service keeps a reference to the object, update the existing object instead of forgetting it.
+
+Application provider hooks run after framework and discovered package provider hooks. The `register` and `boot` methods still run only during the server application's initial bootstrap. Worker startup events run after configuration has been refreshed and should be used for work that must happen in every new worker.
+
+For more information about reloading workers and changes that require a full restart, see the [deployment documentation](/docs/{{version}}/deployment#reloading-services).
+
### Conditionally Loading Providers
@@ -306,4 +344,4 @@ class RiakServiceProvider extends ServiceProvider
Provider priority only applies to auto-discovered package providers. Framework providers are always registered before discovered package providers, and application providers are registered after them.
> [!NOTE]
-> Hypervel does not support deferred service providers. Since Hypervel runs in long-lived Swoole workers, provider registration and booting happen once at worker startup instead of once per request.
+> Hypervel does not support deferred service providers. Provider registration and booting happen once before the server forks its workers, so their cost is shared across the lifetime of those workers instead of being paid for every request.
diff --git a/src/docs/queues.md b/src/docs/queues.md
index 348e210f4..bed4a1745 100644
--- a/src/docs/queues.md
+++ b/src/docs/queues.md
@@ -1250,6 +1250,8 @@ RecordDelivery::dispatch($order)->onConnection('background');
The `background` and `deferred` drivers do not persist jobs to an external queue backend. Delayed jobs on these connections are scheduled with an in-memory timer and will be lost if the worker exits before the timer fires. Use a persistent queue connection such as `database`, `redis`, `sqs`, or `beanstalkd` for durable delayed work.
+Uncaught exceptions from jobs run on either connection are reported through your application's exception handler.
+
You may also chain `afterResponse` onto a dispatch to run the job synchronously when the current coroutine ends:
```php
diff --git a/src/docs/redis.md b/src/docs/redis.md
index 61773346c..0aaea26f0 100644
--- a/src/docs/redis.md
+++ b/src/docs/redis.md
@@ -26,6 +26,7 @@
- [Pub / Sub](#pubsub)
- [Wildcard Subscriptions](#wildcard-subscriptions)
- [Using the Subscriber](#using-the-subscriber)
+- [Credits](#credits)
## Introduction
@@ -777,3 +778,8 @@ try {
```
The subscriber supports `subscribe`, `unsubscribe`, `psubscribe`, `punsubscribe`, `ping`, `channel`, and `close` methods. It uses the selected connection's standalone, Sentinel, or Cluster topology and supports TCP, TLS, IPv4, IPv6, and Unix sockets. Message payloads are returned as the exact bytes sent by Redis, including embedded newlines and null bytes. Messages received from pattern subscriptions include the matched pattern on the message's `pattern` property.
+
+
+## Credits
+
+Hypervel Redis began as a port of [Hyperf Redis](https://github.com/hyperf/hyperf/tree/master/src/redis) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/releases.md b/src/docs/releases.md
index e2649a699..041aa214f 100644
--- a/src/docs/releases.md
+++ b/src/docs/releases.md
@@ -65,7 +65,7 @@ Hypervel runs on Swoole using long-lived workers and coroutines. This architectu
Many Hypervel 0.4 packages are fresh ports of Laravel packages. These ports aim to provide an API that is almost identical to Laravel where the feature makes sense for Hypervel.
-Some differences are intentional. Hypervel uses `Hypervel\` namespaces, adapts internals for coroutine safety, adds Swoole-specific performance optimizations, and removes drivers or integrations that Hypervel does not support.
+Some differences are intentional. Hypervel uses `Hypervel\` namespaces, adapts internals for coroutine safety, adds Swoole-specific performance optimizations, may omit Laravel public APIs whose semantics do not suit Hypervel, and removes drivers or integrations that Hypervel does not support.
### Immutable Dates
diff --git a/src/docs/saloon.md b/src/docs/saloon.md
index 069cc99a3..f3c9da748 100644
--- a/src/docs/saloon.md
+++ b/src/docs/saloon.md
@@ -72,6 +72,7 @@
- [Fixtures](#fixtures)
- [Publishing Configuration and Stubs](#publishing-configuration-and-stubs)
- [Differences From Saloon](#differences-from-saloon)
+- [Credits](#credits)
## Introduction
@@ -2026,3 +2027,8 @@ Hypervel Saloon keeps the connector, request, middleware, authentication, respon
- The optional `xmlReader` response extension is not included. Use the built-in `xml` or `dom` methods instead.
These differences remove framework-neutral adapter layers while retaining the public concepts needed to build complete integrations and reusable SDKs for Hypervel.
+
+
+## Credits
+
+Hypervel Saloon began as a port of [Saloon](https://github.com/saloonphp/saloon) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/sentry.md b/src/docs/sentry.md
index 1d1922cdf..7e52aa9e6 100644
--- a/src/docs/sentry.md
+++ b/src/docs/sentry.md
@@ -17,6 +17,7 @@
- [Sensitive Data](#sensitive-data)
- [Spotlight](#spotlight)
- [Delivery and Shutdown](#delivery-and-shutdown)
+- [Credits](#credits)
## Introduction
@@ -327,3 +328,8 @@ use Swoole\Constant;
```
Increasing these values does not change normal request latency. They only bound transport operations and graceful shutdown work.
+
+
+## Credits
+
+Hypervel Sentry began as a port of [Sentry Laravel](https://github.com/getsentry/sentry-laravel) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/server-processes.md b/src/docs/server-processes.md
index 4168a1a09..a896c6bb3 100644
--- a/src/docs/server-processes.md
+++ b/src/docs/server-processes.md
@@ -15,6 +15,7 @@
- [Inter-Process Communication](#inter-process-communication)
- [Sending Messages](#sending-messages)
- [Receiving Messages](#receiving-messages)
+- [Credits](#credits)
## Introduction
@@ -241,3 +242,8 @@ Values such as `false`, `null`, `0`, empty strings, and empty arrays are deliver
> [!WARNING]
> Server-process IPC uses PHP serialization, so you should only send data created by code you trust. Swoole owns the collected process handles and exported sockets, so you should not close them in application code.
+
+
+## Credits
+
+Hypervel Server Process began as a port of [Hyperf Process](https://github.com/hyperf/hyperf/tree/master/src/process) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/testbench.md b/src/docs/testbench.md
index 3fa338ade..a01f3b878 100644
--- a/src/docs/testbench.md
+++ b/src/docs/testbench.md
@@ -36,13 +36,14 @@
- [Purging the Skeleton](#purging-the-skeleton)
- [Testing Published Files](#testing-published-files)
- [Helpers](#helpers)
+- [Credits](#credits)
## Introduction
Testbench provides a convenient way to write feature and integration tests for Hypervel packages. It creates a small Hypervel application around your package so your tests may register service providers, publish files, define routes, run migrations, dispatch jobs, and make HTTP requests as if the package were installed in a real application.
-Hypervel Testbench is a port of [Orchestra Testbench](https://github.com/orchestral/testbench) adapted for Hypervel's Swoole and coroutine runtime. It also includes several package-development helpers that are commonly useful when testing generated files, command-line behavior, and Workbench applications.
+It also includes several package-development helpers that are commonly useful when testing generated files, command-line behavior, and Workbench applications.
If you are building a package for Hypervel applications, you should generally use Testbench for your package's feature tests. For general package development concepts, see the [package development documentation](/docs/{{version}}/packages).
@@ -1163,3 +1164,8 @@ $process->mustRun();
```
The `remote` helper reuses the active Testbench runtime skeleton so subprocesses operate on the same disposable application copy as the parent test process.
+
+
+## Credits
+
+Hypervel Testbench began as a port of [Orchestra Testbench Core](https://github.com/orchestral/testbench-core) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/testing.md b/src/docs/testing.md
index 81c45285a..694e5cf6d 100644
--- a/src/docs/testing.md
+++ b/src/docs/testing.md
@@ -4,6 +4,7 @@
- [Environment](#environment)
- [Creating Tests](#creating-tests)
- [Running Tests in Coroutines](#running-tests-in-coroutines)
+ - [Request Context](#request-context)
- [Owning Asynchronous Test Resources](#owning-asynchronous-test-resources)
- [Test State Cleanup](#test-state-cleanup)
- [Macro State](#macro-state)
@@ -150,7 +151,7 @@ class AuthContextTest extends TestCase
{
protected function setUpInCoroutine(): void
{
- CoroutineContext::set('auth_context.users.foo', 'Taylor');
+ CoroutineContext::set('auth_context.users.foo', 'John');
}
protected function tearDownInCoroutine(): void
@@ -160,7 +161,7 @@ class AuthContextTest extends TestCase
public function test_context_value_is_available(): void
{
- $this->assertSame('Taylor', CoroutineContext::get('auth_context.users.foo'));
+ $this->assertSame('John', CoroutineContext::get('auth_context.users.foo'));
}
}
```
@@ -171,6 +172,24 @@ By default, Hypervel copies coroutine context values prepared outside the test m
protected bool $copyNonCoroutineContext = false;
```
+
+### Request Context
+
+Hypervel's HTTP testing methods automatically populate the request context for you. Tests that call the `request` helper without making an HTTP request are different: no request exists in the current context, so Hypervel builds a fresh fallback request from your application's configured URL every time the helper runs. Any change you make to one of those requests, such as calling `request()->merge(...)`, will not be visible to the next `request()` call.
+
+If a test needs a stable request, create one and store it in the request context:
+
+```php
+use Hypervel\Context\RequestContext;
+use Hypervel\Http\Request;
+
+RequestContext::set(Request::create('/?name=John'));
+
+$this->assertSame('John', request('name'));
+```
+
+The request is stored in the current coroutine's context, so it is discarded when the test finishes and will not leak into other tests. If several tests need the same request, you may set it in the `setUpInCoroutine` method.
+
### Owning Asynchronous Test Resources
diff --git a/src/docs/watcher.md b/src/docs/watcher.md
index 9c97b121c..bed15fdc2 100644
--- a/src/docs/watcher.md
+++ b/src/docs/watcher.md
@@ -12,6 +12,7 @@
- [Watcher Drivers](#watcher-drivers)
- [Custom Drivers](#custom-drivers)
- [Custom Restart Strategies](#custom-restart-strategies)
+- [Credits](#credits)
## Introduction
@@ -199,3 +200,8 @@ interface RestartStrategy
```
The watcher calls `start` before it begins watching, `restart` after files change, and `stop` when the watcher exits. The `stop` method may be called more than once and should safely ignore repeated calls. You may omit the strategy when you only need to detect and report file changes.
+
+
+## Credits
+
+Hypervel Watcher began as a port of [Hyperf Watcher](https://github.com/hyperf/hyperf/tree/master/src/watcher) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/docs/websockets.md b/src/docs/websockets.md
index f342df94a..13f8ca483 100644
--- a/src/docs/websockets.md
+++ b/src/docs/websockets.md
@@ -7,6 +7,7 @@
- [Sending Messages](#sending-messages)
- [Subprotocols](#subprotocols)
- [Events](#events)
+- [Credits](#credits)
## Introduction
@@ -226,3 +227,8 @@ Hypervel dispatches the following events for custom WebSocket connections:
- `Hypervel\WebSocketServer\Events\ConnectionClosed` provides the file descriptor, reactor ID, and server name.
You may listen for these events using Hypervel's normal [event listeners](/docs/{{version}}/events#registering-events-and-listeners).
+
+
+## Credits
+
+Hypervel WebSocket Server began as a port of [Hyperf WebSocket Server](https://github.com/hyperf/hyperf/tree/master/src/websocket-server) and has been adapted for Hypervel's framework architecture and coroutine runtime.
diff --git a/src/encryption/composer.json b/src/encryption/composer.json
index 5725e57f7..b6910bcb5 100644
--- a/src/encryption/composer.json
+++ b/src/encryption/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/encryption",
"type": "library",
- "description": "The encryption package for Hypervel.",
+ "description": "The Hypervel Encryption package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/encryption/src/EncryptionServiceProvider.php b/src/encryption/src/EncryptionServiceProvider.php
index 1e25877c1..4096585b3 100644
--- a/src/encryption/src/EncryptionServiceProvider.php
+++ b/src/encryption/src/EncryptionServiceProvider.php
@@ -4,13 +4,14 @@
namespace Hypervel\Encryption;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Encryption\Commands\KeyGenerateCommand;
use Hypervel\Support\ServiceProvider;
use Hypervel\Support\Str;
use Laravel\SerializableClosure\SerializableClosure;
use SensitiveParameter;
-class EncryptionServiceProvider extends ServiceProvider
+class EncryptionServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -25,6 +26,18 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use replaces shared encryption state while
+ * concurrent coroutines may still be using the previous key.
+ */
+ public function reloadConfiguration(): void
+ {
+ $this->registerSerializableClosureSecurityKey();
+ $this->app->forgetInstance('encrypter');
+ }
+
/**
* Register the encrypter.
*/
diff --git a/src/engine/composer.json b/src/engine/composer.json
index 4407eadaa..73f24afbb 100644
--- a/src/engine/composer.json
+++ b/src/engine/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/engine",
- "description": "Coroutine engine for Hypervel powered by Swoole.",
+ "description": "A Swoole-powered coroutine engine for Hypervel.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/events/composer.json b/src/events/composer.json
index 58fed68a6..813007b70 100644
--- a/src/events/composer.json
+++ b/src/events/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/events",
"type": "library",
- "description": "The events package for Hypervel.",
+ "description": "The Hypervel Events package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/facade-documenter/composer.json b/src/facade-documenter/composer.json
index 72e4652ca..6fb5332d9 100644
--- a/src/facade-documenter/composer.json
+++ b/src/facade-documenter/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/facade-documenter",
"type": "library",
- "description": "Generate @method docblocks for Hypervel facades from their underlying proxies.",
+ "description": "Generates @method PHPDoc annotations for Hypervel facades from their underlying proxies.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/filesystem/composer.json b/src/filesystem/composer.json
index 0ef0e9310..ab56f96fb 100644
--- a/src/filesystem/composer.json
+++ b/src/filesystem/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/filesystem",
"type": "library",
- "description": "The filesystem package for Hypervel.",
+ "description": "The Hypervel Filesystem package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php
index 3c92e0193..17de8a950 100644
--- a/src/filesystem/src/FilesystemManager.php
+++ b/src/filesystem/src/FilesystemManager.php
@@ -332,7 +332,7 @@ public function createLocalDriver(array $config, string $name = 'local'): Filesy
$name
)->shouldServeSignedUrls(
$config['serve'] ?? false,
- fn () => $this->app['url'],
+ fn () => $this->app->make('url'),
);
}
@@ -734,6 +734,21 @@ public function forgetDisk(array|string $disk): static
return $this;
}
+ /**
+ * Forget all resolved disks.
+ *
+ * Boot or tests only. Mutates the singleton's disk cache; concurrent
+ * coroutines may already hold disks that next resolution will replace.
+ * Shared pools remain available until their idle TTL expires or purge()
+ * deliberately invalidates them.
+ */
+ public function forgetDisks(): static
+ {
+ $this->disks = [];
+
+ return $this;
+ }
+
/**
* Disconnect the given disk, remove it from local cache, and close its pool.
*
diff --git a/src/filesystem/src/FilesystemServiceProvider.php b/src/filesystem/src/FilesystemServiceProvider.php
index 433751603..5bac9b028 100644
--- a/src/filesystem/src/FilesystemServiceProvider.php
+++ b/src/filesystem/src/FilesystemServiceProvider.php
@@ -5,12 +5,13 @@
namespace Hypervel\Filesystem;
use Hypervel\Contracts\Foundation\CachesRoutes;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Route;
use Hypervel\Support\ServiceProvider;
use InvalidArgumentException;
-class FilesystemServiceProvider extends ServiceProvider
+class FilesystemServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Bootstrap the filesystem.
@@ -29,6 +30,21 @@ public function register(): void
$this->registerFlysystem();
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared filesystem disks while
+ * concurrent coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('filesystem')) {
+ $this->app->make('filesystem')->forgetDisks();
+ }
+
+ $this->app->forgetInstance('filesystem.disk');
+ }
+
/**
* Register the native filesystem implementation.
*/
diff --git a/src/fortify/composer.json b/src/fortify/composer.json
index 33f031e6c..f8bc03fdf 100644
--- a/src/fortify/composer.json
+++ b/src/fortify/composer.json
@@ -9,6 +9,16 @@
"auth",
"swoole"
],
+ "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"
diff --git a/src/fortify/src/FortifyServiceProvider.php b/src/fortify/src/FortifyServiceProvider.php
index 96932695d..882538ea9 100644
--- a/src/fortify/src/FortifyServiceProvider.php
+++ b/src/fortify/src/FortifyServiceProvider.php
@@ -4,6 +4,7 @@
namespace Hypervel\Fortify;
+use Hypervel\Config\Repository as ConfigRepository;
use Hypervel\Contracts\Cache\Repository;
use Hypervel\Contracts\Config\Repository as Config;
use Hypervel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
@@ -50,6 +51,7 @@
use Hypervel\Fortify\Http\Responses\TwoFactorEnabledResponse;
use Hypervel\Fortify\Http\Responses\TwoFactorLoginResponse;
use Hypervel\Fortify\Http\Responses\VerifyEmailResponse;
+use Hypervel\Foundation\Configuration\ConfigMutationTracker;
use Hypervel\Http\Request;
use Hypervel\Passkeys\Passkeys;
use Hypervel\Support\Facades\Route;
@@ -112,16 +114,22 @@ protected function configurePasskeys(): void
{
Passkeys::ignoreRoutes();
- $config = $this->app->make(Config::class);
-
- $appUrl = $config->string('app.url');
-
- $config->set([
- 'passkeys.relying_party_id' => $config->string('fortify.passkeys.relying_party_id', parse_url($appUrl, PHP_URL_HOST)),
- 'passkeys.allowed_origins' => $config->array('fortify.passkeys.allowed_origins', [$appUrl]),
- 'passkeys.user_handle_secret' => $config->string('fortify.passkeys.user_handle_secret', $config->string('app.key')),
- 'passkeys.timeout' => $config->integer('fortify.passkeys.timeout', 60000),
- ]);
+ $config = $this->app->make(ConfigRepository::class);
+
+ // Derived config can depend on the worker environment, so replay the operation after config reload rather than its master result.
+ $this->app->make(ConfigMutationTracker::class)->applyAndRecord(
+ $config,
+ static function (ConfigRepository $config): void {
+ $appUrl = $config->string('app.url');
+
+ $config->set([
+ 'passkeys.relying_party_id' => $config->string('fortify.passkeys.relying_party_id', parse_url($appUrl, PHP_URL_HOST)),
+ 'passkeys.allowed_origins' => $config->array('fortify.passkeys.allowed_origins', [$appUrl]),
+ 'passkeys.user_handle_secret' => $config->string('fortify.passkeys.user_handle_secret', $config->string('app.key')),
+ 'passkeys.timeout' => $config->integer('fortify.passkeys.timeout', 60000),
+ ]);
+ },
+ );
Passkeys::redirectUsing(
static fn (Request $request): string => Fortify::redirects('login', request: $request),
diff --git a/src/foundation/composer.json b/src/foundation/composer.json
index dcb304954..353da5e78 100644
--- a/src/foundation/composer.json
+++ b/src/foundation/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/foundation",
- "description": "The Foundation package for Hypervel.",
+ "description": "The Hypervel Foundation package.",
"license": "MIT",
"keywords": [
"php",
@@ -25,6 +25,7 @@
"require": {
"php": "^8.4",
"composer-runtime-api": "^2.2",
+ "ext-posix": "*",
"guzzlehttp/guzzle": "^7.15.1",
"laravel/serializable-closure": "^2.0.10",
"league/flysystem": "^3.25.1",
diff --git a/src/foundation/src/Application.php b/src/foundation/src/Application.php
index 922e2be6f..89eff1ec5 100644
--- a/src/foundation/src/Application.php
+++ b/src/foundation/src/Application.php
@@ -308,11 +308,11 @@ public function bootstrapWith(array $bootstrappers): void
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
- $this['events']->dispatch('bootstrapping: ' . $bootstrapper, [$this]);
+ $this->make('events')->dispatch('bootstrapping: ' . $bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
- $this['events']->dispatch('bootstrapped: ' . $bootstrapper, [$this]);
+ $this->make('events')->dispatch('bootstrapped: ' . $bootstrapper, [$this]);
}
}
@@ -321,7 +321,7 @@ public function bootstrapWith(array $bootstrappers): void
*/
public function beforeBootstrapping(string $bootstrapper, Closure $callback): void
{
- $this['events']->listen('bootstrapping: ' . $bootstrapper, $callback);
+ $this->make('events')->listen('bootstrapping: ' . $bootstrapper, $callback);
}
/**
@@ -329,7 +329,7 @@ public function beforeBootstrapping(string $bootstrapper, Closure $callback): vo
*/
public function afterBootstrapping(string $bootstrapper, Closure $callback): void
{
- $this['events']->listen('bootstrapped: ' . $bootstrapper, $callback);
+ $this->make('events')->listen('bootstrapped: ' . $bootstrapper, $callback);
}
/**
@@ -758,10 +758,10 @@ public function environment(array|string ...$environments): bool|string
if (count($environments) > 0) {
$patterns = is_array($environments[0]) ? $environments[0] : $environments;
- return Str::is($patterns, $this['env']);
+ return Str::is($patterns, $this->make('env'));
}
- return $this['env'];
+ return $this->make('env');
}
/**
@@ -769,7 +769,7 @@ public function environment(array|string ...$environments): bool|string
*/
public function isLocal(): bool
{
- return $this['env'] === 'local';
+ return $this->make('env') === 'local';
}
/**
@@ -777,7 +777,7 @@ public function isLocal(): bool
*/
public function isProduction(): bool
{
- return $this['env'] === 'production';
+ return $this->make('env') === 'production';
}
/**
@@ -789,7 +789,7 @@ public function detectEnvironment(Closure $callback): string
? $_SERVER['argv']
: null;
- return $this['env'] = (new EnvironmentDetector)->detect($callback, $args);
+ return $this->instance('env', (new EnvironmentDetector)->detect($callback, $args));
}
/**
@@ -841,7 +841,7 @@ public function setRunningInConsole(bool $runningInConsole): void
*/
public function runningUnitTests(): bool
{
- return $this->bound('env') && $this['env'] === 'testing';
+ return $this->bound('env') && $this->make('env') === 'testing';
}
/**
diff --git a/src/foundation/src/Bootstrap/HandleExceptions.php b/src/foundation/src/Bootstrap/HandleExceptions.php
index 3c14fd27b..d1185dbe1 100644
--- a/src/foundation/src/Bootstrap/HandleExceptions.php
+++ b/src/foundation/src/Bootstrap/HandleExceptions.php
@@ -86,7 +86,7 @@ public function handleDeprecationError(string $message, string $file, int $line,
$this->ensureDeprecationLoggerIsConfigured();
- $options = static::$app['config']->get('logging.deprecations') ?? [];
+ $options = static::$app->make('config')->get('logging.deprecations') ?? [];
with($logger->channel('deprecations'), function ($log) use ($message, $file, $line, $level, $options) {
if ($options['trace'] ?? false) {
@@ -118,7 +118,7 @@ protected function shouldIgnoreDeprecationErrors(): bool
*/
protected function ensureDeprecationLoggerIsConfigured(): void
{
- $config = static::$app['config'];
+ $config = static::$app->make('config');
if ($config->get('logging.channels.deprecations')) {
return;
@@ -140,7 +140,7 @@ protected function ensureDeprecationLoggerIsConfigured(): void
*/
protected function ensureNullLogDriverIsConfigured(): void
{
- $config = static::$app['config'];
+ $config = static::$app->make('config');
if ($config->get('logging.channels.null')) {
return;
diff --git a/src/foundation/src/Bootstrap/RegisterProviders.php b/src/foundation/src/Bootstrap/RegisterProviders.php
index a9bc32d3f..afc7752de 100644
--- a/src/foundation/src/Bootstrap/RegisterProviders.php
+++ b/src/foundation/src/Bootstrap/RegisterProviders.php
@@ -49,6 +49,7 @@ protected function mergeAdditionalProviders(ApplicationContract $app): void
}
}
+ // Providers are installed in the master, so replay its final topology rather than recomputing it in replacement workers.
$app->make('config')->set(
'app.providers',
array_merge(
diff --git a/src/foundation/src/Concerns/ResolvesDumpSource.php b/src/foundation/src/Concerns/ResolvesDumpSource.php
index 78f2010a8..2a5eaf5e7 100644
--- a/src/foundation/src/Concerns/ResolvesDumpSource.php
+++ b/src/foundation/src/Concerns/ResolvesDumpSource.php
@@ -4,41 +4,9 @@
namespace Hypervel\Foundation\Concerns;
-use Hypervel\Support\Str;
-use Throwable;
-
trait ResolvesDumpSource
{
- /**
- * All of the href formats for common editors.
- *
- * @var array
- */
- protected array $editorHrefs = [
- 'antigravity' => 'antigravity://file/{file}:{line}',
- 'atom' => 'atom://core/open/file?filename={file}&line={line}',
- 'cursor' => 'cursor://file/{file}:{line}',
- 'emacs' => 'emacs://open?url=file://{file}&line={line}',
- 'fleet' => 'fleet://open?file={file}&line={line}',
- 'idea' => 'idea://open?file={file}&line={line}',
- 'kiro' => 'kiro://file/{file}:{line}',
- 'macvim' => 'mvim://open/?url=file://{file}&line={line}',
- 'neovim' => 'nvim://open?url=file://{file}&line={line}',
- 'netbeans' => 'netbeans://open/?f={file}:{line}',
- 'nova' => 'nova://core/open/file?filename={file}&line={line}',
- 'phpstorm' => 'phpstorm://open?file={file}&line={line}',
- 'sublime' => 'subl://open?url=file://{file}&line={line}',
- 'textmate' => 'txmt://open?url=file://{file}&line={line}',
- 'trae' => 'trae://file/{file}:{line}',
- 'vscode' => 'vscode://file/{file}:{line}',
- 'vscode-insiders' => 'vscode-insiders://file/{file}:{line}',
- 'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/{file}:{line}',
- 'vscode-remote' => 'vscode://vscode-remote/{file}:{line}',
- 'vscodium' => 'vscodium://file/{file}:{line}',
- 'windsurf' => 'windsurf://file/{file}:{line}',
- 'xdebug' => 'xdebug://{file}@{line}',
- 'zed' => 'zed://file/{file}:{line}',
- ];
+ use ResolvesSourceHref;
/**
* Files that require special trace handling and their levels.
@@ -154,35 +122,14 @@ protected function getOriginalFileForCompiledView(string $file): string
}
/**
- * Resolve the source href, if possible.
+ * Set the compiled view path used for source resolution.
+ *
+ * Boot-only. Request-time use changes shared worker configuration while
+ * concurrent coroutines may still be resolving dump sources.
*/
- protected function resolveSourceHref(string $file, ?int $line): ?string
+ public function setCompiledViewPath(string $compiledViewPath): void
{
- try {
- $editor = config('app.editor');
- } catch (Throwable) {
- // ..
- }
-
- if (! isset($editor)) {
- return null;
- }
-
- $href = is_array($editor) && isset($editor['href'])
- ? $editor['href']
- : ($this->editorHrefs[$editor['name'] ?? $editor] ?? sprintf('%s://open?file={file}&line={line}', $editor['name'] ?? $editor));
-
- $basePath = $editor['base_path'] ?? false;
-
- if ($basePath !== false) {
- $file = Str::replaceStart($this->basePath, $basePath, $file);
- }
-
- return str_replace(
- ['{file}', '{line}'],
- [$file, (string) ($line ?? 1)],
- $href,
- );
+ $this->compiledViewPath = $compiledViewPath;
}
/**
diff --git a/src/foundation/src/Concerns/ResolvesSourceHref.php b/src/foundation/src/Concerns/ResolvesSourceHref.php
new file mode 100644
index 000000000..66843b71d
--- /dev/null
+++ b/src/foundation/src/Concerns/ResolvesSourceHref.php
@@ -0,0 +1,74 @@
+
+ */
+ protected array $editorHrefs = [
+ 'antigravity' => 'antigravity://file/{file}:{line}',
+ 'atom' => 'atom://core/open/file?filename={file}&line={line}',
+ 'cursor' => 'cursor://file/{file}:{line}',
+ 'emacs' => 'emacs://open?url=file://{file}&line={line}',
+ 'fleet' => 'fleet://open?file={file}&line={line}',
+ 'idea' => 'idea://open?file={file}&line={line}',
+ 'kiro' => 'kiro://file/{file}:{line}',
+ 'macvim' => 'mvim://open/?url=file://{file}&line={line}',
+ 'neovim' => 'nvim://open?url=file://{file}&line={line}',
+ 'netbeans' => 'netbeans://open/?f={file}:{line}',
+ 'nova' => 'nova://core/open/file?filename={file}&line={line}',
+ 'phpstorm' => 'phpstorm://open?file={file}&line={line}',
+ 'sublime' => 'subl://open?url=file://{file}&line={line}',
+ 'textmate' => 'txmt://open?url=file://{file}&line={line}',
+ 'trae' => 'trae://file/{file}:{line}',
+ 'vscode' => 'vscode://file/{file}:{line}',
+ 'vscode-insiders' => 'vscode-insiders://file/{file}:{line}',
+ 'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/{file}:{line}',
+ 'vscode-remote' => 'vscode://vscode-remote/{file}:{line}',
+ 'vscodium' => 'vscodium://file/{file}:{line}',
+ 'windsurf' => 'windsurf://file/{file}:{line}',
+ 'xdebug' => 'xdebug://{file}@{line}',
+ 'zed' => 'zed://file/{file}:{line}',
+ ];
+
+ /**
+ * Resolve the source href, if possible.
+ */
+ protected function resolveSourceHref(string $file, ?int $line): ?string
+ {
+ try {
+ $editor = config('app.editor');
+ } catch (Throwable) {
+ // ..
+ }
+
+ if (! isset($editor)) {
+ return null;
+ }
+
+ $href = is_array($editor) && isset($editor['href'])
+ ? $editor['href']
+ : ($this->editorHrefs[$editor['name'] ?? $editor] ?? sprintf('%s://open?file={file}&line={line}', $editor['name'] ?? $editor));
+
+ $basePath = $editor['base_path'] ?? false;
+
+ if ($basePath !== false) {
+ $file = Str::replaceStart($this->basePath, $basePath, $file);
+ }
+
+ return str_replace(
+ ['{file}', '{line}'],
+ [$file, (string) ($line ?? 1)],
+ $href,
+ );
+ }
+}
diff --git a/src/foundation/src/Console/CliDumper.php b/src/foundation/src/Console/CliDumper.php
index fa6af2db1..5a0e6c2eb 100644
--- a/src/foundation/src/Console/CliDumper.php
+++ b/src/foundation/src/Console/CliDumper.php
@@ -21,17 +21,26 @@ class CliDumper extends BaseCliDumper
protected const string DUMPING_CONTEXT_KEY = '__foundation.cli_dumper.dumping';
/**
- * Create a new CLI dumper instance.
+ * The console output instance.
*
- * @param OutputInterface $output
+ * This remains separate from the constructor parameter because Symfony's
+ * inherited `$output` PHPDoc describes a dump destination and PHPStan
+ * applies it to a promoted property with the same name.
+ */
+ protected OutputInterface $output;
+
+ /**
+ * Create a new CLI dumper instance.
*/
public function __construct(
- protected mixed $output,
+ OutputInterface $output,
protected string $basePath,
- protected ?string $compiledViewPath,
+ protected string $compiledViewPath,
) {
parent::__construct();
+ $this->output = $output;
+
$this->setColors($this->supportsColors());
}
@@ -40,17 +49,16 @@ public function __construct(
*
* Boot-only. Registers a process-wide VarDumper handler for the worker
* lifetime.
- *
- * @param string $basePath
- * @param string $compiledViewPath
*/
- public static function register($basePath, $compiledViewPath): void
+ public static function register(string $basePath, string $compiledViewPath): static
{
$cloner = tap(new VarCloner)->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); // @phpstan-ignore method.notFound (tap proxy __call)
$dumper = new static(new ConsoleOutput, $basePath, $compiledViewPath);
VarDumper::setHandler(fn ($value) => $dumper->dumpWithSource($cloner->cloneVar($value)));
+
+ return $dumper;
}
/**
diff --git a/src/foundation/src/Console/ConfigCacheCommand.php b/src/foundation/src/Console/ConfigCacheCommand.php
index c0ea22216..3bdaca348 100644
--- a/src/foundation/src/Console/ConfigCacheCommand.php
+++ b/src/foundation/src/Console/ConfigCacheCommand.php
@@ -194,7 +194,7 @@ protected function getFreshConfigurationCacheContentsFromSubprocess(): string
*/
protected function buildFreshConfigurationCacheContents(): string
{
- $config = $this->hypervel['config']->all();
+ $config = $this->hypervel->make('config')->all();
$contents = 'components->info(sprintf(
'The application environment is [%s].',
- $this->hypervel['env'],
+ $this->hypervel->make('env'),
));
}
}
diff --git a/src/foundation/src/Console/Kernel.php b/src/foundation/src/Console/Kernel.php
index 08732b5c3..033743dd8 100644
--- a/src/foundation/src/Console/Kernel.php
+++ b/src/foundation/src/Console/Kernel.php
@@ -280,7 +280,7 @@ public function resolveConsoleSchedule(): Schedule
*/
protected function scheduleTimezone(): ?string
{
- $config = $this->app['config'];
+ $config = $this->app->make('config');
return $config->get('app.schedule_timezone', $config->get('app.timezone'));
}
@@ -290,7 +290,7 @@ protected function scheduleTimezone(): ?string
*/
protected function scheduleCache(): ?string
{
- return $this->app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER', function () {
+ return $this->app->make('config')->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER', function () {
return Env::get('SCHEDULE_CACHE_STORE');
}));
}
diff --git a/src/foundation/src/Console/RouteCacheCommand.php b/src/foundation/src/Console/RouteCacheCommand.php
index 2f044e2e6..b4a1900ff 100644
--- a/src/foundation/src/Console/RouteCacheCommand.php
+++ b/src/foundation/src/Console/RouteCacheCommand.php
@@ -50,7 +50,7 @@ public function handle(): int
// The app booted against a guaranteed-unused cache path, so the router
// holds a live RouteCollection loaded from source route definitions.
if (is_string($dumpPath = $this->option('dump-to')) && $dumpPath !== '') {
- $routes = $this->hypervel['router']->getRoutes();
+ $routes = $this->hypervel->make('router')->getRoutes();
if (! $routes instanceof RouteCollection) {
throw new LogicException('Fresh route dump expected a live RouteCollection.');
diff --git a/src/foundation/src/Console/RouteListCommand.php b/src/foundation/src/Console/RouteListCommand.php
index 75ba5a827..3597fe26f 100644
--- a/src/foundation/src/Console/RouteListCommand.php
+++ b/src/foundation/src/Console/RouteListCommand.php
@@ -413,7 +413,7 @@ protected function formatActionForCli(array $route): ?string
$name = $name ? "{$name} " : null;
- $rootControllerNamespace = $this->hypervel[UrlGenerator::class]->getRootControllerNamespace()
+ $rootControllerNamespace = $this->hypervel->make(UrlGenerator::class)->getRootControllerNamespace()
?? ($this->hypervel->getNamespace() . 'Http\Controllers');
if (str_starts_with($action, $rootControllerNamespace)) {
diff --git a/src/foundation/src/Exceptions/Renderer/Frame.php b/src/foundation/src/Exceptions/Renderer/Frame.php
index ae95bd1dc..bc433a332 100644
--- a/src/foundation/src/Exceptions/Renderer/Frame.php
+++ b/src/foundation/src/Exceptions/Renderer/Frame.php
@@ -4,19 +4,14 @@
namespace Hypervel\Foundation\Exceptions\Renderer;
-use Hypervel\Foundation\Concerns\ResolvesDumpSource;
+use Hypervel\Foundation\Concerns\ResolvesSourceHref;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use function Hypervel\Filesystem\join_paths;
class Frame
{
- use ResolvesDumpSource;
-
- /**
- * The compiled view path (required by ResolvesDumpSource, unused by Frame).
- */
- protected string $compiledViewPath = '';
+ use ResolvesSourceHref;
/**
* Whether this frame is the main (first non-vendor) frame.
diff --git a/src/foundation/src/Http/HtmlDumper.php b/src/foundation/src/Http/HtmlDumper.php
index 16b75fa69..e980eb59e 100644
--- a/src/foundation/src/Http/HtmlDumper.php
+++ b/src/foundation/src/Http/HtmlDumper.php
@@ -43,17 +43,16 @@ public function __construct(
*
* Boot-only. Registers a process-wide VarDumper handler for the worker
* lifetime.
- *
- * @param string $basePath
- * @param string $compiledViewPath
*/
- public static function register($basePath, $compiledViewPath): void
+ public static function register(string $basePath, string $compiledViewPath): static
{
$cloner = tap(new VarCloner)->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); // @phpstan-ignore method.notFound (tap proxy __call)
$dumper = new static($basePath, $compiledViewPath);
VarDumper::setHandler(fn ($value) => $dumper->dumpWithSource($cloner->cloneVar($value)));
+
+ return $dumper;
}
/**
diff --git a/src/foundation/src/Http/Kernel.php b/src/foundation/src/Http/Kernel.php
index 620f82a38..d3aba0dd3 100644
--- a/src/foundation/src/Http/Kernel.php
+++ b/src/foundation/src/Http/Kernel.php
@@ -133,7 +133,7 @@ public function handle(Request $request): Response
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
- $events = $this->app['events'];
+ $events = $this->app->make('events');
if ($events->hasListeners(RequestHandled::class)) {
$events->dispatch(
@@ -209,7 +209,7 @@ protected function dispatchToRouter(): Closure
public function terminate(Request $request, Response $response): void
{
$exception = null;
- $events = $this->app['events'];
+ $events = $this->app->make('events');
try {
if ($events->hasListeners(Terminating::class)) {
diff --git a/src/foundation/src/Listeners/ReloadDotenvAndConfig.php b/src/foundation/src/Listeners/ReloadDotenvAndConfig.php
index b139a1547..20cc4b974 100644
--- a/src/foundation/src/Listeners/ReloadDotenvAndConfig.php
+++ b/src/foundation/src/Listeners/ReloadDotenvAndConfig.php
@@ -5,6 +5,7 @@
namespace Hypervel\Foundation\Listeners;
use Hypervel\Config\Repository;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Core\Events\BeforeWorkerStart;
use Hypervel\Foundation\Application;
use Hypervel\Foundation\Bootstrap\LoadConfiguration;
@@ -33,6 +34,10 @@ protected function reloadConfig(): void
$config = $this->rebuildConfigRepository();
$this->configMutationTracker->replay($config);
+
+ foreach ($this->container->getProviders(ReloadsConfiguration::class) as $provider) {
+ $provider->reloadConfiguration();
+ }
}
protected function reloadDotenv(): void
diff --git a/src/foundation/src/Providers/FoundationServiceProvider.php b/src/foundation/src/Providers/FoundationServiceProvider.php
index 440adfe89..b54c912c3 100644
--- a/src/foundation/src/Providers/FoundationServiceProvider.php
+++ b/src/foundation/src/Providers/FoundationServiceProvider.php
@@ -15,6 +15,7 @@
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Foundation\ExceptionRenderer;
use Hypervel\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\View\Factory as ViewFactory;
use Hypervel\Core\Events\BeforeWorkerStart;
use Hypervel\Database\ConnectionInterface;
@@ -105,10 +106,12 @@
use Symfony\Component\VarDumper\Caster\StubCaster;
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
-class FoundationServiceProvider extends ServiceProvider
+class FoundationServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
protected Repository $config;
+ protected CliDumper|HtmlDumper|null $dumper = null;
+
public function __construct(protected ApplicationContract $app)
{
$this->config = $app->make('config');
@@ -145,7 +148,7 @@ public function boot(): void
public function register(): void
{
$this->app->singleton('composer', fn ($app) => new Composer(
- $app['files'],
+ $app->make('files'),
$app->basePath()
));
@@ -226,6 +229,25 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use replaces shared worker state while
+ * concurrent coroutines may still hold the previous objects.
+ */
+ public function reloadConfiguration(): void
+ {
+ $this->setDefaultTimezone();
+ $this->dumper?->setCompiledViewPath($this->config->string('view.compiled'));
+
+ if ($this->app->resolved(MaintenanceModeManager::class)) {
+ $this->app->make(MaintenanceModeManager::class)->forgetDrivers();
+ }
+
+ WorkerCachedMaintenanceMode::flushCache();
+ $this->app->forgetInstance(MaintenanceModeContract::class);
+ }
+
/**
* Register the framework clock implementation.
*/
@@ -253,13 +275,14 @@ protected function registerConsoleSchedule(): void
protected function registerDeferHandler(): void
{
$this->app->scoped(DeferredCallbackCollection::class);
+ $events = $this->app->make('events');
- $this->app['events']->listen(function (CommandFinished $event) {
+ $events->listen(function (CommandFinished $event) {
$this->app->make(DeferredCallbackCollection::class)
->invokeWhen(fn (DeferredCallback $callback) => $this->app->runningInConsole() && ($event->exitCode === 0 || $callback->always));
});
- $this->app['events']->listen(function (JobAttempted $event) {
+ $events->listen(function (JobAttempted $event) {
if (in_array($event->connectionName, ['sync', 'deferred'], true)) {
return;
}
@@ -420,12 +443,26 @@ protected function registerDumper(): void
$format = $_SERVER['VAR_DUMPER_FORMAT'] ?? null;
- match (true) {
- $format === 'html' => HtmlDumper::register($basePath, $compiledViewPath),
- $format === 'cli' => CliDumper::register($basePath, $compiledViewPath),
+ if (in_array($format, ['html', 'cli'], true)) {
+ unset($_SERVER['VAR_DUMPER_FORMAT']);
+
+ try {
+ $this->dumper = $format === 'html'
+ ? HtmlDumper::register($basePath, $compiledViewPath)
+ : CliDumper::register($basePath, $compiledViewPath);
+ } finally {
+ $_SERVER['VAR_DUMPER_FORMAT'] = $format;
+ }
+
+ return;
+ }
+
+ $this->dumper = match (true) {
$format === 'server' => null,
$format && parse_url($format, PHP_URL_SCHEME) === 'tcp' => null,
- default => php_sapi_name() === 'cli' ? CliDumper::register($basePath, $compiledViewPath) : HtmlDumper::register($basePath, $compiledViewPath),
+ default => in_array(PHP_SAPI, ['cli', 'phpdbg'], true)
+ ? CliDumper::register($basePath, $compiledViewPath)
+ : HtmlDumper::register($basePath, $compiledViewPath),
};
}
}
diff --git a/src/foundation/src/Support/Providers/RouteServiceProvider.php b/src/foundation/src/Support/Providers/RouteServiceProvider.php
index 5fc28a034..efaff67d6 100644
--- a/src/foundation/src/Support/Providers/RouteServiceProvider.php
+++ b/src/foundation/src/Support/Providers/RouteServiceProvider.php
@@ -51,8 +51,9 @@ public function register(): void
$this->loadRoutes();
$this->app->booted(function () {
- $this->app['router']->getRoutes()->refreshNameLookups();
- $this->app['router']->getRoutes()->refreshActionLookups();
+ $routes = $this->app->make('router')->getRoutes();
+ $routes->refreshNameLookups();
+ $routes->refreshActionLookups();
});
}
});
@@ -114,7 +115,7 @@ public static function flushState(): void
protected function setRootControllerNamespace(): void
{
if (! is_null($this->namespace)) {
- $this->app[UrlGenerator::class]->setRootControllerNamespace($this->namespace);
+ $this->app->make(UrlGenerator::class)->setRootControllerNamespace($this->namespace);
}
}
diff --git a/src/foundation/src/Testing/Concerns/InteractsWithAuthentication.php b/src/foundation/src/Testing/Concerns/InteractsWithAuthentication.php
index 8f2f05e30..97fd24cfe 100644
--- a/src/foundation/src/Testing/Concerns/InteractsWithAuthentication.php
+++ b/src/foundation/src/Testing/Concerns/InteractsWithAuthentication.php
@@ -21,9 +21,11 @@ public function actingAs(UserContract $user, ?string $guard = null): static
*/
public function actingAsGuest(?string $guard = null): static
{
- $this->app['auth']->guard($guard)->forgetUser();
+ $auth = $this->app->make('auth');
- $this->app['auth']->shouldUse($guard);
+ $auth->guard($guard)->forgetUser();
+
+ $auth->shouldUse($guard);
return $this;
}
@@ -37,9 +39,11 @@ public function be(UserContract $user, ?string $guard = null): static
$user->wasRecentlyCreated = false;
}
- $this->app['auth']->guard($guard)->setUser($user);
+ $auth = $this->app->make('auth');
+
+ $auth->guard($guard)->setUser($user);
- $this->app['auth']->shouldUse($guard);
+ $auth->shouldUse($guard);
return $this;
}
diff --git a/src/foundation/src/Testing/Concerns/InteractsWithSession.php b/src/foundation/src/Testing/Concerns/InteractsWithSession.php
index 9c7089409..6f15d4f18 100644
--- a/src/foundation/src/Testing/Concerns/InteractsWithSession.php
+++ b/src/foundation/src/Testing/Concerns/InteractsWithSession.php
@@ -22,9 +22,10 @@ public function withSession(array $data): static
public function session(array $data): static
{
$this->startSession();
+ $session = $this->app->make('session');
foreach ($data as $key => $value) {
- $this->app['session']->put($key, $value);
+ $session->put($key, $value);
}
return $this;
@@ -35,8 +36,10 @@ public function session(array $data): static
*/
protected function startSession(): static
{
- if (! $this->app['session']->isStarted()) {
- $this->app['session']->start();
+ $session = $this->app->make('session');
+
+ if (! $session->isStarted()) {
+ $session->start();
}
return $this;
@@ -49,7 +52,7 @@ public function flushSession(): static
{
$this->startSession();
- $this->app['session']->flush();
+ $this->app->make('session')->flush();
return $this;
}
diff --git a/src/foundation/src/Testing/Concerns/MakesHttpRequests.php b/src/foundation/src/Testing/Concerns/MakesHttpRequests.php
index d4b454cb6..dca1c7681 100644
--- a/src/foundation/src/Testing/Concerns/MakesHttpRequests.php
+++ b/src/foundation/src/Testing/Concerns/MakesHttpRequests.php
@@ -181,13 +181,13 @@ public function withoutMiddleware($middleware = null): static
public function withMiddleware($middleware = null): static
{
if (is_null($middleware)) {
- unset($this->app['middleware.disable']);
+ $this->app->forgetInstance('middleware.disable');
return $this;
}
foreach ((array) $middleware as $abstract) {
- unset($this->app[$abstract]);
+ $this->app->forgetInstance($abstract);
}
return $this;
@@ -268,7 +268,7 @@ public function disableCookieEncryption(): static
*/
public function from(string $url): static
{
- $this->app['session']->setPreviousUrl($url);
+ $this->app->make('session')->setPreviousUrl($url);
return $this->withHeader('referer', $url);
}
@@ -278,7 +278,7 @@ public function from(string $url): static
*/
public function fromRoute(BackedEnum|string $name, mixed $parameters = []): static
{
- return $this->from($this->app['url']->route($name, $parameters));
+ return $this->from($this->app->make('url')->route($name, $parameters));
}
/**
diff --git a/src/foundation/src/Testing/DatabaseTruncation.php b/src/foundation/src/Testing/DatabaseTruncation.php
index 85c9ea02f..8e9a93bdb 100644
--- a/src/foundation/src/Testing/DatabaseTruncation.php
+++ b/src/foundation/src/Testing/DatabaseTruncation.php
@@ -30,7 +30,7 @@ protected function truncateDatabaseTables(): void
if (! RefreshDatabaseState::$migrated) {
$this->artisan('migrate:fresh', $this->migrateFreshUsing());
- $this->app[Kernel::class]->setArtisan(null);
+ $this->app->make(Kernel::class)->setArtisan(null);
RefreshDatabaseState::$migrated = true;
@@ -151,7 +151,7 @@ protected function tablesToTruncate(ConnectionInterface $connection, ?string $co
*/
protected function exceptTables(ConnectionInterface $connection, ?string $connectionName): array
{
- $migrations = $this->app['config']->get('database.migrations');
+ $migrations = $this->app->make('config')->get('database.migrations');
$migrationsTable = is_array($migrations) ? ($migrations['table'] ?? 'migrations') : $migrations;
$migrationsTable = $connection->getTablePrefix() . $migrationsTable;
diff --git a/src/foundation/src/Testing/WithConsoleEvents.php b/src/foundation/src/Testing/WithConsoleEvents.php
index f9c4ba368..2c8013dd0 100644
--- a/src/foundation/src/Testing/WithConsoleEvents.php
+++ b/src/foundation/src/Testing/WithConsoleEvents.php
@@ -13,6 +13,6 @@ trait WithConsoleEvents
*/
protected function setUpWithConsoleEvents(): void
{
- $this->app[ConsoleKernel::class]->rerouteSymfonyCommandEvents();
+ $this->app->make(ConsoleKernel::class)->rerouteSymfonyCommandEvents();
}
}
diff --git a/src/foundation/src/WorkerCachedMaintenanceMode.php b/src/foundation/src/WorkerCachedMaintenanceMode.php
index 1e67efa31..2ac229201 100644
--- a/src/foundation/src/WorkerCachedMaintenanceMode.php
+++ b/src/foundation/src/WorkerCachedMaintenanceMode.php
@@ -71,6 +71,9 @@ public function data(): array
/**
* Flush the cached maintenance mode state.
+ *
+ * Boot or tests only. Request-time use clears worker-wide state while
+ * concurrent coroutines may still be using the previous snapshot.
*/
public static function flushCache(): void
{
diff --git a/src/grpc/src/GrpcServiceProvider.php b/src/grpc/src/GrpcServiceProvider.php
index d5301ff03..be03ae1f8 100644
--- a/src/grpc/src/GrpcServiceProvider.php
+++ b/src/grpc/src/GrpcServiceProvider.php
@@ -158,6 +158,7 @@ private function appendServer(ConfigRepository $config, array $server): void
),
];
+ // Replay the master snapshot; replacement workers cannot change the bound server topology.
$config->set('server.servers', $servers);
}
diff --git a/src/hashing/composer.json b/src/hashing/composer.json
index d03126344..3bffaf238 100644
--- a/src/hashing/composer.json
+++ b/src/hashing/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/hashing",
"type": "library",
- "description": "The hashing package for Hypervel.",
+ "description": "The Hypervel Hashing package.",
"license": "MIT",
"keywords": [
"php",
@@ -46,4 +46,4 @@
"dev-main": "0.4-dev"
}
}
-}
\ No newline at end of file
+}
diff --git a/src/hashing/src/HashingServiceProvider.php b/src/hashing/src/HashingServiceProvider.php
index 567f394da..9e06dde99 100644
--- a/src/hashing/src/HashingServiceProvider.php
+++ b/src/hashing/src/HashingServiceProvider.php
@@ -4,9 +4,10 @@
namespace Hypervel\Hashing;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Support\ServiceProvider;
-class HashingServiceProvider extends ServiceProvider
+class HashingServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -17,4 +18,19 @@ public function register(): void
$this->app->singleton('hash.driver', fn ($app) => $app->make('hash')->driver());
}
+
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared hash drivers while concurrent
+ * coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('hash')) {
+ $this->app->make('hash')->forgetDrivers();
+ }
+
+ $this->app->forgetInstance('hash.driver');
+ }
}
diff --git a/src/horizon/composer.json b/src/horizon/composer.json
index 2b8cfe076..fc57248aa 100644
--- a/src/horizon/composer.json
+++ b/src/horizon/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/horizon",
- "description": "The horizon package for Hypervel.",
+ "description": "The Hypervel Horizon package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/horizon/src/HorizonServiceProvider.php b/src/horizon/src/HorizonServiceProvider.php
index f1dd574d5..5908b4fcd 100644
--- a/src/horizon/src/HorizonServiceProvider.php
+++ b/src/horizon/src/HorizonServiceProvider.php
@@ -4,8 +4,10 @@
namespace Hypervel\Horizon;
+use Hypervel\Config\Repository;
use Hypervel\Contracts\Events\Dispatcher;
use Hypervel\Contracts\Redis\Factory as RedisFactory;
+use Hypervel\Foundation\Configuration\ConfigMutationTracker;
use Hypervel\Horizon\Connectors\RedisConnector;
use Hypervel\Queue\QueueManager;
use Hypervel\Support\Facades\Route;
@@ -35,11 +37,17 @@ public function boot(): void
*/
protected function normalizeConfig(): void
{
- $config = $this->app->make('config');
-
- if (($name = $config->get('horizon.name')) === null || $name === '') {
- $config->set('horizon.name', $config->string('app.name'));
- }
+ $config = $this->app->make(Repository::class);
+
+ // Derived config can depend on the worker environment, so replay the operation after config reload rather than its master result.
+ $this->app->make(ConfigMutationTracker::class)->applyAndRecord(
+ $config,
+ static function (Repository $config): void {
+ if (($name = $config->get('horizon.name')) === null || $name === '') {
+ $config->set('horizon.name', $config->string('app.name'));
+ }
+ },
+ );
}
/**
diff --git a/src/http/composer.json b/src/http/composer.json
index 040b649ed..0a5440dd0 100644
--- a/src/http/composer.json
+++ b/src/http/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/http",
"type": "library",
- "description": "The http package for Hypervel.",
+ "description": "The Hypervel HTTP package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/http/src/Client/Factory.php b/src/http/src/Client/Factory.php
index 3022b252e..b59370506 100644
--- a/src/http/src/Client/Factory.php
+++ b/src/http/src/Client/Factory.php
@@ -681,6 +681,19 @@ public function setConnectionConfig(string $name, array $config): static
return $this;
}
+ /**
+ * Forget all resolved connection handlers.
+ *
+ * Boot or tests only. Request-time use forces subsequent requests to
+ * rebuild warmed keep-alive, DNS, and TLS session state.
+ */
+ public function forgetConnectionHandlers(): static
+ {
+ $this->connectionHandlers = [];
+
+ return $this;
+ }
+
/**
* Ensure the given HTTP client connection is registered.
*/
diff --git a/src/http/src/HttpServiceProvider.php b/src/http/src/HttpServiceProvider.php
index cb0d31330..e549c75fb 100644
--- a/src/http/src/HttpServiceProvider.php
+++ b/src/http/src/HttpServiceProvider.php
@@ -6,6 +6,8 @@
use Http\Discovery\ClassDiscovery;
use Hypervel\Context\RequestContext;
+use Hypervel\Core\Events\BeforeServerFork;
+use Hypervel\Http\Client\Factory;
use Hypervel\Http\Discovery\GuzzlePsr18Strategy;
use Hypervel\Support\ServiceProvider;
@@ -21,6 +23,22 @@ public function register(): void
$this->registerResponseFactory();
}
+ /**
+ * Bootstrap the service provider.
+ */
+ public function boot(): void
+ {
+ $events = $this->app->make('events');
+
+ $events->listen(BeforeServerFork::class, function (): void {
+ // The framework leaves Factory unbound, so a resolved concrete
+ // identifies the auto-singleton that would cross the fork.
+ if ($this->app->resolved(Factory::class)) {
+ $this->app->make(Factory::class)->forgetConnectionHandlers();
+ }
+ });
+ }
+
/**
* Register Guzzle as the preferred PSR-18 client for auto-discovery.
*
diff --git a/src/image/composer.json b/src/image/composer.json
index 6155d9ba0..96bc89a14 100644
--- a/src/image/composer.json
+++ b/src/image/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/image",
"type": "library",
- "description": "The image package for Hypervel.",
+ "description": "The Hypervel Image package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/inertia/src/InertiaServiceProvider.php b/src/inertia/src/InertiaServiceProvider.php
index 134699199..d17b65d69 100644
--- a/src/inertia/src/InertiaServiceProvider.php
+++ b/src/inertia/src/InertiaServiceProvider.php
@@ -4,6 +4,7 @@
namespace Hypervel\Inertia;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Http\Kernel as HttpKernelContract;
use Hypervel\Http\RedirectResponse;
use Hypervel\Http\Request;
@@ -18,7 +19,7 @@
use Hypervel\View\FileViewFinder;
use LogicException;
-class InertiaServiceProvider extends ServiceProvider
+class InertiaServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -67,6 +68,17 @@ public function boot(): void
]);
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ $this->app->forgetInstance('inertia.view-finder');
+ }
+
/**
* Register the global redirect middleware for Inertia requests.
*/
diff --git a/src/json-schema/composer.json b/src/json-schema/composer.json
index e38e9082c..0ff4c7590 100644
--- a/src/json-schema/composer.json
+++ b/src/json-schema/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/json-schema",
"type": "library",
- "description": "The JSON Schema package for Hypervel.",
+ "description": "The Hypervel JSON Schema package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/jwt/composer.json b/src/jwt/composer.json
index c1fb118f9..520aefba1 100644
--- a/src/jwt/composer.json
+++ b/src/jwt/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/jwt",
- "description": "The jwt package for Hypervel.",
+ "description": "JWT authentication for Hypervel applications.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/jwt/src/ClaimFactory.php b/src/jwt/src/ClaimFactory.php
index a60caf8b9..739b70258 100644
--- a/src/jwt/src/ClaimFactory.php
+++ b/src/jwt/src/ClaimFactory.php
@@ -32,13 +32,25 @@ class ClaimFactory
/**
* Create a new claim factory.
*/
- public function __construct(Repository $config)
+ public function __construct(
+ protected Repository $config
+ ) {
+ $this->reloadConfiguration();
+ }
+
+ /**
+ * Reload configuration-derived claim state.
+ *
+ * Boot-only. Mutates the worker-shared factory while concurrent coroutines
+ * may still issue or inspect claims using its previous configuration.
+ */
+ public function reloadConfiguration(): void
{
/** @var null|string $issuer */
- $issuer = $config->get('jwt.issuer');
+ $issuer = $this->config->get('jwt.issuer');
$this->issuer = ($issuer === null || $issuer === '') ? null : $issuer;
- $this->lockSubject = $config->boolean('jwt.lock_subject');
+ $this->lockSubject = $this->config->boolean('jwt.lock_subject');
}
/**
diff --git a/src/jwt/src/JwtManager.php b/src/jwt/src/JwtManager.php
index 2f57825e4..d3be16b08 100644
--- a/src/jwt/src/JwtManager.php
+++ b/src/jwt/src/JwtManager.php
@@ -36,9 +36,22 @@ public function __construct(
) {
parent::__construct($container);
+ $this->reloadConfiguration();
+ }
+
+ /**
+ * Reload configuration-derived manager state.
+ *
+ * Boot-only. Mutates the worker-shared manager while concurrent coroutines
+ * may still use drivers and validations built from previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ $this->forgetDrivers();
+ $this->validations = [];
$this->blacklistEnabled = $this->config->boolean('jwt.blacklist_enabled');
$this->blacklist = $this->blacklistEnabled
- ? $container->make(BlacklistContract::class)
+ ? $this->container->make(BlacklistContract::class)
: null;
}
diff --git a/src/jwt/src/JwtServiceProvider.php b/src/jwt/src/JwtServiceProvider.php
index 72043323c..dba01e57e 100644
--- a/src/jwt/src/JwtServiceProvider.php
+++ b/src/jwt/src/JwtServiceProvider.php
@@ -7,6 +7,7 @@
use Hypervel\Auth\AuthManager;
use Hypervel\Cache\Repository as CacheRepository;
use Hypervel\Contracts\Container\Container;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Jwt\Console\JwtGenerateCertsCommand;
use Hypervel\Jwt\Console\JwtSecretCommand;
use Hypervel\Jwt\Contracts\BlacklistContract;
@@ -18,7 +19,7 @@
use Hypervel\Support\ServiceProvider;
use RuntimeException;
-class JwtServiceProvider extends ServiceProvider
+class JwtServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -99,6 +100,30 @@ public function boot(): void
}
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(ClaimFactory::class)) {
+ $this->app->make(ClaimFactory::class)->reloadConfiguration();
+ }
+
+ $this->app->forgetInstance(Parser::class);
+ $this->app->forgetInstance(BlacklistContract::class);
+
+ if ($this->app->resolved('jwt')) {
+ $manager = $this->app->make('jwt');
+
+ if ($manager instanceof JwtManager) {
+ $manager->reloadConfiguration();
+ }
+ }
+ }
+
/**
* Register the JWT authentication guard.
*/
diff --git a/src/log/composer.json b/src/log/composer.json
index eb064c4c2..8cc6c82fe 100644
--- a/src/log/composer.json
+++ b/src/log/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/log",
"type": "library",
- "description": "The log package for Hypervel.",
+ "description": "The Hypervel Log package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/log/src/Context/ContextServiceProvider.php b/src/log/src/Context/ContextServiceProvider.php
index ce3790dbb..cffa5b3d5 100644
--- a/src/log/src/Context/ContextServiceProvider.php
+++ b/src/log/src/Context/ContextServiceProvider.php
@@ -40,7 +40,7 @@ public function boot(): void
});
// IMPORTANT: Uses Laravel's payload key for cross-framework queue interoperability.
- $this->app['events']->listen(JobProcessing::class, function (JobProcessing $event): void {
+ $this->app->make('events')->listen(JobProcessing::class, function (JobProcessing $event): void {
$context = $event->job->payload()['illuminate:log:context'] ?? null;
if ($context !== null || Repository::hasInstance()) {
diff --git a/src/log/src/LogManager.php b/src/log/src/LogManager.php
index 70bf4f24a..2fb02585f 100644
--- a/src/log/src/LogManager.php
+++ b/src/log/src/LogManager.php
@@ -103,7 +103,7 @@ public function stack(array $channels, ?string $channel = null): LoggerInterface
return (new Logger(
$monolog,
- $this->app['events']
+ $this->app->make('events')
))->withContext($this->sharedContext());
}
@@ -153,7 +153,7 @@ protected function createLogger(?string $name, ?array $config = null, bool $cach
$logger = $this->tap(
$config,
- new Logger($this->resolve($name, $config), $this->app['events'])
+ new Logger($this->resolve($name, $config), $this->app->make('events'))
)->withContext($this->sharedContext());
$underlyingLogger = $logger->getLogger();
@@ -212,7 +212,7 @@ protected function createEmergencyLogger(): LoggerInterface
return new Logger(
new Monolog('hypervel', $this->prepareHandlers([$handler])),
- $this->app['events']
+ $this->app->make('events')
);
}
@@ -622,6 +622,19 @@ public function forgetChannel(?string $driver = null): void
}
}
+ /**
+ * Forget all resolved log channels.
+ *
+ * Boot or tests only. Mutates the singleton's channel cache; concurrent
+ * coroutines may already hold channels that next resolution will replace.
+ */
+ public function forgetChannels(): static
+ {
+ $this->channels = [];
+
+ return $this;
+ }
+
/**
* Parse the driver name.
*/
diff --git a/src/log/src/LogServiceProvider.php b/src/log/src/LogServiceProvider.php
index 26978ef0b..9fcfb2840 100644
--- a/src/log/src/LogServiceProvider.php
+++ b/src/log/src/LogServiceProvider.php
@@ -4,9 +4,12 @@
namespace Hypervel\Log;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
+use Hypervel\Contracts\Log\StdoutLoggerInterface;
+use Hypervel\Core\Logger\StdoutLogger;
use Hypervel\Support\ServiceProvider;
-class LogServiceProvider extends ServiceProvider
+class LogServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -15,4 +18,27 @@ public function register(): void
{
$this->app->singleton('log', fn ($app) => new LogManager($app));
}
+
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use replaces shared logging configuration while
+ * concurrent coroutines may still be writing through existing loggers.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('log')) {
+ $this->app->make('log')->forgetChannels();
+ }
+
+ if (! $this->app->resolved(StdoutLoggerInterface::class)) {
+ return;
+ }
+
+ $logger = $this->app->make(StdoutLoggerInterface::class);
+
+ if ($logger instanceof StdoutLogger) {
+ $logger->reloadConfiguration();
+ }
+ }
}
diff --git a/src/mail/composer.json b/src/mail/composer.json
index 4051c3c88..b444e31dd 100644
--- a/src/mail/composer.json
+++ b/src/mail/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/mail",
"type": "library",
- "description": "The mail package for Hypervel.",
+ "description": "The Hypervel Mail package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/mail/src/MailServiceProvider.php b/src/mail/src/MailServiceProvider.php
index 0df1e0cbd..d266b5fee 100644
--- a/src/mail/src/MailServiceProvider.php
+++ b/src/mail/src/MailServiceProvider.php
@@ -4,9 +4,10 @@
namespace Hypervel\Mail;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Support\ServiceProvider;
-class MailServiceProvider extends ServiceProvider
+class MailServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -17,6 +18,21 @@ public function register(): void
$this->registerMarkdownRenderer();
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared mailers and Markdown settings
+ * while concurrent coroutines may still be using the previous objects.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('mail.manager')) {
+ $this->app->make('mail.manager')->forgetMailers();
+ }
+
+ $this->app->forgetInstance(Markdown::class);
+ }
+
/**
* Register the mailer instance.
*
diff --git a/src/nested-set/composer.json b/src/nested-set/composer.json
index 2376112ed..9ab1c4526 100644
--- a/src/nested-set/composer.json
+++ b/src/nested-set/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/nested-set",
"type": "library",
- "description": "The nested-set package for Hypervel.",
+ "description": "Nested set support for Hypervel Eloquent models.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/notifications/composer.json b/src/notifications/composer.json
index dc34ff3f1..0ab385086 100644
--- a/src/notifications/composer.json
+++ b/src/notifications/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/notifications",
"type": "library",
- "description": "The notifications package for Hypervel.",
+ "description": "The Hypervel Notifications package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/notifications/src/NotificationServiceProvider.php b/src/notifications/src/NotificationServiceProvider.php
index f480cb5f8..96aabfd87 100644
--- a/src/notifications/src/NotificationServiceProvider.php
+++ b/src/notifications/src/NotificationServiceProvider.php
@@ -5,12 +5,14 @@
namespace Hypervel\Notifications;
use Hypervel\Context\CoroutineContext;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Notifications\Dispatcher as DispatcherContract;
use Hypervel\Contracts\Notifications\Factory as FactoryContract;
+use Hypervel\Notifications\Channels\MailChannel;
use Hypervel\Notifications\Events\NotificationFailed;
use Hypervel\Support\ServiceProvider;
-class NotificationServiceProvider extends ServiceProvider
+class NotificationServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -25,6 +27,21 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared notification channels while
+ * concurrent coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(ChannelManager::class)) {
+ $this->app->make(ChannelManager::class)->forgetDrivers();
+ }
+
+ $this->app->forgetInstance(MailChannel::class);
+ }
+
/**
* Bootstrap the service provider.
*/
diff --git a/src/object-pool/composer.json b/src/object-pool/composer.json
index b57b802b5..4d1982c02 100644
--- a/src/object-pool/composer.json
+++ b/src/object-pool/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/object-pool",
- "description": "The object pool package for Hypervel.",
+ "description": "Object pooling for Hypervel applications and packages.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/object-pool/src/ObjectPoolServiceProvider.php b/src/object-pool/src/ObjectPoolServiceProvider.php
index 739df4c3d..98c5a7958 100644
--- a/src/object-pool/src/ObjectPoolServiceProvider.php
+++ b/src/object-pool/src/ObjectPoolServiceProvider.php
@@ -5,6 +5,7 @@
namespace Hypervel\ObjectPool;
use Hypervel\Core\Events\AfterWorkerStart;
+use Hypervel\Core\Events\BeforeServerFork;
use Hypervel\ObjectPool\Contracts\Factory;
use Hypervel\ObjectPool\Contracts\Recycler;
use Hypervel\ObjectPool\Listeners\StartRecycler;
@@ -29,7 +30,13 @@ public function boot(): void
{
$events = $this->app->make('events');
- $events->listen(AfterWorkerStart::class, function (AfterWorkerStart $event) {
+ $events->listen(BeforeServerFork::class, function (): void {
+ if ($this->app->resolved(PoolManager::class)) {
+ $this->app->make(PoolManager::class)->flush();
+ }
+ });
+
+ $events->listen(AfterWorkerStart::class, function (AfterWorkerStart $event): void {
$this->app->make(StartRecycler::class)->handle($event);
});
}
diff --git a/src/pagination/composer.json b/src/pagination/composer.json
index 4e9dac94c..e2284641f 100644
--- a/src/pagination/composer.json
+++ b/src/pagination/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/pagination",
"type": "library",
- "description": "The pagination package for Hypervel.",
+ "description": "The Hypervel Pagination package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/passkeys/composer.json b/src/passkeys/composer.json
index 89ffcd923..84db710b5 100644
--- a/src/passkeys/composer.json
+++ b/src/passkeys/composer.json
@@ -10,6 +10,16 @@
"swoole",
"webauthn"
],
+ "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"
diff --git a/src/permission/composer.json b/src/permission/composer.json
index 7de92d3ed..9b43f3adb 100644
--- a/src/permission/composer.json
+++ b/src/permission/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/permission",
"type": "library",
- "description": "The permission package for Hypervel.",
+ "description": "Role and permission management for Hypervel Eloquent models.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/permission/src/PermissionServiceProvider.php b/src/permission/src/PermissionServiceProvider.php
index 5b5c1b7ee..d16089611 100644
--- a/src/permission/src/PermissionServiceProvider.php
+++ b/src/permission/src/PermissionServiceProvider.php
@@ -9,6 +9,7 @@
use Hypervel\Container\Container;
use Hypervel\Contracts\Auth\Access\Gate as GateContract;
use Hypervel\Contracts\Auth\Factory as AuthFactory;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Foundation\Console\AboutCommand;
use Hypervel\Permission\Commands\AssignRoleCommand;
use Hypervel\Permission\Commands\CacheResetCommand;
@@ -29,7 +30,7 @@
use function Hypervel\Support\enum_value;
-class PermissionServiceProvider extends ServiceProvider
+class PermissionServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register any package services.
@@ -72,6 +73,19 @@ public function boot(): void
// state in CoroutineContext and keeps permission cache freshness in the cache layer.
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(PermissionRegistrar::class)) {
+ $this->app->make(PermissionRegistrar::class)->initializeCache();
+ }
+ }
+
/**
* Wrap a Blade authorization check.
*/
diff --git a/src/pipeline/composer.json b/src/pipeline/composer.json
index 11b30fcca..2b90bc056 100644
--- a/src/pipeline/composer.json
+++ b/src/pipeline/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/pipeline",
"type": "library",
- "description": "The pipeline package for Hypervel.",
+ "description": "The Hypervel Pipeline package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/pool/composer.json b/src/pool/composer.json
index fe6f3c554..78f6d8590 100644
--- a/src/pool/composer.json
+++ b/src/pool/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/pool",
"type": "library",
- "description": "The Hypervel Pool package for connection pooling.",
+ "description": "Connection pooling for Hypervel packages.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/process/composer.json b/src/process/composer.json
index f22f90db8..0a82d884c 100644
--- a/src/process/composer.json
+++ b/src/process/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/process",
- "description": "The process package for Hypervel.",
+ "description": "The Hypervel Process package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/queue/composer.json b/src/queue/composer.json
index 37257e738..6df1d3c5a 100644
--- a/src/queue/composer.json
+++ b/src/queue/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/queue",
- "description": "The queue package for Hypervel.",
+ "description": "The Hypervel Queue package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/queue/src/Connectors/BackgroundConnector.php b/src/queue/src/Connectors/BackgroundConnector.php
index 7c699e291..2c306c1d6 100644
--- a/src/queue/src/Connectors/BackgroundConnector.php
+++ b/src/queue/src/Connectors/BackgroundConnector.php
@@ -4,16 +4,26 @@
namespace Hypervel\Queue\Connectors;
+use Closure;
use Hypervel\Contracts\Queue\Queue;
use Hypervel\Queue\BackgroundQueue;
class BackgroundConnector implements ConnectorInterface
{
+ /**
+ * Create a new background connector instance.
+ */
+ public function __construct(
+ protected ?Closure $exceptionCallback = null
+ ) {
+ }
+
/**
* Establish a queue connection.
*/
public function connect(array $config): Queue
{
- return new BackgroundQueue($config['after_commit'] ?? false);
+ return (new BackgroundQueue($config['after_commit'] ?? false))
+ ->setExceptionCallback($this->exceptionCallback);
}
}
diff --git a/src/queue/src/Connectors/DeferredConnector.php b/src/queue/src/Connectors/DeferredConnector.php
index db2f00ed0..a59e7d066 100644
--- a/src/queue/src/Connectors/DeferredConnector.php
+++ b/src/queue/src/Connectors/DeferredConnector.php
@@ -4,16 +4,26 @@
namespace Hypervel\Queue\Connectors;
+use Closure;
use Hypervel\Contracts\Queue\Queue;
use Hypervel\Queue\DeferredQueue;
class DeferredConnector implements ConnectorInterface
{
+ /**
+ * Create a new deferred connector instance.
+ */
+ public function __construct(
+ protected ?Closure $exceptionCallback = null
+ ) {
+ }
+
/**
* Establish a queue connection.
*/
public function connect(array $config): Queue
{
- return new DeferredQueue($config['after_commit'] ?? false);
+ return (new DeferredQueue($config['after_commit'] ?? false))
+ ->setExceptionCallback($this->exceptionCallback);
}
}
diff --git a/src/queue/src/Console/ClearCommand.php b/src/queue/src/Console/ClearCommand.php
index fea947e58..9016a8b58 100644
--- a/src/queue/src/Console/ClearCommand.php
+++ b/src/queue/src/Console/ClearCommand.php
@@ -48,7 +48,7 @@ public function handle(): ?int
// connection being run for the queue operation currently being executed.
$queueName = $this->getQueue($connection);
- $queue = $this->hypervel['queue']->connection($connection);
+ $queue = $this->hypervel->make('queue')->connection($connection);
if ($queue instanceof ClearableQueue) {
$count = $queue->clear($queueName);
diff --git a/src/queue/src/Console/RetryCommand.php b/src/queue/src/Console/RetryCommand.php
index d7c6b0e3a..cbfa0d48d 100644
--- a/src/queue/src/Console/RetryCommand.php
+++ b/src/queue/src/Console/RetryCommand.php
@@ -53,7 +53,7 @@ public function handle(): void
if (is_null($job)) {
$this->components->error("Unable to find failed job with ID [{$id}].");
} else {
- $this->hypervel['events']->dispatch(new JobRetryRequested($job));
+ $this->hypervel->make('events')->dispatch(new JobRetryRequested($job));
$this->components->task($id, fn () => $this->retryJob($job));
@@ -127,7 +127,7 @@ protected function getJobIdsByRanges(array $ranges): array
*/
protected function retryJob(stdClass $job): void
{
- $queue = $this->hypervel['queue']->connection($job->connection);
+ $queue = $this->hypervel->make('queue')->connection($job->connection);
$queue->pushRaw(
$this->refreshRetryUntil($this->resetAttempts($job->payload)),
diff --git a/src/queue/src/Console/WorkCommand.php b/src/queue/src/Console/WorkCommand.php
index 996e7f2fc..c25c07ade 100644
--- a/src/queue/src/Console/WorkCommand.php
+++ b/src/queue/src/Console/WorkCommand.php
@@ -175,19 +175,21 @@ protected function listenForEvents(): void
return;
}
- $this->hypervel['events']->listen(JobProcessing::class, static function (JobProcessing $event): void {
+ $events = $this->hypervel->make('events');
+
+ $events->listen(JobProcessing::class, static function (JobProcessing $event): void {
static::currentCommand()?->writeOutput($event->job, 'starting');
});
- $this->hypervel['events']->listen(JobProcessed::class, static function (JobProcessed $event): void {
+ $events->listen(JobProcessed::class, static function (JobProcessed $event): void {
static::currentCommand()?->writeOutput($event->job, 'success');
});
- $this->hypervel['events']->listen(JobReleasedAfterException::class, static function (JobReleasedAfterException $event): void {
+ $events->listen(JobReleasedAfterException::class, static function (JobReleasedAfterException $event): void {
static::currentCommand()?->writeOutput($event->job, 'released_after_exception');
});
- $this->hypervel['events']->listen(JobFailed::class, static function (JobFailed $event): void {
+ $events->listen(JobFailed::class, static function (JobFailed $event): void {
$command = static::currentCommand();
$command?->logFailedJob($event);
diff --git a/src/queue/src/QueueManager.php b/src/queue/src/QueueManager.php
index 4e90cd7e9..201c57046 100644
--- a/src/queue/src/QueueManager.php
+++ b/src/queue/src/QueueManager.php
@@ -60,7 +60,7 @@ public function __construct(
*/
public function before(mixed $callback): void
{
- $this->app['events']
+ $this->app->make('events')
->listen(Events\JobProcessing::class, $callback);
}
@@ -72,7 +72,7 @@ public function before(mixed $callback): void
*/
public function after(mixed $callback): void
{
- $this->app['events']
+ $this->app->make('events')
->listen(Events\JobProcessed::class, $callback);
}
@@ -84,7 +84,7 @@ public function after(mixed $callback): void
*/
public function exceptionOccurred(mixed $callback): void
{
- $this->app['events']
+ $this->app->make('events')
->listen(Events\JobExceptionOccurred::class, $callback);
}
@@ -96,7 +96,7 @@ public function exceptionOccurred(mixed $callback): void
*/
public function looping(mixed $callback): void
{
- $this->app['events']
+ $this->app->make('events')
->listen(Events\Looping::class, $callback);
}
@@ -108,7 +108,7 @@ public function looping(mixed $callback): void
*/
public function failing(mixed $callback): void
{
- $this->app['events']
+ $this->app->make('events')
->listen(Events\JobFailed::class, $callback);
}
@@ -120,7 +120,7 @@ public function failing(mixed $callback): void
*/
public function starting(mixed $callback): void
{
- $this->app['events']
+ $this->app->make('events')
->listen(Events\WorkerStarting::class, $callback);
}
@@ -132,7 +132,7 @@ public function starting(mixed $callback): void
*/
public function stopping(mixed $callback): void
{
- $this->app['events']
+ $this->app->make('events')
->listen(Events\WorkerStopping::class, $callback);
}
@@ -155,11 +155,11 @@ public function route(array|string $class, UnitEnum|string|null $queue = null, U
public function pause(string $connection, string $queue): void
{
// IMPORTANT: Uses Laravel's key for cross-framework queue interoperability.
- $this->app['cache']
+ $this->app->make('cache')
->store()
->forever("illuminate:queue:paused:{$connection}:{$queue}", true);
- $this->app['events']->dispatch(
+ $this->app->make('events')->dispatch(
new Events\QueuePaused($connection, $queue)
);
}
@@ -170,11 +170,11 @@ public function pause(string $connection, string $queue): void
public function pauseFor(string $connection, string $queue, DateInterval|DateTimeInterface|int $ttl): void
{
// IMPORTANT: Uses Laravel's key for cross-framework queue interoperability.
- $this->app['cache']
+ $this->app->make('cache')
->store()
->put("illuminate:queue:paused:{$connection}:{$queue}", true, $ttl);
- $this->app['events']->dispatch(
+ $this->app->make('events')->dispatch(
new Events\QueuePaused($connection, $queue, $ttl)
);
}
@@ -185,11 +185,11 @@ public function pauseFor(string $connection, string $queue, DateInterval|DateTim
public function resume(string $connection, string $queue): void
{
// IMPORTANT: Uses Laravel's key for cross-framework queue interoperability.
- $this->app['cache']
+ $this->app->make('cache')
->store()
->forget("illuminate:queue:paused:{$connection}:{$queue}");
- $this->app['events']->dispatch(
+ $this->app->make('events')->dispatch(
new Events\QueueResumed($connection, $queue)
);
}
@@ -200,7 +200,7 @@ public function resume(string $connection, string $queue): void
public function isPaused(string $connection, string $queue): bool
{
// IMPORTANT: Uses Laravel's key for cross-framework queue interoperability.
- return (bool) $this->app['cache']
+ return (bool) $this->app->make('cache')
->store()
->get("illuminate:queue:paused:{$connection}:{$queue}", false);
}
@@ -394,6 +394,19 @@ public function getName(?string $connection = null): string
: $connection;
}
+ /**
+ * Forget all resolved queue connections.
+ *
+ * Boot or tests only. Mutates the singleton's connection cache; concurrent
+ * coroutines may already hold connections that next resolution will replace.
+ */
+ public function forgetConnections(): static
+ {
+ $this->connections = [];
+
+ return $this;
+ }
+
/**
* Disconnect a queue connection and close its shared resource pool.
*
diff --git a/src/queue/src/QueueServiceProvider.php b/src/queue/src/QueueServiceProvider.php
index ad746d3da..30a7c87a0 100644
--- a/src/queue/src/QueueServiceProvider.php
+++ b/src/queue/src/QueueServiceProvider.php
@@ -4,9 +4,11 @@
namespace Hypervel\Queue;
+use Closure;
use Hypervel\Contracts\Database\ModelIdentifier;
use Hypervel\Contracts\Debug\ExceptionHandler;
use Hypervel\Contracts\Events\Dispatcher as EventDispatcher;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Redis\Factory as RedisFactory;
use Hypervel\Database\ConnectionResolverInterface;
use Hypervel\Queue\Connectors\BackgroundConnector;
@@ -44,7 +46,7 @@
use Laravel\SerializableClosure\SerializableClosure;
use Throwable;
-class QueueServiceProvider extends ServiceProvider
+class QueueServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
use SerializesAndRestoresModelIdentifiers;
@@ -86,6 +88,22 @@ public function register(): void
$this->registerLaravelInteropAliases();
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use replaces shared queue connections while
+ * concurrent coroutines may still be using the previous objects.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('queue')) {
+ $this->app->make('queue')->forgetConnections();
+ }
+
+ $this->app->forgetInstance('queue.connection');
+ $this->app->forgetInstance('queue.failer');
+ }
+
/**
* Configure serializable closure uses.
*/
@@ -142,26 +160,9 @@ class_alias(ModelIdentifier::class, 'Illuminate\Contracts\Database\ModelIdentifi
protected function registerManager(): void
{
$this->app->singleton('queue', function ($app) {
- $manager = tap(new QueueManager($app), function ($manager) {
+ return tap(new QueueManager($app), function ($manager) {
$this->registerConnectors($manager);
});
-
- if (! $app->has(ExceptionHandler::class)) {
- return $manager;
- }
-
- $reportHandler = fn (Throwable $e) => $app->make(ExceptionHandler::class)->report($e);
-
- foreach (['background', 'deferred'] as $connector) {
- try {
- $manager->connection($connector)
- ->setExceptionCallback($reportHandler); // @phpstan-ignore method.notFound (setExceptionCallback is on concrete Queue, not contract)
- } catch (InvalidArgumentException) {
- // Ignore exception when the connector is not configured.
- }
- }
-
- return $manager;
});
}
@@ -170,7 +171,7 @@ protected function registerManager(): void
*/
protected function registerConnection(): void
{
- $this->app->singleton('queue.connection', fn ($app) => $app['queue']->connection());
+ $this->app->singleton('queue.connection', fn ($app) => $app->make('queue')->connection());
}
/**
@@ -183,6 +184,18 @@ public function registerConnectors(QueueManager $manager): void
}
}
+ /**
+ * Get the exception reporter for in-process queue connections.
+ */
+ protected function exceptionReporter(): ?Closure
+ {
+ if (! $this->app->has(ExceptionHandler::class)) {
+ return null;
+ }
+
+ return fn (Throwable $exception) => $this->app->make(ExceptionHandler::class)->report($exception);
+ }
+
/**
* Register the Null queue connector.
*/
@@ -204,7 +217,7 @@ protected function registerSyncConnector(QueueManager $manager): void
*/
protected function registerDeferredConnector(QueueManager $manager): void
{
- $manager->addConnector('deferred', fn () => new DeferredConnector);
+ $manager->addConnector('deferred', fn () => new DeferredConnector($this->exceptionReporter()));
}
/**
@@ -212,7 +225,7 @@ protected function registerDeferredConnector(QueueManager $manager): void
*/
protected function registerBackgroundConnector(QueueManager $manager): void
{
- $manager->addConnector('background', fn () => new BackgroundConnector);
+ $manager->addConnector('background', fn () => new BackgroundConnector($this->exceptionReporter()));
}
/**
@@ -275,8 +288,8 @@ protected function registerWorker(): void
{
$this->app->singleton('queue.worker', function ($app) {
return new Worker(
- $app['queue'],
- $app['events'],
+ $app->make('queue'),
+ $app->make('events'),
$app->make(ExceptionHandler::class),
fn () => $app->isDownForMaintenance(),
);
diff --git a/src/queue/src/SyncQueue.php b/src/queue/src/SyncQueue.php
index 040810ca8..46dbfdc97 100644
--- a/src/queue/src/SyncQueue.php
+++ b/src/queue/src/SyncQueue.php
@@ -191,7 +191,7 @@ protected function resolveJob(string $payload, ?string $queue): SyncJob
protected function raiseBeforeJobEvent(JobContract $job): void
{
if ($this->container->bound('events')) {
- $this->container['events']
+ $this->container->make('events')
->dispatch(new JobProcessing($this->connectionName, $job));
}
}
@@ -202,7 +202,7 @@ protected function raiseBeforeJobEvent(JobContract $job): void
protected function raiseAfterJobEvent(JobContract $job): void
{
if ($this->container->bound('events')) {
- $this->container['events']
+ $this->container->make('events')
->dispatch(new JobProcessed($this->connectionName, $job));
}
}
@@ -213,7 +213,7 @@ protected function raiseAfterJobEvent(JobContract $job): void
protected function raiseJobAttemptedEvent(JobContract $job, ?Throwable $exceptionOccurred = null): void
{
if ($this->container->bound('events')) {
- $this->container['events']
+ $this->container->make('events')
->dispatch(new JobAttempted($this->connectionName, $job, $exceptionOccurred));
}
}
@@ -224,7 +224,7 @@ protected function raiseJobAttemptedEvent(JobContract $job, ?Throwable $exceptio
protected function raiseExceptionOccurredJobEvent(JobContract $job, Throwable $e): void
{
if ($this->container->bound('events')) {
- $this->container['events']
+ $this->container->make('events')
->dispatch(new JobExceptionOccurred($this->connectionName, $job, $e));
}
}
diff --git a/src/rate-limiter/composer.json b/src/rate-limiter/composer.json
index 95c54a383..7876b0898 100644
--- a/src/rate-limiter/composer.json
+++ b/src/rate-limiter/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/rate-limiter",
"type": "library",
- "description": "The rate limiter package for Hypervel.",
+ "description": "Atomic rate limiting for Hypervel applications.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/rate-limiter/src/RateLimiterServiceProvider.php b/src/rate-limiter/src/RateLimiterServiceProvider.php
index bcb4aaced..e419fe3f4 100644
--- a/src/rate-limiter/src/RateLimiterServiceProvider.php
+++ b/src/rate-limiter/src/RateLimiterServiceProvider.php
@@ -4,6 +4,7 @@
namespace Hypervel\RateLimiter;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Core\Events\AfterWorkerStart;
use Hypervel\Core\Events\BeforeServerStart;
use Hypervel\RateLimiter\Console\PruneCommand;
@@ -12,7 +13,7 @@
use Hypervel\RateLimiter\Listeners\RegisterPruneTimer;
use Hypervel\Support\ServiceProvider;
-class RateLimiterServiceProvider extends ServiceProvider
+class RateLimiterServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -25,6 +26,19 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use clears shared rate limiter stores while
+ * concurrent coroutines may still be using them.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(RateLimiter::class)) {
+ $this->app->make(RateLimiter::class)->forgetInstances();
+ }
+ }
+
/**
* Bootstrap the service provider.
*/
diff --git a/src/redis/composer.json b/src/redis/composer.json
index b16931170..e5da713f6 100644
--- a/src/redis/composer.json
+++ b/src/redis/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/redis",
"type": "library",
- "description": "The redis package for Hypervel.",
+ "description": "The Hypervel Redis package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/reverb/src/ReverbServiceProvider.php b/src/reverb/src/ReverbServiceProvider.php
index ab818db66..8db946171 100644
--- a/src/reverb/src/ReverbServiceProvider.php
+++ b/src/reverb/src/ReverbServiceProvider.php
@@ -5,6 +5,7 @@
namespace Hypervel\Reverb;
use Hypervel\Contracts\Bus\Dispatcher as BusDispatcher;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Redis\Factory as RedisFactory;
use Hypervel\Coordinator\Timer;
use Hypervel\Core\Events\AfterWorkerStart;
@@ -62,7 +63,7 @@
use Swoole\Table;
use Throwable;
-class ReverbServiceProvider extends ServiceProvider
+class ReverbServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register any application services.
@@ -166,6 +167,7 @@ protected function registerWebSocketServer(): void
'settings' => $this->resolveServerSettings($tls),
];
+ // Replay the master snapshot; replacement workers cannot change the bound server topology.
$config->set('server.servers', $servers);
}
@@ -246,6 +248,21 @@ public function boot(): void
$this->registerShutdownHandler();
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(ApplicationManager::class)) {
+ $this->app->make(ApplicationManager::class)->forgetDrivers();
+ }
+
+ $this->app->forgetInstance(WebhookBatchBuffer::class);
+ }
+
/**
* Register periodic tasks for connection cleanup and table monitoring.
*/
diff --git a/src/routing/composer.json b/src/routing/composer.json
index b1cd8a2e0..621e8a3a1 100644
--- a/src/routing/composer.json
+++ b/src/routing/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/routing",
"type": "library",
- "description": "The routing package for Hypervel.",
+ "description": "The Hypervel Routing package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/routing/src/Redirector.php b/src/routing/src/Redirector.php
index 21b145155..dc2a4a739 100755
--- a/src/routing/src/Redirector.php
+++ b/src/routing/src/Redirector.php
@@ -157,6 +157,9 @@ 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.
*/
public function setSession(SessionStore $session): void
{
diff --git a/src/routing/src/RoutingServiceProvider.php b/src/routing/src/RoutingServiceProvider.php
index a3a0a074b..bdc7f9684 100644
--- a/src/routing/src/RoutingServiceProvider.php
+++ b/src/routing/src/RoutingServiceProvider.php
@@ -5,8 +5,10 @@
namespace Hypervel\Routing;
use Hypervel\Contracts\Container\BindingResolutionException;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Routing\ResponseFactory as ResponseFactoryContract;
use Hypervel\Contracts\View\Factory as ViewFactoryContract;
+use Hypervel\Http\Request;
use Hypervel\Routing\Console\ControllerMakeCommand;
use Hypervel\Routing\Console\MiddlewareMakeCommand;
use Hypervel\Routing\Contracts\CallableDispatcher as CallableDispatcherContract;
@@ -17,7 +19,7 @@
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\Response;
-class RoutingServiceProvider extends ServiceProvider
+class RoutingServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -40,6 +42,26 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use changes shared URL generation state while
+ * concurrent coroutines may still be using the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if (! $this->app->resolved('url')) {
+ return;
+ }
+
+ $config = $this->app->make('config');
+ $url = $this->app->make('url');
+
+ $url->setRequest(Request::create($config->string('app.url')));
+ $url->setAssetRoot($config->get('app.asset_url'));
+ $url->forceHttps($config->boolean('app.force_https'));
+ }
+
/**
* Register the router instance.
*/
diff --git a/src/routing/src/UrlGenerator.php b/src/routing/src/UrlGenerator.php
index c7a217202..4d6740944 100755
--- a/src/routing/src/UrlGenerator.php
+++ b/src/routing/src/UrlGenerator.php
@@ -779,6 +779,17 @@ public function useAssetOrigin(?string $root): void
}
}
+ /**
+ * Set the fallback asset URL root.
+ *
+ * Boot-only. The root is stored on the worker-shared URL generator and
+ * affects subsequent requests without a coroutine-local asset origin.
+ */
+ public function setAssetRoot(?string $root): void
+ {
+ $this->assetRoot = $root;
+ }
+
/**
* Set a callback to be used to format the host of generated URLs.
*
@@ -834,9 +845,9 @@ public function getRequest(): Request
/**
* Set the current request instance.
*
- * Tests only. 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
+ * Boot or tests only. 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.
*/
public function setRequest(Request $request): void
diff --git a/src/saloon/src/SaloonServiceProvider.php b/src/saloon/src/SaloonServiceProvider.php
index c0283af5f..54f905082 100644
--- a/src/saloon/src/SaloonServiceProvider.php
+++ b/src/saloon/src/SaloonServiceProvider.php
@@ -7,6 +7,7 @@
use Hypervel\Contracts\Cache\Factory as CacheFactory;
use Hypervel\Contracts\Config\Repository as ConfigRepository;
use Hypervel\Contracts\Events\Dispatcher;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Http\Client\Factory as HttpFactory;
use Hypervel\RateLimiter\RateLimiter;
use Hypervel\Saloon\Console\Commands\ListCommand;
@@ -19,7 +20,7 @@
use Hypervel\Saloon\Http\Sender;
use Hypervel\Support\ServiceProvider;
-class SaloonServiceProvider extends ServiceProvider
+class SaloonServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the package services.
@@ -42,19 +43,42 @@ public function register(): void
/**
* Bootstrap the package services.
*/
- public function boot(HttpFactory $http, ConfigRepository $config): void
+ public function boot(HttpFactory $httpFactory, ConfigRepository $config): void
+ {
+ $this->registerHttpConnection($httpFactory, $config);
+ $this->registerConsoleResources();
+ }
+
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use replaces the shared connection preset and
+ * discards its transport handler, so subsequent requests rebuild warmed
+ * keep-alive, DNS, and TLS session state.
+ */
+ public function reloadConfiguration(): void
+ {
+ // boot() method-injects the factory, so it is already resolved here.
+ $this->registerHttpConnection(
+ $this->app->make(HttpFactory::class),
+ $this->app->make(ConfigRepository::class),
+ );
+ }
+
+ /**
+ * Register the configured HTTP connection.
+ */
+ protected function registerHttpConnection(HttpFactory $httpFactory, ConfigRepository $config): void
{
$connection = $config->string('saloon.connection.name');
$options = $config->array('saloon.connection.options');
RequestOptionValidator::validate($options, "HTTP connection [{$connection}]");
- $http->registerConnection(
+ $httpFactory->registerConnection(
$connection,
$options,
);
-
- $this->registerConsoleResources();
}
/**
diff --git a/src/sanctum/composer.json b/src/sanctum/composer.json
index b782a47ad..d79f5a775 100644
--- a/src/sanctum/composer.json
+++ b/src/sanctum/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/sanctum",
- "description": "The Sanctum package for Hypervel.",
+ "description": "The Hypervel Sanctum package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/scout/src/ScoutServiceProvider.php b/src/scout/src/ScoutServiceProvider.php
index 767366e0f..8da313689 100644
--- a/src/scout/src/ScoutServiceProvider.php
+++ b/src/scout/src/ScoutServiceProvider.php
@@ -11,6 +11,7 @@
use Algolia\AlgoliaSearch\Support\AlgoliaAgent;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\HandlerStack;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Telescope\TelescopeTag;
use Hypervel\Foundation\Application as HypervelApplication;
use Hypervel\Scout\Console\DeleteAllIndexesCommand;
@@ -25,7 +26,7 @@
use Meilisearch\Client as MeilisearchClient;
use Typesense\Client as TypesenseClient;
-class ScoutServiceProvider extends ServiceProvider
+class ScoutServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register Scout services.
@@ -55,6 +56,23 @@ public function boot(): void
}
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved(EngineManager::class)) {
+ $this->app->make(EngineManager::class)->forgetEngines();
+ }
+
+ foreach ([AlgoliaSearchClient::class, MeilisearchClient::class, TypesenseClient::class] as $client) {
+ $this->app->forgetInstance($client);
+ }
+ }
+
/**
* Configure Algolia's SDK-wide settings.
*/
diff --git a/src/sentry/composer.json b/src/sentry/composer.json
index 1e25c3922..85efd9c2d 100644
--- a/src/sentry/composer.json
+++ b/src/sentry/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/sentry",
"type": "library",
- "description": "The sentry component for Hypervel framework.",
+ "description": "Sentry error tracking and performance monitoring for Hypervel.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/sentry/src/SentryServiceProvider.php b/src/sentry/src/SentryServiceProvider.php
index 8f52b7cf8..5498072bf 100644
--- a/src/sentry/src/SentryServiceProvider.php
+++ b/src/sentry/src/SentryServiceProvider.php
@@ -4,13 +4,16 @@
namespace Hypervel\Sentry;
+use Hypervel\Config\Repository as ConfigRepository;
use Hypervel\Context\CoroutineContext;
use Hypervel\Contracts\Container\BindingResolutionException;
use Hypervel\Contracts\Events\Dispatcher;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Http\Kernel as HttpKernelInterface;
use Hypervel\Contracts\View\Engine;
use Hypervel\Contracts\View\View;
use Hypervel\Coroutine\Coroutine;
+use Hypervel\Foundation\Configuration\ConfigMutationTracker;
use Hypervel\Foundation\Console\AboutCommand;
use Hypervel\Http\Request;
use Hypervel\ObjectPool\PoolOptions;
@@ -41,6 +44,7 @@
use Psr\Log\LoggerInterface;
use RuntimeException;
use Sentry\ClientBuilder;
+use Sentry\ClientInterface;
use Sentry\Integration as SdkIntegration;
use Sentry\Logger\DebugFileLogger;
use Sentry\Logs\Logs;
@@ -50,7 +54,7 @@
use Sentry\State\Layer;
use Throwable;
-class SentryServiceProvider extends ServiceProvider
+class SentryServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Configuration options that are Hypervel-specific and should not be sent to the base PHP SDK.
@@ -132,6 +136,28 @@ public function register(): void
}
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if (! $this->app->resolved(HubInterface::class)) {
+ return;
+ }
+
+ $hub = $this->app->make(HubInterface::class);
+
+ if (! $hub instanceof Hub) {
+ return;
+ }
+
+ $hub->bindClient($this->createClient());
+ $this->app->forgetInstance(BacktraceHelper::class);
+ }
+
/**
* Configure and register the Sentry client with the container.
*/
@@ -188,84 +214,92 @@ protected function configureAndRegisterClient(): void
// HubInterface singleton — coroutine-scoped hub with full integration setup
$this->app->singleton(HubInterface::class, function () {
- /** @var ClientBuilder $clientBuilder */
- $clientBuilder = $this->app->make(ClientBuilder::class);
+ $hub = new Hub($this->createClient());
- $options = $clientBuilder->getOptions();
+ SentrySdk::setCurrentHub($hub);
- $userConfig = $this->getUserConfig();
+ return $hub;
+ });
- /** @var array|callable $userIntegrationOption */
- $userIntegrationOption = $userConfig['integrations'] ?? [];
+ $this->app->alias(HubInterface::class, static::$abstract);
- $userIntegrations = $this->resolveIntegrationsFromUserConfig(
- is_array($userIntegrationOption) ? $userIntegrationOption : [],
- );
+ $this->app->singleton(BacktraceHelper::class, function () {
+ $sentry = $this->app->make(HubInterface::class);
- $options->setIntegrations(static function (array $integrations) use ($options, $userIntegrations, $userIntegrationOption): array {
- if ($options->hasDefaultIntegrations()) {
- // Remove the default error and fatal exception listeners to let the framework handle those
- // through the exception handler and log channel integration
- $integrations = array_filter($integrations, static function (SdkIntegration\IntegrationInterface $integration): bool {
- if ($integration instanceof SdkIntegration\ErrorListenerIntegration) {
- return false;
- }
-
- if ($integration instanceof SdkIntegration\ExceptionListenerIntegration) {
- return false;
- }
-
- if ($integration instanceof SdkIntegration\FatalErrorListenerIntegration) {
- return false;
- }
-
- // Remove the default request integration so it can be re-added with
- // a Hypervel-specific request fetcher that reads from coroutine context.
- if ($integration instanceof SdkIntegration\RequestIntegration) {
- return false;
- }
-
- return true;
- });
+ $options = $sentry->getClient()->getOptions();
- $integrations[] = new SdkIntegration\RequestIntegration(
- new HypervelRequestFetcher
- );
- }
+ return new BacktraceHelper($options, new RepresentationSerializer($options));
+ });
+ }
- $integrations = array_merge(
- $integrations,
- [
- new Integration,
- new ContextIntegration,
- new ExceptionContextIntegration,
- ],
- $userIntegrations
- );
+ /**
+ * Create the configured Sentry client.
+ */
+ protected function createClient(): ClientInterface
+ {
+ /** @var ClientBuilder $clientBuilder */
+ $clientBuilder = $this->app->make(ClientBuilder::class);
- if (is_callable($userIntegrationOption)) {
- return $userIntegrationOption($integrations);
- }
+ $options = $clientBuilder->getOptions();
- return $integrations;
- });
+ $userConfig = $this->getUserConfig();
- $hub = new Hub($clientBuilder->getClient());
+ /** @var array|callable $userIntegrationOption */
+ $userIntegrationOption = $userConfig['integrations'] ?? [];
- SentrySdk::setCurrentHub($hub);
+ $userIntegrations = $this->resolveIntegrationsFromUserConfig(
+ is_array($userIntegrationOption) ? $userIntegrationOption : [],
+ );
- return $hub;
- });
+ $options->setIntegrations(static function (array $integrations) use ($options, $userIntegrations, $userIntegrationOption): array {
+ if ($options->hasDefaultIntegrations()) {
+ // Remove the default error and fatal exception listeners to let the framework handle those
+ // through the exception handler and log channel integration
+ $integrations = array_filter($integrations, static function (SdkIntegration\IntegrationInterface $integration): bool {
+ if ($integration instanceof SdkIntegration\ErrorListenerIntegration) {
+ return false;
+ }
- $this->app->alias(HubInterface::class, static::$abstract);
+ if ($integration instanceof SdkIntegration\ExceptionListenerIntegration) {
+ return false;
+ }
- $this->app->singleton(BacktraceHelper::class, function () {
- $sentry = $this->app->make(HubInterface::class);
+ if ($integration instanceof SdkIntegration\FatalErrorListenerIntegration) {
+ return false;
+ }
- $options = $sentry->getClient()->getOptions();
+ // Remove the default request integration so it can be re-added with
+ // a Hypervel-specific request fetcher that reads from coroutine context.
+ if ($integration instanceof SdkIntegration\RequestIntegration) {
+ return false;
+ }
- return new BacktraceHelper($options, new RepresentationSerializer($options));
+ return true;
+ });
+
+ $integrations[] = new SdkIntegration\RequestIntegration(
+ new HypervelRequestFetcher
+ );
+ }
+
+ $integrations = array_merge(
+ $integrations,
+ [
+ new Integration,
+ new ContextIntegration,
+ new ExceptionContextIntegration,
+ ],
+ $userIntegrations
+ );
+
+ if (is_callable($userIntegrationOption)) {
+ return $userIntegrationOption($integrations);
+ }
+
+ return $integrations;
});
+
+ return $clientBuilder->getClient();
}
/**
@@ -557,22 +591,28 @@ private function reportFeatureFailure(string $feature, string $phase, Throwable
*/
protected function registerLogChannels(): void
{
- $config = $this->app->make('config');
-
- $logChannels = $config->array('logging.channels', []);
-
- if (! array_key_exists('sentry', $logChannels)) {
- $config->set('logging.channels.sentry', [
- 'driver' => 'sentry',
- ]);
- }
+ $config = $this->app->make(ConfigRepository::class);
+
+ // Derived config can depend on the worker environment, so replay the operation after config reload rather than its master result.
+ $this->app->make(ConfigMutationTracker::class)->applyAndRecord(
+ $config,
+ static function (ConfigRepository $config): void {
+ $logChannels = $config->array('logging.channels', []);
+
+ if (! array_key_exists('sentry', $logChannels)) {
+ $config->set('logging.channels.sentry', [
+ 'driver' => 'sentry',
+ ]);
+ }
- if (! array_key_exists('sentry_logs', $logChannels)) {
- $config->set('logging.channels.sentry_logs', [
- 'driver' => 'sentry_logs',
- 'level' => $config->string('sentry.logs_channel_level', 'debug'),
- ]);
- }
+ if (! array_key_exists('sentry_logs', $logChannels)) {
+ $config->set('logging.channels.sentry_logs', [
+ 'driver' => 'sentry_logs',
+ 'level' => $config->string('sentry.logs_channel_level', 'debug'),
+ ]);
+ }
+ },
+ );
}
/**
@@ -677,8 +717,6 @@ protected function hasSpotlightEnabled(): bool
*/
protected function getUserConfig(): array
{
- $config = $this->app['config'][static::$abstract];
-
- return empty($config) ? [] : $config;
+ return $this->app->make('config')->array(static::$abstract);
}
}
diff --git a/src/server/src/Commands/ServerReloadCommand.php b/src/server/src/Commands/ServerReloadCommand.php
index 511b6ddef..d4e9ab853 100644
--- a/src/server/src/Commands/ServerReloadCommand.php
+++ b/src/server/src/Commands/ServerReloadCommand.php
@@ -5,9 +5,10 @@
namespace Hypervel\Server\Commands;
use Hypervel\Console\Command;
-use Hypervel\Contracts\Config\Repository;
use Hypervel\Contracts\Filesystem\FileNotFoundException;
-use Hypervel\Filesystem\Filesystem;
+use Hypervel\Server\Exceptions\InvalidArgumentException;
+use Hypervel\Server\Exceptions\ServerException;
+use Hypervel\Server\ServerReloader;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'server:reload')]
@@ -15,66 +16,28 @@ class ServerReloadCommand extends Command
{
protected ?string $signature = 'server:reload';
- protected string $description = 'Reload all workers gracefully.';
+ protected string $description = 'Reload the server event and task workers gracefully.';
public function __construct(
- protected Repository $config,
- protected Filesystem $filesystem
+ protected ServerReloader $reloader,
) {
parent::__construct();
}
public function handle(): int
{
- $file = $this->config->string('server.settings.pid_file');
- $hasTaskWorkers = $this->config->integer('server.settings.task_worker_num') > 0;
-
- try {
- $contents = $this->filesystem->get($file);
- } catch (FileNotFoundException) {
- $this->warn("Unable to read the server PID file [{$file}].");
-
- return self::FAILURE;
- }
-
- $pid = filter_var(trim($contents), FILTER_VALIDATE_INT, [
- 'options' => ['min_range' => 1],
- ]);
-
- if ($pid === false) {
- $this->error("The server PID file [{$file}] does not contain a valid process ID.");
-
- return self::FAILURE;
- }
-
$this->info('Reloading workers...');
- if (! $this->signalProcess($pid, SIGUSR1)) {
- $this->warn('Unable to reload workers.');
+ try {
+ $this->reloader->reload();
+ } catch (FileNotFoundException|InvalidArgumentException|ServerException $exception) {
+ $this->error($exception->getMessage());
return self::FAILURE;
}
- if ($hasTaskWorkers) {
- $this->info('Reloading task workers...');
-
- if (! $this->signalProcess($pid, SIGUSR2)) {
- $this->warn('Unable to reload task workers.');
-
- return self::FAILURE;
- }
- }
-
$this->info('Done.');
return self::SUCCESS;
}
-
- /**
- * Send a signal to the server process.
- */
- protected function signalProcess(int $pid, int $signal): bool
- {
- return posix_kill($pid, $signal);
- }
}
diff --git a/src/server/src/ServerReloader.php b/src/server/src/ServerReloader.php
new file mode 100644
index 000000000..f25786f60
--- /dev/null
+++ b/src/server/src/ServerReloader.php
@@ -0,0 +1,62 @@
+config->string('server.settings.pid_file');
+ $contents = $this->filesystem->get($pidFile);
+ $pid = filter_var(trim($contents), FILTER_VALIDATE_INT, [
+ 'options' => ['min_range' => 1],
+ ]);
+
+ if ($pid === false) {
+ throw new InvalidArgumentException(
+ "The server PID file [{$pidFile}] does not contain a valid process ID."
+ );
+ }
+
+ if (! $this->signalProcess($pid, SIGUSR1)) {
+ throw new ServerException('Unable to send [SIGUSR1] to reload event workers.');
+ }
+
+ if ($this->config->integer('server.settings.task_worker_num') > 0
+ && ! $this->signalProcess($pid, SIGUSR2)) {
+ throw new ServerException('Unable to send [SIGUSR2] to reload task workers.');
+ }
+ }
+
+ /**
+ * Send a signal to the server process.
+ */
+ protected function signalProcess(int $pid, int $signal): bool
+ {
+ return posix_kill($pid, $signal);
+ }
+}
diff --git a/src/session/composer.json b/src/session/composer.json
index be0433eda..d6e27ea43 100644
--- a/src/session/composer.json
+++ b/src/session/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/session",
"type": "library",
- "description": "The session package for Hypervel.",
+ "description": "The Hypervel Session package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/session/src/SessionServiceProvider.php b/src/session/src/SessionServiceProvider.php
index 119d86c98..78cc00977 100644
--- a/src/session/src/SessionServiceProvider.php
+++ b/src/session/src/SessionServiceProvider.php
@@ -4,9 +4,10 @@
namespace Hypervel\Session;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Support\ServiceProvider;
-class SessionServiceProvider extends ServiceProvider
+class SessionServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -21,6 +22,25 @@ public function register(): void
]);
}
+ /**
+ * Reload configuration-derived worker state.
+ *
+ * Boot-only. Request-time use replaces shared session state while
+ * concurrent coroutines may still hold the previous store.
+ */
+ public function reloadConfiguration(): void
+ {
+ if ($this->app->resolved('session')) {
+ $this->app->make('session')->forgetDrivers();
+ }
+
+ $this->app->forgetInstance('session.store');
+
+ if ($this->app->resolved('redirect')) {
+ $this->app->make('redirect')->setSession($this->app->make('session.store'));
+ }
+ }
+
/**
* Register the session manager instance.
*/
diff --git a/src/signal/composer.json b/src/signal/composer.json
index 97f8e9733..5d0544990 100644
--- a/src/signal/composer.json
+++ b/src/signal/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/signal",
"type": "library",
- "description": "The Hypervel Signal package for OS signal handling.",
+ "description": "Operating system signal handling for Hypervel.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/socialite/composer.json b/src/socialite/composer.json
index bcbff523c..7fbf7628c 100644
--- a/src/socialite/composer.json
+++ b/src/socialite/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/socialite",
"type": "library",
- "description": "Hypervel wrapper around OAuth 2 libraries.",
+ "description": "OAuth 2.0 and OpenID Connect authentication for Hypervel.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/socialite/src/SocialiteServiceProvider.php b/src/socialite/src/SocialiteServiceProvider.php
index c83a63d44..02130033b 100644
--- a/src/socialite/src/SocialiteServiceProvider.php
+++ b/src/socialite/src/SocialiteServiceProvider.php
@@ -4,10 +4,11 @@
namespace Hypervel\Socialite;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Socialite\Contracts\Factory;
use Hypervel\Support\ServiceProvider;
-class SocialiteServiceProvider extends ServiceProvider
+class SocialiteServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -16,4 +17,19 @@ public function register(): void
{
$this->app->alias(SocialiteManager::class, Factory::class);
}
+
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if (! $this->app->resolved(SocialiteManager::class)) {
+ return;
+ }
+
+ $this->app->make(SocialiteManager::class)->forgetDrivers();
+ }
}
diff --git a/src/support/composer.json b/src/support/composer.json
index fd13cf793..c404d29d5 100644
--- a/src/support/composer.json
+++ b/src/support/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/support",
- "description": "The Support package for Hypervel.",
+ "description": "The Hypervel Support package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/support/src/Facades/Blade.php b/src/support/src/Facades/Blade.php
index 9629d29bd..d03edfa45 100644
--- a/src/support/src/Facades/Blade.php
+++ b/src/support/src/Facades/Blade.php
@@ -37,6 +37,7 @@
* @method static string newComponentHash(string $component)
* @method static void precompiler(callable $precompiler)
* @method static \Hypervel\View\Compilers\BladeCompiler prepareStringsForCompilationUsing(callable $callback)
+ * @method static void reloadConfiguration(string $cachePath, string $basePath, bool $shouldCache, string $compiledExtension, bool $shouldCheckTimestamps)
* @method static string render(string $string, array $data = [], bool $deleteCachedView = false)
* @method static string renderComponent(\Hypervel\View\Component $component)
* @method static mixed sanitizeComponentAttribute(mixed $value)
diff --git a/src/support/src/Facades/Cache.php b/src/support/src/Facades/Cache.php
index c28cd1f57..141e925a1 100644
--- a/src/support/src/Facades/Cache.php
+++ b/src/support/src/Facades/Cache.php
@@ -12,6 +12,7 @@
* @method static \Hypervel\Contracts\Cache\Repository driver(\UnitEnum|string|null $driver = null)
* @method static \Hypervel\Cache\CacheManager extend(string $driver, \Closure $callback)
* @method static \Hypervel\Cache\CacheManager forgetDriver(\UnitEnum|array|string|null $name = null)
+ * @method static \Hypervel\Cache\CacheManager forgetDrivers()
* @method static string getDefaultDriver()
* @method static void handleUnserializableClassUsing(callable|null $callback)
* @method static \Hypervel\Contracts\Cache\Repository memo(\UnitEnum|string|null $driver = null)
diff --git a/src/support/src/Facades/Concurrency.php b/src/support/src/Facades/Concurrency.php
index 6a8ef7db2..8c3df3837 100644
--- a/src/support/src/Facades/Concurrency.php
+++ b/src/support/src/Facades/Concurrency.php
@@ -13,6 +13,7 @@
* @method static mixed driver(\UnitEnum|string|null $name = null)
* @method static \Hypervel\Concurrency\ConcurrencyManager extend(string $name, \Closure $callback)
* @method static \Hypervel\Concurrency\ConcurrencyManager forgetInstance(array|string|null $name = null)
+ * @method static \Hypervel\Concurrency\ConcurrencyManager forgetInstances()
* @method static string getDefaultInstance()
* @method static array getInstanceConfig(string $name)
* @method static mixed instance(string|null $name = null)
diff --git a/src/support/src/Facades/Cookie.php b/src/support/src/Facades/Cookie.php
index 176fa736a..e70d18751 100644
--- a/src/support/src/Facades/Cookie.php
+++ b/src/support/src/Facades/Cookie.php
@@ -4,6 +4,7 @@
namespace Hypervel\Support\Facades;
+use Hypervel\Contracts\Container\Container as ContainerContract;
use UnitEnum;
use function Hypervel\Support\enum_value;
@@ -36,8 +37,10 @@ class Cookie extends Facade
public static function has(UnitEnum|string $key): bool
{
$key = $key instanceof UnitEnum ? (string) enum_value($key) : $key;
+ /** @var ContainerContract $app */
+ $app = static::$app;
- return ! is_null(static::$app['request']->cookie($key));
+ return ! is_null($app->make('request')->cookie($key));
}
/**
@@ -48,8 +51,10 @@ public static function has(UnitEnum|string $key): bool
public static function get(UnitEnum|string|null $key = null, mixed $default = null): mixed
{
$key = $key instanceof UnitEnum ? (string) enum_value($key) : $key;
+ /** @var ContainerContract $app */
+ $app = static::$app;
- return static::$app['request']->cookie($key) ?? $default;
+ return $app->make('request')->cookie($key) ?? $default;
}
/**
diff --git a/src/support/src/Facades/Date.php b/src/support/src/Facades/Date.php
index e6ab0ec00..b7ad47a7a 100644
--- a/src/support/src/Facades/Date.php
+++ b/src/support/src/Facades/Date.php
@@ -125,7 +125,7 @@ protected static function getFacadeAccessor(): string
*/
protected static function resolveFacadeInstance(string $name): mixed
{
- if (! isset(static::$resolvedInstance[$name]) && ! isset(static::$app, static::$app[$name])) {
+ if (! isset(static::$resolvedInstance[$name]) && (static::$app === null || ! static::$app->bound($name))) {
$class = static::DEFAULT_FACADE;
static::swap(new $class);
diff --git a/src/support/src/Facades/Facade.php b/src/support/src/Facades/Facade.php
index 82bdf1523..68f3dc0b4 100644
--- a/src/support/src/Facades/Facade.php
+++ b/src/support/src/Facades/Facade.php
@@ -5,6 +5,7 @@
namespace Hypervel\Support\Facades;
use Closure;
+use Hypervel\Contracts\Container\Container as ContainerContract;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Support\Arr;
use Hypervel\Support\Benchmark;
@@ -23,7 +24,7 @@ abstract class Facade
/**
* The application instance being facaded.
*/
- protected static $app;
+ protected static ?ContainerContract $app = null;
/**
* The resolved object instances.
@@ -55,12 +56,14 @@ abstract class Facade
public static function resolved(Closure $callback): void
{
$accessor = static::getFacadeAccessor();
+ /** @var ContainerContract $app */
+ $app = static::$app;
- if (static::$app->resolved($accessor) === true) {
- $callback(static::getFacadeRoot(), static::$app);
+ if ($app->resolved($accessor) === true) {
+ $callback(static::getFacadeRoot(), $app);
}
- static::$app->afterResolving($accessor, function ($service, $app) use ($callback) {
+ $app->afterResolving($accessor, function ($service, $app) use ($callback) {
$callback($service, $app);
});
}
@@ -228,10 +231,10 @@ protected static function resolveFacadeInstance(string $name): mixed
if (static::$app) {
if (static::$cached) {
- return static::$resolvedInstance[$name] = static::$app[$name];
+ return static::$resolvedInstance[$name] = static::$app->make($name);
}
- return static::$app[$name];
+ return static::$app->make($name);
}
return null;
@@ -321,7 +324,7 @@ public static function defaultAliases(): Collection
/**
* Get the application instance behind the facade.
*/
- public static function getFacadeApplication()
+ public static function getFacadeApplication(): ?ContainerContract
{
return static::$app;
}
@@ -329,12 +332,10 @@ public static function getFacadeApplication()
/**
* Set the application instance.
*
- * Tests only. Replaces the worker-wide facade application reference;
+ * Boot or tests only. Replaces the worker-wide facade application reference;
* runtime use races across coroutines and breaks every facade lookup.
- *
- * @param mixed $app
*/
- public static function setFacadeApplication($app): void
+ 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 94013d9b5..d5241a14f 100644
--- a/src/support/src/Facades/Http.php
+++ b/src/support/src/Facades/Http.php
@@ -23,6 +23,7 @@
* @method static \Hypervel\Http\Client\RequestException failedRequest(mixed $body = null, int $status = 200, array $headers = [])
* @method static void flushMacros()
* @method static void flushState()
+ * @method static \Hypervel\Http\Client\Factory forgetConnectionHandlers()
* @method static array getConnectionConfig(string $name)
* @method static array getConnectionConfigs()
* @method static callable getConnectionHandler(string $name)
diff --git a/src/support/src/Facades/Jwt.php b/src/support/src/Facades/Jwt.php
index c3e391889..877b9c35d 100644
--- a/src/support/src/Facades/Jwt.php
+++ b/src/support/src/Facades/Jwt.php
@@ -17,6 +17,7 @@
* @method static bool hasBlacklistEnabled()
* @method static bool invalidate(string $token, bool $forceForever = false)
* @method static string refresh(string $token, bool $forceForever = false, bool $resetClaims = false, array $customClaims = [], int|false|null $ttl = false)
+ * @method static void reloadConfiguration()
* @method static \Hypervel\Jwt\JwtManager setContainer(\Hypervel\Contracts\Container\Container $container)
*
* @see \Hypervel\Jwt\JwtManager
diff --git a/src/support/src/Facades/Lang.php b/src/support/src/Facades/Lang.php
index 081a41af1..5115cbd22 100644
--- a/src/support/src/Facades/Lang.php
+++ b/src/support/src/Facades/Lang.php
@@ -15,6 +15,7 @@
* @method static void flushMacros()
* @method static void flushParsedKeys()
* @method static void flushState()
+ * @method static void forgetLoadedGroups()
* @method static array|string get(string $key, array $replace = [], string|null $locale = null, bool $fallback = true)
* @method static string getFallback()
* @method static \Hypervel\Contracts\Translation\Loader getLoader()
@@ -29,6 +30,7 @@
* @method static void macro(string $name, callable|object $macro)
* @method static void mixin(object $mixin, bool $replace = true)
* @method static array parseKey(string $key)
+ * @method static void setBaseLocale(string $locale)
* @method static void setFallback(string $fallback)
* @method static void setLoaded(array $loaded)
* @method static void setLocale(string $locale)
diff --git a/src/support/src/Facades/Log.php b/src/support/src/Facades/Log.php
index 7eb872062..f6991478c 100644
--- a/src/support/src/Facades/Log.php
+++ b/src/support/src/Facades/Log.php
@@ -16,6 +16,7 @@
* @method static \Hypervel\Log\LogManager extend(string $driver, \Closure $callback)
* @method static \Hypervel\Log\LogManager flushSharedContext()
* @method static void forgetChannel(string|null $driver = null)
+ * @method static \Hypervel\Log\LogManager forgetChannels()
* @method static array getChannels()
* @method static string|null getDefaultDriver()
* @method static void info(\Hypervel\Contracts\Support\Arrayable|\Hypervel\Contracts\Support\Jsonable|\Stringable|array|string $message, mixed[] $context = [])
diff --git a/src/support/src/Facades/Password.php b/src/support/src/Facades/Password.php
index 923d91c2a..440c6cdee 100755
--- a/src/support/src/Facades/Password.php
+++ b/src/support/src/Facades/Password.php
@@ -8,6 +8,7 @@
/**
* @method static \Hypervel\Contracts\Auth\PasswordBroker broker(\UnitEnum|string|null $name = null)
+ * @method static \Hypervel\Auth\Passwords\PasswordBrokerManager forgetBrokers()
* @method static string getDefaultDriver()
* @method static void refreshEventDispatcher(\Hypervel\Contracts\Events\Dispatcher $events)
* @method static string|null resolveBrokerNameForGuard(\UnitEnum|string $guard)
diff --git a/src/support/src/Facades/Queue.php b/src/support/src/Facades/Queue.php
index de2d79813..2b753434e 100644
--- a/src/support/src/Facades/Queue.php
+++ b/src/support/src/Facades/Queue.php
@@ -4,6 +4,7 @@
namespace Hypervel\Support\Facades;
+use Hypervel\Contracts\Container\Container as ContainerContract;
use Hypervel\Queue\Worker;
use Hypervel\Support\Testing\Fakes\QueueFake;
@@ -17,6 +18,7 @@
* @method static void exceptionOccurred(mixed $callback)
* @method static void extend(string $driver, \Closure $resolver)
* @method static void failing(mixed $callback)
+ * @method static \Hypervel\Queue\QueueManager forgetConnections()
* @method static \Hypervel\Contracts\Container\Container getApplication()
* @method static string getDefaultDriver()
* @method static string getName(string|null $connection = null)
@@ -118,9 +120,11 @@ public static function fake(array|string $jobsToFake = []): QueueFake
$actualQueueManager = static::isFake()
? tap(static::getFacadeRoot(), fn ($fake) => $fake->releaseUniqueJobLocks())->queue
: static::getFacadeRoot();
+ /** @var ContainerContract $app */
+ $app = static::getFacadeApplication();
return tap(new QueueFake(
- static::getFacadeApplication(),
+ $app,
$jobsToFake,
$actualQueueManager
), function ($fake) {
diff --git a/src/support/src/Facades/RateLimiter.php b/src/support/src/Facades/RateLimiter.php
index bff15c34a..56dc257fa 100644
--- a/src/support/src/Facades/RateLimiter.php
+++ b/src/support/src/Facades/RateLimiter.php
@@ -8,6 +8,7 @@
* @method static \Hypervel\RateLimiter\RateLimiter extend(string $name, \Closure $callback)
* @method static \Hypervel\RateLimiter\RateLimiter for(\UnitEnum|string $name, \Closure $callback, \UnitEnum|string|null $store = null)
* @method static \Hypervel\RateLimiter\RateLimiter forgetInstance(array|string|null $name = null)
+ * @method static \Hypervel\RateLimiter\RateLimiter forgetInstances()
* @method static string getDefaultInstance()
* @method static array getInstanceConfig(string $name)
* @method static mixed instance(string|null $name = null)
diff --git a/src/support/src/Facades/Schema.php b/src/support/src/Facades/Schema.php
index 993c9751e..fd9b3e5f6 100644
--- a/src/support/src/Facades/Schema.php
+++ b/src/support/src/Facades/Schema.php
@@ -4,6 +4,9 @@
namespace Hypervel\Support\Facades;
+use Hypervel\Contracts\Container\Container as ContainerContract;
+use Hypervel\Database\Schema\Builder;
+
/**
* @method static void blueprintResolver(\Closure $resolver)
* @method static void create(string $table, \Closure $callback)
@@ -72,9 +75,12 @@ class Schema extends Facade
/**
* Get a schema builder instance for a connection.
*/
- public static function connection(?string $name = null): \Hypervel\Database\Schema\Builder
+ public static function connection(?string $name = null): Builder
{
- return static::$app['db']->connection($name)->getSchemaBuilder();
+ /** @var ContainerContract $app */
+ $app = static::$app;
+
+ return $app->make('db')->connection($name)->getSchemaBuilder();
}
/**
diff --git a/src/support/src/Facades/Storage.php b/src/support/src/Facades/Storage.php
index e6715a40e..ed1385566 100644
--- a/src/support/src/Facades/Storage.php
+++ b/src/support/src/Facades/Storage.php
@@ -4,6 +4,7 @@
namespace Hypervel\Support\Facades;
+use Hypervel\Contracts\Container\Container as ContainerContract;
use Hypervel\Filesystem\Filesystem;
use Hypervel\Filesystem\FilesystemAdapter;
use UnitEnum;
@@ -24,6 +25,7 @@
* @method static \Hypervel\Filesystem\FilesystemManager extend(string $driver, \Closure $callback, bool $poolable = false)
* @method static void flushState()
* @method static \Hypervel\Filesystem\FilesystemManager forgetDisk(array|string $disk)
+ * @method static \Hypervel\Filesystem\FilesystemManager forgetDisks()
* @method static string getDefaultDriver()
* @method static array getPoolables()
* @method static \Closure|null getReleaseCallback(string $driver)
@@ -115,9 +117,11 @@ public static function fake(UnitEnum|string|null $disk = null, array $config = [
if ($disk instanceof UnitEnum) {
$disk = (string) enum_value($disk);
}
+ /** @var ContainerContract $app */
+ $app = static::$app;
$disk = $disk === null || $disk === ''
- ? static::$app['config']->get('filesystems.default')
+ ? $app->make('config')->string('filesystems.default')
: $disk;
$root = self::getRootPath($disk);
@@ -156,9 +160,11 @@ public static function persistentFake(UnitEnum|string|null $disk = null, array $
if ($disk instanceof UnitEnum) {
$disk = (string) enum_value($disk);
}
+ /** @var ContainerContract $app */
+ $app = static::$app;
$disk = $disk === null || $disk === ''
- ? static::$app['config']->get('filesystems.default')
+ ? $app->make('config')->string('filesystems.default')
: $disk;
static::set($disk, $fake = static::createLocalDriver(
@@ -181,7 +187,9 @@ protected static function getRootPath(string $disk): string
*/
protected static function buildDiskConfiguration(string $disk, array $config, string $root): array
{
- $originalConfig = static::$app['config']["filesystems.disks.{$disk}"] ?? [];
+ /** @var ContainerContract $app */
+ $app = static::$app;
+ $originalConfig = $app->make('config')->get("filesystems.disks.{$disk}") ?? [];
return array_merge(
['throw' => $originalConfig['throw'] ?? false],
diff --git a/src/support/src/Facades/URL.php b/src/support/src/Facades/URL.php
index cc074ded8..b5deb64b6 100644
--- a/src/support/src/Facades/URL.php
+++ b/src/support/src/Facades/URL.php
@@ -42,6 +42,7 @@
* @method static string route(\BackedEnum|string $name, mixed $parameters = [], bool $absolute = true)
* @method static string secure(string $path, mixed $parameters = [])
* @method static string secureAsset(string $path)
+ * @method static void setAssetRoot(string|null $root)
* @method static \Hypervel\Routing\UrlGenerator setKeyResolver(callable $keyResolver)
* @method static void setRequest(\Hypervel\Http\Request $request)
* @method static \Hypervel\Routing\UrlGenerator setRootControllerNamespace(string $rootNamespace)
diff --git a/src/support/src/MultipleInstanceManager.php b/src/support/src/MultipleInstanceManager.php
index 2f2f460a5..5c7fb56c4 100644
--- a/src/support/src/MultipleInstanceManager.php
+++ b/src/support/src/MultipleInstanceManager.php
@@ -147,6 +147,19 @@ public function forgetInstance(array|string|null $name = null): static
return $this;
}
+ /**
+ * Forget all resolved instances.
+ *
+ * Boot or tests only. Mutates the singleton's instance cache; concurrent
+ * coroutines may already hold instances that next resolution will replace.
+ */
+ public function forgetInstances(): static
+ {
+ $this->instances = [];
+
+ return $this;
+ }
+
/**
* Disconnect the given instance and remove from local cache.
*
diff --git a/src/support/src/ServiceProvider.php b/src/support/src/ServiceProvider.php
index 1307dba65..c4a89e748 100644
--- a/src/support/src/ServiceProvider.php
+++ b/src/support/src/ServiceProvider.php
@@ -239,9 +239,10 @@ protected function loadRoutesFrom(string $path): void
protected function loadViewsFrom(array|string $path, string $namespace): void
{
$this->callAfterResolving(ViewFactoryContract::class, function ($view) use ($path, $namespace) {
- if (isset($this->app->config['view']['paths'])
- && is_array($this->app->config['view']['paths'])) {
- foreach ($this->app->config['view']['paths'] as $viewPath) {
+ $config = $this->app->make('config');
+
+ if (is_array($viewPaths = $config->get('view.paths'))) {
+ foreach ($viewPaths as $viewPath) {
if (is_dir($appPath = $viewPath . '/vendor/' . $namespace)) {
$view->addNamespace($namespace, $appPath);
}
diff --git a/src/support/src/Testing/Fakes/MailFake.php b/src/support/src/Testing/Fakes/MailFake.php
index 2dd90dca5..9b88061ef 100644
--- a/src/support/src/Testing/Fakes/MailFake.php
+++ b/src/support/src/Testing/Fakes/MailFake.php
@@ -575,11 +575,17 @@ protected function pullCurrentMailer(): string
/**
* Forget all of the resolved mailer instances.
+ *
+ * Boot or tests only. This is cache-only: pooled transports on the wrapped
+ * manager remain shared resources until purged or reclaimed by their idle TTL.
*/
public function forgetMailers(): static
{
$this->currentMailer = null;
+ // Calls not handled by the fake are forwarded to this manager.
+ $this->manager->forgetMailers();
+
return $this;
}
diff --git a/src/support/src/Testing/Fakes/NotificationFake.php b/src/support/src/Testing/Fakes/NotificationFake.php
index 60d33055b..b8fa5a8e1 100644
--- a/src/support/src/Testing/Fakes/NotificationFake.php
+++ b/src/support/src/Testing/Fakes/NotificationFake.php
@@ -300,6 +300,16 @@ public function channel(UnitEnum|string|null $name = null): mixed
return null;
}
+ /**
+ * Forget all resolved notification drivers.
+ */
+ public function forgetDrivers(): static
+ {
+ // Worker refresh invokes the manager reset through its canonical key;
+ // this fake has no drivers to clear.
+ return $this;
+ }
+
/**
* Set the locale of notifications.
*/
diff --git a/src/support/src/Testing/Fakes/QueueFake.php b/src/support/src/Testing/Fakes/QueueFake.php
index 6fe17214e..5a622fd88 100644
--- a/src/support/src/Testing/Fakes/QueueFake.php
+++ b/src/support/src/Testing/Fakes/QueueFake.php
@@ -379,6 +379,24 @@ public function connection(mixed $value = null): Queue
return $this;
}
+ /**
+ * Forget all resolved queue connections.
+ *
+ * Boot or tests only. Mutates the fake and wrapped manager connection caches;
+ * concurrent coroutines may already hold connections that will be replaced.
+ */
+ public function forgetConnections(): static
+ {
+ parent::forgetConnections();
+
+ // Jobs excluded from the fake pass through to this manager.
+ if ($this->queue instanceof QueueManager) {
+ $this->queue->forgetConnections();
+ }
+
+ return $this;
+ }
+
/**
* Get the size of the queue.
*/
diff --git a/src/telescope/composer.json b/src/telescope/composer.json
index a355f5e23..a948c7ec7 100644
--- a/src/telescope/composer.json
+++ b/src/telescope/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/telescope",
- "description": "The telescope package for Hypervel.",
+ "description": "The Hypervel Telescope package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/telescope/src/Storage/DatabaseEntriesRepository.php b/src/telescope/src/Storage/DatabaseEntriesRepository.php
index 091696ff0..4e371670a 100644
--- a/src/telescope/src/Storage/DatabaseEntriesRepository.php
+++ b/src/telescope/src/Storage/DatabaseEntriesRepository.php
@@ -24,6 +24,11 @@
class DatabaseEntriesRepository implements EntriesRepository, ClearableRepository, PrunableRepository, TerminableRepository
{
+ /**
+ * The default number of entries inserted at once.
+ */
+ protected const int DEFAULT_CHUNK_SIZE = 1000;
+
/**
* Context key for the per-request monitored tags cache.
*/
@@ -37,18 +42,37 @@ class DatabaseEntriesRepository implements EntriesRepository, ClearableRepositor
/**
* The number of entries that will be inserted at once into the database.
*/
- protected int $chunkSize = 1000;
+ protected int $chunkSize = self::DEFAULT_CHUNK_SIZE;
/**
* Create a new database repository.
*/
public function __construct(string $connection, ?int $chunkSize = null)
+ {
+ $this->setConnection($connection);
+ $this->setChunkSize($chunkSize);
+ }
+
+ /**
+ * Set the database connection name.
+ *
+ * Boot-only. Request-time use changes shared worker configuration while
+ * concurrent coroutines may still be using the previous connection.
+ */
+ public function setConnection(string $connection): void
{
$this->connection = $connection;
+ }
- if ($chunkSize) {
- $this->chunkSize = $chunkSize;
- }
+ /**
+ * Set the database insertion chunk size.
+ *
+ * Boot-only. Request-time use changes shared worker configuration while
+ * concurrent coroutines may still be using the previous chunk size.
+ */
+ public function setChunkSize(?int $chunkSize): void
+ {
+ $this->chunkSize = $chunkSize ?: self::DEFAULT_CHUNK_SIZE;
}
/**
diff --git a/src/telescope/src/TelescopeServiceProvider.php b/src/telescope/src/TelescopeServiceProvider.php
index 8ad546c3e..871a617a8 100644
--- a/src/telescope/src/TelescopeServiceProvider.php
+++ b/src/telescope/src/TelescopeServiceProvider.php
@@ -5,7 +5,9 @@
namespace Hypervel\Telescope;
use Hypervel\Context\CoroutineContext;
+use Hypervel\Contracts\Config\Repository as ConfigRepository;
use Hypervel\Contracts\Events\Dispatcher;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Coroutine\Coroutine;
use Hypervel\Support\Facades\Route;
use Hypervel\Support\ServiceProvider;
@@ -19,7 +21,7 @@
use Hypervel\Telescope\Watchers\ClientRequestWatcher;
use Hypervel\Telescope\Watchers\RedisWatcher;
-class TelescopeServiceProvider extends ServiceProvider
+class TelescopeServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Bootstrap any package services.
@@ -133,6 +135,30 @@ public function register(): void
$this->registerGuzzleHttpClientAspect();
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ $config = $this->app->make(ConfigRepository::class);
+
+ foreach ([EntriesRepository::class, ClearableRepository::class, PrunableRepository::class] as $abstract) {
+ if (! $this->app->resolved($abstract)) {
+ continue;
+ }
+
+ $repository = $this->app->make($abstract);
+
+ if ($repository instanceof DatabaseEntriesRepository) {
+ $repository->setConnection($config->string('telescope.storage.database.connection'));
+ $repository->setChunkSize($config->integer('telescope.storage.database.chunk'));
+ }
+ }
+ }
+
/**
* Register the Redis events if the watcher is enabled.
*/
@@ -196,6 +222,8 @@ protected function registerStorageDriver(): void
*/
protected function registerDatabaseDriver(): void
{
+ $config = $this->app->make(ConfigRepository::class);
+
$this->app->singleton(
EntriesRepository::class,
DatabaseEntriesRepository::class
@@ -213,11 +241,11 @@ protected function registerDatabaseDriver(): void
$this->app->when(DatabaseEntriesRepository::class)
->needs('$connection')
- ->give(fn () => config('telescope.storage.database.connection'));
+ ->give(fn () => $config->string('telescope.storage.database.connection'));
$this->app->when(DatabaseEntriesRepository::class)
->needs('$chunkSize')
- ->give(fn () => config('telescope.storage.database.chunk'));
+ ->give(fn () => $config->integer('telescope.storage.database.chunk'));
}
/**
diff --git a/src/testbench/composer.json b/src/testbench/composer.json
index 68c2afdf6..42665a03a 100644
--- a/src/testbench/composer.json
+++ b/src/testbench/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/testbench",
- "description": "The testbench package for Hypervel.",
+ "description": "Application testing tools for Hypervel packages.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/testbench/src/Attributes/UsesVendor.php b/src/testbench/src/Attributes/UsesVendor.php
index f033d4c32..82a688c03 100644
--- a/src/testbench/src/Attributes/UsesVendor.php
+++ b/src/testbench/src/Attributes/UsesVendor.php
@@ -24,7 +24,7 @@ public function beforeEach(ApplicationContract $app): void
(new CreateVendorSymlink(package_path('vendor')))->handle($hypervel);
- $this->vendorSymlinkCreated = $hypervel['TESTBENCH_VENDOR_SYMLINK'] ?? false;
+ $this->vendorSymlinkCreated = $hypervel->make('TESTBENCH_VENDOR_SYMLINK');
}
public function afterEach(ApplicationContract $app): void
diff --git a/src/testbench/src/Concerns/HandlesDatabases.php b/src/testbench/src/Concerns/HandlesDatabases.php
index 3659d065d..945a4f19c 100644
--- a/src/testbench/src/Concerns/HandlesDatabases.php
+++ b/src/testbench/src/Concerns/HandlesDatabases.php
@@ -33,7 +33,7 @@ protected function setUpDatabaseRequirements(Closure $callback): void
attribute: fn () => $this->parseTestMethodAttributes($app, RequiresDatabase::class),
);
- $app['events']->listen(DatabaseRefreshed::class, function () {
+ $app->make('events')->listen(DatabaseRefreshed::class, function () {
$this->defineDatabaseMigrationsAfterDatabaseRefreshed();
});
diff --git a/src/testbench/src/Concerns/InteractsWithPublishedFiles.php b/src/testbench/src/Concerns/InteractsWithPublishedFiles.php
index 4ad86d982..987e37344 100644
--- a/src/testbench/src/Concerns/InteractsWithPublishedFiles.php
+++ b/src/testbench/src/Concerns/InteractsWithPublishedFiles.php
@@ -64,7 +64,7 @@ protected function tearDownInteractsWithPublishedFiles(): void
protected function cacheExistingMigrationsFiles(): void
{
$this->cachedExistingMigrationsFiles ??= (new Collection(
- $this->app['files']->files($this->app->databasePath('migrations'))
+ $this->app->make('files')->files($this->app->databasePath('migrations'))
))->map($this->publishedFilePath(...))
->filter(static fn (string $file) => str_ends_with($file, '.php'))
->all();
@@ -79,7 +79,7 @@ protected function assertFileContains(array $contains, string $file, string $mes
{
$this->assertFilenameExists($file);
- $haystack = $this->app['files']->get(
+ $haystack = $this->app->make('files')->get(
$this->app->basePath($file)
);
@@ -97,7 +97,7 @@ protected function assertFileDoesNotContains(array $contains, string $file, stri
{
$this->assertFilenameExists($file);
- $haystack = $this->app['files']->get(
+ $haystack = $this->app->make('files')->get(
$this->app->basePath($file)
);
@@ -127,7 +127,7 @@ protected function assertMigrationFileContains(array $contains, string $file, st
$this->assertTrue(! is_null($migrationFile), "Assert migration file {$file} does exist");
- $haystack = $this->app['files']->get($migrationFile);
+ $haystack = $this->app->make('files')->get($migrationFile);
foreach ($contains as $needle) {
$this->assertStringContainsString($needle, $haystack, $message);
@@ -145,7 +145,7 @@ protected function assertMigrationFileDoesNotContains(array $contains, string $f
$this->assertTrue(! is_null($migrationFile), "Assert migration file {$file} does exist");
- $haystack = $this->app['files']->get($migrationFile);
+ $haystack = $this->app->make('files')->get($migrationFile);
foreach ($contains as $needle) {
$this->assertStringNotContainsString($needle, $haystack, $message);
@@ -169,7 +169,7 @@ protected function assertFilenameExists(string $file): void
{
$appFile = $this->app->basePath($file);
- $this->assertTrue($this->app['files']->exists($appFile), "Assert file {$file} does exist");
+ $this->assertTrue($this->app->make('files')->exists($appFile), "Assert file {$file} does exist");
}
/**
@@ -179,7 +179,7 @@ protected function assertFilenameDoesNotExists(string $file): void
{
$appFile = $this->app->basePath($file);
- $this->assertTrue(! $this->app['files']->exists($appFile), "Assert file {$file} doesn't exist");
+ $this->assertTrue(! $this->app->make('files')->exists($appFile), "Assert file {$file} doesn't exist");
}
/**
@@ -256,7 +256,7 @@ protected function findFirstPublishedMigrationFile(string $filename, ?string $di
? $this->app->basePath($directory)
: $this->app->databasePath('migrations');
- return $this->app['files']->glob(join_paths($migrationPath, "*{$filename}"))[0] ?? null;
+ return $this->app->make('files')->glob(join_paths($migrationPath, "*{$filename}"))[0] ?? null;
}
/**
diff --git a/src/testing/composer.json b/src/testing/composer.json
index ddedfd055..71fa0d6a8 100644
--- a/src/testing/composer.json
+++ b/src/testing/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/testing",
"type": "library",
- "description": "The testing package for Hypervel.",
+ "description": "The Hypervel Testing package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/testing/src/Concerns/TestCaches.php b/src/testing/src/Concerns/TestCaches.php
index b907c2af3..a0f4cdc3b 100644
--- a/src/testing/src/Concerns/TestCaches.php
+++ b/src/testing/src/Concerns/TestCaches.php
@@ -29,7 +29,7 @@ protected function parallelSafeCachePrefix(): string
{
$token = ParallelTesting::token();
$suffix = "test_{$token}_";
- $prefix = $this->app->make('config')->string('cache.prefix', '');
+ $prefix = $this->app->make('config')->string('cache.prefix');
return str_ends_with($prefix, $suffix)
? $prefix
@@ -41,6 +41,6 @@ protected function parallelSafeCachePrefix(): string
*/
protected function switchToCachePrefix(string $prefix): void
{
- $this->app['config']->set('cache.prefix', $prefix);
+ $this->app->make('config')->set('cache.prefix', $prefix);
}
}
diff --git a/src/testing/src/Concerns/TestViews.php b/src/testing/src/Concerns/TestViews.php
index 3787f1f38..6506458f3 100644
--- a/src/testing/src/Concerns/TestViews.php
+++ b/src/testing/src/Concerns/TestViews.php
@@ -38,7 +38,7 @@ protected function bootTestViews(): void
*/
protected function parallelSafeCompiledViewPath(): ?string
{
- $path = $this->app->make('config')->string('view.compiled', '');
+ $path = $this->app->make('config')->string('view.compiled');
if (! $path) {
return null;
@@ -57,10 +57,10 @@ protected function parallelSafeCompiledViewPath(): ?string
*/
protected function switchToCompiledViewPath(string $path): void
{
- $this->app['config']->set('view.compiled', $path);
+ $this->app->make('config')->set('view.compiled', $path);
if ($this->app->resolved('blade.compiler')) {
- $compiler = $this->app['blade.compiler'];
+ $compiler = $this->app->make('blade.compiler');
(function () use ($path) {
$this->cachePath = $path; /* @phpstan-ignore property.notFound */
diff --git a/src/testing/src/PHPUnit/AfterEachTestSubscriber.php b/src/testing/src/PHPUnit/AfterEachTestSubscriber.php
index bdbee7f28..dabb68c32 100644
--- a/src/testing/src/PHPUnit/AfterEachTestSubscriber.php
+++ b/src/testing/src/PHPUnit/AfterEachTestSubscriber.php
@@ -196,7 +196,6 @@ protected function flushFrameworkState(): void
\Hypervel\Foundation\Console\VendorPublishCommand::flushState();
\Hypervel\Foundation\DevCommands::flushState();
\Hypervel\Foundation\Events\DiscoverEvents::flushState();
- \Hypervel\Foundation\Exceptions\Renderer\Frame::flushState();
\Hypervel\Foundation\Http\FormRequest::flushState();
\Hypervel\Foundation\Http\HtmlDumper::flushState();
\Hypervel\Foundation\Http\Middleware\ConvertEmptyStringsToNull::flushState();
diff --git a/src/testing/src/PendingCommand.php b/src/testing/src/PendingCommand.php
index 0cadad5c4..9f3d47793 100644
--- a/src/testing/src/PendingCommand.php
+++ b/src/testing/src/PendingCommand.php
@@ -382,7 +382,7 @@ public function run(): int
} finally {
$this->flushExpectations();
- $this->app->offsetUnset(OutputStyle::class);
+ $this->app->forgetInstance(OutputStyle::class);
}
}
@@ -475,7 +475,7 @@ protected function mockConsoleOutput()
});
}
- $this->app->bind(OutputStyle::class, fn () => $mock);
+ $this->app->instance(OutputStyle::class, $mock);
return $mock;
}
diff --git a/src/translation/README.md b/src/translation/README.md
index 72ad27808..f83b84f49 100644
--- a/src/translation/README.md
+++ b/src/translation/README.md
@@ -7,8 +7,7 @@ Documentation: https://hypervel.org/docs/localization
## Differences From Laravel
-- `Translator::setLocale()` changes the locale only for the current coroutine and does not affect other concurrent requests in the worker.
-- `Translator::setFallback()` changes the fallback shared by the worker and is intended for application boot.
+- `Translator::setLocale()` changes the locale only for the current coroutine and does not affect other concurrent requests in the worker. Use the boot-only `setBaseLocale()` method to change the worker's default locale. The `setFallback()` method likewise changes the fallback shared by the worker.
- `Translator` rejects JSON translation files whose top-level values are not strings or arrays, naming the file and key. A `null` value is allowed and means the key is untranslated.
Ported from: https://github.com/laravel/framework
diff --git a/src/translation/composer.json b/src/translation/composer.json
index c0f5d422d..207f980c7 100644
--- a/src/translation/composer.json
+++ b/src/translation/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/translation",
"type": "library",
- "description": "The translation package for Hypervel.",
+ "description": "The Hypervel Translation package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/translation/src/FileLoader.php b/src/translation/src/FileLoader.php
index 42319151e..4a07dafb7 100644
--- a/src/translation/src/FileLoader.php
+++ b/src/translation/src/FileLoader.php
@@ -45,7 +45,7 @@ public function __construct(
*/
public function load(string $locale, string $group, ?string $namespace = null): array
{
- // Mirrors the eager check in Translator::setLocale(); keep both predicates identical.
+ // Mirrors Translator::assertValidLocale(); keep both predicates identical.
if (Str::contains($locale, ['/', '\\']) || $locale === '.' || $locale === '..') {
throw new InvalidArgumentException('Invalid characters present in locale.');
}
diff --git a/src/translation/src/TranslationServiceProvider.php b/src/translation/src/TranslationServiceProvider.php
index fc7772e76..5984e1a50 100644
--- a/src/translation/src/TranslationServiceProvider.php
+++ b/src/translation/src/TranslationServiceProvider.php
@@ -5,11 +5,12 @@
namespace Hypervel\Translation;
use Hypervel\Contracts\Config\Repository as ConfigRepository;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Contracts\Translation\Loader;
use Hypervel\Filesystem\Filesystem;
use Hypervel\Support\ServiceProvider;
-class TranslationServiceProvider extends ServiceProvider
+class TranslationServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -33,6 +34,31 @@ public function register(): void
});
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ if (! $this->app->resolved('translator')) {
+ return;
+ }
+
+ $translator = $this->app->make('translator');
+
+ if (! $translator instanceof Translator) {
+ return;
+ }
+
+ $config = $this->app->make(ConfigRepository::class);
+
+ $translator->setBaseLocale($config->string('app.locale'));
+ $translator->setFallback($config->string('app.fallback_locale'));
+ $translator->forgetLoadedGroups();
+ }
+
/**
* Register the translation line loader.
*/
diff --git a/src/translation/src/Translator.php b/src/translation/src/Translator.php
index 2af8e10ab..03179578d 100644
--- a/src/translation/src/Translator.php
+++ b/src/translation/src/Translator.php
@@ -38,11 +38,23 @@ class Translator extends NamespacedItemResolver implements TranslatorContract
*/
protected string $fallback = '';
+ /**
+ * The base locale used by the translator.
+ */
+ protected string $locale;
+
/**
* The array of loaded translation groups.
*/
protected array $loaded = [];
+ /**
+ * The translation lines registered at boot.
+ *
+ * @var array>>>
+ */
+ protected array $registeredLines = [];
+
/**
* The message selector.
*/
@@ -77,9 +89,9 @@ class Translator extends NamespacedItemResolver implements TranslatorContract
*/
public function __construct(
protected Loader $loader,
- protected string $locale
+ string $locale
) {
- $this->setLocale($locale);
+ $this->setBaseLocale($locale);
}
/**
@@ -318,7 +330,14 @@ public function addLines(array $lines, string $locale, string $namespace = '*'):
foreach ($lines as $key => $value) {
[$group, $item] = explode('.', $key, 2);
- Arr::set($this->loaded, "{$namespace}.{$group}.{$locale}.{$item}", $value);
+ $this->registeredLines[$namespace][$group][$locale][] = [
+ 'item' => $item,
+ 'value' => $value,
+ ];
+
+ if ($this->isLoaded($namespace, $group, $locale)) {
+ Arr::set($this->loaded, "{$namespace}.{$group}.{$locale}.{$item}", $value);
+ }
}
}
@@ -336,6 +355,10 @@ public function load(string $namespace, string $group, string $locale): void
// lines that have already been loaded so that we can easily access them.
$lines = $this->loader->load($locale, $group, $namespace);
+ foreach ($this->registeredLines[$namespace][$group][$locale] ?? [] as $registeredLine) {
+ Arr::set($lines, $registeredLine['item'], $registeredLine['value']);
+ }
+
$this->loaded[$namespace][$group][$locale] = $lines;
}
@@ -529,13 +552,38 @@ public function getLocale(): string
* @throws InvalidArgumentException
*/
public function setLocale(string $locale): void
+ {
+ $this->assertValidLocale($locale);
+
+ CoroutineContext::set(self::LOCALE_CONTEXT_KEY, $locale);
+ }
+
+ /**
+ * 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.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function setBaseLocale(string $locale): void
+ {
+ $this->assertValidLocale($locale);
+
+ $this->locale = $locale;
+ }
+
+ /**
+ * Ensure the locale is safe to use as part of a translation path.
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function assertValidLocale(string $locale): void
{
// Mirrors the trust-boundary check in FileLoader::load(); keep both predicates identical.
if (Str::contains($locale, ['/', '\\']) || $locale === '.' || $locale === '..') {
throw new InvalidArgumentException('Invalid characters present in locale.');
}
-
- CoroutineContext::set(self::LOCALE_CONTEXT_KEY, $locale);
}
/**
@@ -555,9 +603,23 @@ public function getFallback(): string
*/
public function setFallback(string $fallback): void
{
+ $this->assertValidLocale($fallback);
+
$this->fallback = $fallback;
}
+ /**
+ * Forget the loaded translation groups.
+ *
+ * Boot or tests only. The groups are shared by the worker's Translator instance,
+ * and forgetting them during request handling can expose different loaded state
+ * to concurrent coroutines while the groups are repopulated.
+ */
+ public function forgetLoadedGroups(): void
+ {
+ $this->loaded = [];
+ }
+
/**
* Set the loaded translation groups.
*
diff --git a/src/validation/composer.json b/src/validation/composer.json
index e5b72c39e..0f9222aba 100644
--- a/src/validation/composer.json
+++ b/src/validation/composer.json
@@ -1,7 +1,7 @@
{
"name": "hypervel/validation",
"type": "library",
- "description": "The validation package for Hypervel.",
+ "description": "The Hypervel Validation package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/validation/src/ValidationServiceProvider.php b/src/validation/src/ValidationServiceProvider.php
index f6d1273ae..ce503ec26 100644
--- a/src/validation/src/ValidationServiceProvider.php
+++ b/src/validation/src/ValidationServiceProvider.php
@@ -30,13 +30,13 @@ public function register(): void
protected function registerValidationFactory(): void
{
$this->app->singleton('validator', function ($app) {
- $validator = new Factory($app['translator'], $app);
+ $validator = new Factory($app->make('translator'), $app);
// The validation presence verifier is responsible for determining the existence of
// values in a given data collection which is typically a relational database or
// other persistent data stores. It is used to check for "uniqueness" as well.
- if (isset($app['db'], $app['validation.presence'])) {
- $validator->setPresenceVerifier($app['validation.presence']);
+ if ($app->bound('db') && $app->bound('validation.presence')) {
+ $validator->setPresenceVerifier($app->make('validation.presence'));
}
return $validator;
@@ -49,7 +49,7 @@ protected function registerValidationFactory(): void
protected function registerPresenceVerifier(): void
{
$this->app->singleton('validation.presence', function ($app) {
- return new DatabasePresenceVerifier($app['db']);
+ return new DatabasePresenceVerifier($app->make('db'));
});
}
diff --git a/src/view/composer.json b/src/view/composer.json
index a434182da..11bba5eef 100644
--- a/src/view/composer.json
+++ b/src/view/composer.json
@@ -1,6 +1,6 @@
{
"name": "hypervel/view",
- "description": "The view package for Hypervel.",
+ "description": "The Hypervel View package.",
"license": "MIT",
"keywords": [
"php",
diff --git a/src/view/src/Compilers/Compiler.php b/src/view/src/Compilers/Compiler.php
index 535052a54..00a7183a1 100755
--- a/src/view/src/Compilers/Compiler.php
+++ b/src/view/src/Compilers/Compiler.php
@@ -11,20 +11,75 @@
abstract class Compiler
{
+ /**
+ * The directory where compiled views are stored.
+ */
+ protected string $cachePath;
+
+ /**
+ * The application base path removed from compiled view hashes.
+ */
+ protected string $basePath;
+
+ /**
+ * Determine whether compiled views should be cached.
+ */
+ protected bool $shouldCache;
+
+ /**
+ * The compiled view file extension.
+ */
+ protected string $compiledExtension;
+
+ /**
+ * Determine whether compiled view timestamps should be checked.
+ */
+ protected bool $shouldCheckTimestamps;
+
/**
* Create a new compiler instance.
*/
public function __construct(
protected Filesystem $files,
- protected string $cachePath,
- protected string $basePath = '',
- protected bool $shouldCache = true,
- protected string $compiledExtension = 'php',
- protected bool $shouldCheckTimestamps = true,
+ string $cachePath,
+ string $basePath = '',
+ bool $shouldCache = true,
+ string $compiledExtension = 'php',
+ bool $shouldCheckTimestamps = true,
) {
+ $this->reloadConfiguration(
+ $cachePath,
+ $basePath,
+ $shouldCache,
+ $compiledExtension,
+ $shouldCheckTimestamps,
+ );
+ }
+
+ /**
+ * Reload configuration-derived compiler state.
+ *
+ * Boot-only. Mutates the worker-shared compiler while concurrent
+ * coroutines may still compile views using its previous configuration.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function reloadConfiguration(
+ string $cachePath,
+ string $basePath,
+ bool $shouldCache,
+ string $compiledExtension,
+ bool $shouldCheckTimestamps,
+ ): void {
if ($cachePath === '') {
throw new InvalidArgumentException('Please provide a valid cache path.');
}
+
+ $this->cachePath = $cachePath;
+ $this->basePath = $basePath;
+ $this->shouldCache = $shouldCache;
+ $this->compiledExtension = $compiledExtension;
+ $this->shouldCheckTimestamps = $shouldCheckTimestamps;
}
/**
diff --git a/src/view/src/FileViewFinder.php b/src/view/src/FileViewFinder.php
index 2aa205926..2d7b25cb0 100755
--- a/src/view/src/FileViewFinder.php
+++ b/src/view/src/FileViewFinder.php
@@ -122,6 +122,9 @@ 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.
*/
public function addLocation(string $location): void
{
@@ -130,6 +133,9 @@ 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.
*/
public function prependLocation(string $location): void
{
@@ -146,6 +152,9 @@ 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.
*/
public function addNamespace(string $namespace, string|array $hints): void
{
@@ -160,6 +169,9 @@ 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.
*/
public function prependNamespace(string $namespace, string|array $hints): void
{
@@ -174,6 +186,9 @@ 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.
*/
public function replaceNamespace(string $namespace, string|array $hints): void
{
@@ -182,6 +197,9 @@ 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.
*/
public function addExtension(string $extension): void
{
@@ -202,6 +220,9 @@ 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
{
@@ -218,6 +239,9 @@ 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.
*/
public function setPaths(array $paths): static
{
diff --git a/src/view/src/ViewServiceProvider.php b/src/view/src/ViewServiceProvider.php
index 5fbf6e05b..61156c7de 100755
--- a/src/view/src/ViewServiceProvider.php
+++ b/src/view/src/ViewServiceProvider.php
@@ -5,15 +5,18 @@
namespace Hypervel\View;
use Hypervel\Container\Container;
+use Hypervel\Contracts\Config\Repository as ConfigRepository;
use Hypervel\Contracts\Events\Dispatcher;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
use Hypervel\Support\ServiceProvider;
use Hypervel\View\Compilers\BladeCompiler;
+use Hypervel\View\Compilers\Compiler;
use Hypervel\View\Engines\CompilerEngine;
use Hypervel\View\Engines\EngineResolver;
use Hypervel\View\Engines\FileEngine;
use Hypervel\View\Engines\PhpEngine;
-class ViewServiceProvider extends ServiceProvider
+class ViewServiceProvider extends ServiceProvider implements ReloadsConfiguration
{
/**
* Register the service provider.
@@ -35,11 +38,11 @@ public function registerFactory(): void
// Next we need to grab the engine resolver instance that will be used by the
// environment. The resolver will be used by an environment to get each of
// the various engine implementations such as plain PHP or Blade engine.
- $resolver = $app['view.engine.resolver'];
+ $resolver = $app->make('view.engine.resolver');
- $finder = $app['view.finder'];
+ $finder = $app->make('view.finder');
- $factory = $this->createFactory($resolver, $finder, $app['events']);
+ $factory = $this->createFactory($resolver, $finder, $app->make('events'));
// We will also set the container instance on this view environment since the
// view composers may be classes registered in the container, which allows
@@ -52,6 +55,42 @@ public function registerFactory(): void
});
}
+ /**
+ * Reload the worker configuration owned by the provider.
+ *
+ * Boot-only. Calling this while requests are running mutates shared worker
+ * state while concurrent coroutines may still use the previous configuration.
+ */
+ public function reloadConfiguration(): void
+ {
+ $config = $this->app->make(ConfigRepository::class);
+
+ if ($this->app->resolved('view')) {
+ $factory = $this->app->make('view');
+
+ if ($factory instanceof Factory && ($finder = $factory->getFinder()) instanceof FileViewFinder) {
+ $finder->setPaths($config->array('view.paths'));
+ $finder->flush();
+ }
+ }
+
+ if ($this->app->resolved('blade.compiler')) {
+ $compiler = $this->app->make('blade.compiler');
+
+ if ($compiler instanceof Compiler) {
+ $compiler->reloadConfiguration(
+ $config->string('view.compiled'),
+ $config->boolean('view.relative_hash') ? $this->app->basePath() : '',
+ $config->boolean('view.cache'),
+ $config->string('view.compiled_extension'),
+ $config->boolean('view.check_cache_timestamps'),
+ );
+ }
+ }
+
+ CompilerEngine::forgetCompiledOrNotExpired();
+ }
+
/**
* Create a new Factory Instance.
*/
diff --git a/tests/Auth/AuthPasswordBrokerManagerTest.php b/tests/Auth/AuthPasswordBrokerManagerTest.php
index 3443308f5..5e822fe1f 100644
--- a/tests/Auth/AuthPasswordBrokerManagerTest.php
+++ b/tests/Auth/AuthPasswordBrokerManagerTest.php
@@ -311,6 +311,22 @@ public function testBrokerNormalizesEnumsBeforeCaching(): void
$this->assertSame(['0'], $manager->resolvedNames);
}
+ public function testForgetBrokersClearsEveryResolvedBroker(): void
+ {
+ $manager = new AuthPasswordBrokerManagerStub(new Container);
+ $first = m::mock(PasswordBrokerContract::class);
+ $second = m::mock(PasswordBrokerContract::class);
+ $replacement = m::mock(PasswordBrokerContract::class);
+ $manager->seedBroker('first', $first);
+ $manager->seedBroker('second', $second);
+ $manager->resolvedBroker = $replacement;
+
+ $this->assertSame($manager, $manager->forgetBrokers());
+ $this->assertSame($replacement, $manager->broker('first'));
+ $this->assertSame($replacement, $manager->broker('second'));
+ $this->assertSame(['first', 'second'], $manager->resolvedNames);
+ }
+
public function testRefreshingDispatcherUpdatesOnlyConcreteResolvedBrokers(): void
{
$manager = new AuthPasswordBrokerManagerStub(new Container);
diff --git a/tests/Auth/AuthPasswordResetServiceProviderTest.php b/tests/Auth/AuthPasswordResetServiceProviderTest.php
index c3c0e35c1..988cda310 100644
--- a/tests/Auth/AuthPasswordResetServiceProviderTest.php
+++ b/tests/Auth/AuthPasswordResetServiceProviderTest.php
@@ -14,6 +14,23 @@
class AuthPasswordResetServiceProviderTest extends TestCase
{
+ public function testReloadConfigurationClearsResolvedBrokersWithoutResolvingUnusedManager(): void
+ {
+ $application = m::mock(Application::class);
+ $manager = m::mock(PasswordBrokerManager::class);
+ $application->shouldReceive('resolved')->once()->with('auth.password')->andReturnTrue();
+ $application->shouldReceive('make')->once()->with('auth.password')->andReturn($manager);
+ $manager->shouldReceive('forgetBrokers')->once()->andReturnSelf();
+
+ (new PasswordResetServiceProvider($application))->reloadConfiguration();
+
+ $unusedApplication = m::mock(Application::class);
+ $unusedApplication->shouldReceive('resolved')->once()->with('auth.password')->andReturnFalse();
+ $unusedApplication->shouldNotReceive('make');
+
+ (new PasswordResetServiceProvider($unusedApplication))->reloadConfiguration();
+ }
+
public function testEventRebindDoesNotResolveAnUnusedPasswordManager(): void
{
[$application, $callback] = $this->registerProvider();
diff --git a/tests/Auth/AuthServiceProviderTest.php b/tests/Auth/AuthServiceProviderTest.php
index 26525d2b1..7108ae662 100644
--- a/tests/Auth/AuthServiceProviderTest.php
+++ b/tests/Auth/AuthServiceProviderTest.php
@@ -5,6 +5,7 @@
namespace Hypervel\Tests\Auth;
use Closure;
+use Hypervel\Auth\AuthManager;
use Hypervel\Auth\AuthServiceProvider;
use Hypervel\Cache\CacheManager;
use Hypervel\Cache\ModelCacheStoreValidator;
@@ -29,6 +30,23 @@
class AuthServiceProviderTest extends TestCase
{
+ public function testReloadConfigurationClearsResolvedGuardsWithoutResolvingUnusedManager(): void
+ {
+ $application = m::mock(Application::class);
+ $manager = m::mock(AuthManager::class);
+ $application->shouldReceive('resolved')->once()->with('auth')->andReturnTrue();
+ $application->shouldReceive('make')->once()->with('auth')->andReturn($manager);
+ $manager->shouldReceive('forgetGuards')->once()->andReturnSelf();
+
+ (new AuthServiceProvider($application))->reloadConfiguration();
+
+ $unusedApplication = m::mock(Application::class);
+ $unusedApplication->shouldReceive('resolved')->once()->with('auth')->andReturnFalse();
+ $unusedApplication->shouldNotReceive('make');
+
+ (new AuthServiceProvider($unusedApplication))->reloadConfiguration();
+ }
+
public function testBootContributesEnabledConfiguredEloquentModelsAndFrameworkContainers(): void
{
$config = new ConfigRepository([
diff --git a/tests/Auth/EnsureEmailIsVerifiedTest.php b/tests/Auth/EnsureEmailIsVerifiedTest.php
index fad7adbbb..93564f588 100644
--- a/tests/Auth/EnsureEmailIsVerifiedTest.php
+++ b/tests/Auth/EnsureEmailIsVerifiedTest.php
@@ -36,9 +36,9 @@ public function testVerifiedUserPassesThrough()
$this->assertSame($expectedResponse, $result);
}
- public function testUserThatDoesNotImplementMustVerifyEmailPassesThrough()
+ public function testUserThatDoesNotImplementMustVerifyEmailPassesThrough(): void
{
- // User implements Authenticatable but NOT MustVerifyEmail
+ // User implements Authenticatable but does not implement MustVerifyEmail.
$user = m::mock(Authenticatable::class);
$request = m::mock(Request::class);
@@ -78,10 +78,10 @@ public function testUnverifiedUserReturnsJsonWhenExpectsJson()
$middleware->handle($request, fn () => new Response('should not reach'));
}
- public function testUnverifiedUserRedirectsWhenNotJson()
+ public function testUnverifiedUserRedirectsWhenNotJson(): void
{
// Register a named route so URL::route() can resolve it
- $this->app['router']->get('/email/verify', fn () => 'verify')->name('verify.email');
+ $this->app->make('router')->get('/email/verify', fn () => 'verify')->name('verify.email');
$user = m::mock(Authenticatable::class . ',' . MustVerifyEmail::class);
$user->shouldReceive('hasVerifiedEmail')->andReturnFalse();
@@ -96,9 +96,9 @@ public function testUnverifiedUserRedirectsWhenNotJson()
$this->assertSame(302, $result->getStatusCode());
}
- public function testGuestRequestRedirectsWhenNotJson()
+ public function testGuestRequestRedirectsWhenNotJson(): void
{
- $this->app['router']->get('/email/verify', fn () => 'verify')->name('verify.email');
+ $this->app->make('router')->get('/email/verify', fn () => 'verify')->name('verify.email');
$request = m::mock(Request::class);
$request->shouldReceive('user')->andReturn(null);
diff --git a/tests/Broadcasting/BroadcastServiceProviderTest.php b/tests/Broadcasting/BroadcastServiceProviderTest.php
new file mode 100644
index 000000000..faa64bf93
--- /dev/null
+++ b/tests/Broadcasting/BroadcastServiceProviderTest.php
@@ -0,0 +1,34 @@
+shouldReceive('resolved')->once()->with(BroadcastManager::class)->andReturnTrue();
+ $application->shouldReceive('make')->once()->with(BroadcastManager::class)->andReturn($manager);
+ $application->shouldReceive('forgetInstance')->once()->with(Broadcaster::class);
+ $manager->shouldReceive('forgetDrivers')->once()->andReturnSelf();
+
+ (new BroadcastServiceProvider($application))->reloadConfiguration();
+
+ $unusedApplication = m::mock(Application::class);
+ $unusedApplication->shouldReceive('resolved')->once()->with(BroadcastManager::class)->andReturnFalse();
+ $unusedApplication->shouldReceive('forgetInstance')->once()->with(Broadcaster::class);
+ $unusedApplication->shouldNotReceive('make');
+
+ (new BroadcastServiceProvider($unusedApplication))->reloadConfiguration();
+ }
+}
diff --git a/tests/Bus/BusServiceProviderTest.php b/tests/Bus/BusServiceProviderTest.php
new file mode 100644
index 000000000..1deb81749
--- /dev/null
+++ b/tests/Bus/BusServiceProviderTest.php
@@ -0,0 +1,30 @@
+app->make(BatchRepository::class);
+ $databaseRepository = $this->app->make(DatabaseBatchRepository::class);
+
+ $this->assertSame($repository, $databaseRepository);
+
+ $this->app->getProvider(BusServiceProvider::class)->reloadConfiguration();
+
+ $refreshedRepository = $this->app->make(BatchRepository::class);
+ $refreshedDatabaseRepository = $this->app->make(DatabaseBatchRepository::class);
+
+ $this->assertNotSame($repository, $refreshedRepository);
+ $this->assertNotSame($databaseRepository, $refreshedDatabaseRepository);
+ $this->assertSame($refreshedRepository, $refreshedDatabaseRepository);
+ }
+}
diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php
index 33c1b6e10..cae6b70ff 100644
--- a/tests/Cache/CacheManagerTest.php
+++ b/tests/Cache/CacheManagerTest.php
@@ -500,6 +500,27 @@ public function testForgetDriverForgets()
$this->assertNull($cacheManager->store('forget')->get('foo'));
}
+ public function testForgetDriversClearsEveryStoreAndPreservesCustomCreators(): void
+ {
+ $config = [
+ 'cache' => [
+ 'stores' => [
+ 'first' => ['driver' => 'custom'],
+ 'second' => ['driver' => 'custom'],
+ ],
+ ],
+ ];
+ $cacheManager = new CacheManager($this->getApp($config));
+ $cacheManager->extend('custom', fn () => m::mock(CacheRepository::class));
+ $first = $cacheManager->store('first');
+ $second = $cacheManager->store('second');
+
+ $this->assertSame($cacheManager, $cacheManager->forgetDrivers());
+
+ $this->assertNotSame($first, $cacheManager->store('first'));
+ $this->assertNotSame($second, $cacheManager->store('second'));
+ }
+
public function testThrowExceptionWhenUnknownDriverIsUsed()
{
$this->expectException(InvalidArgumentException::class);
diff --git a/tests/Cache/CacheServiceProviderTest.php b/tests/Cache/CacheServiceProviderTest.php
index 486007967..2ee8597f2 100644
--- a/tests/Cache/CacheServiceProviderTest.php
+++ b/tests/Cache/CacheServiceProviderTest.php
@@ -5,14 +5,17 @@
namespace Hypervel\Tests\Cache;
use Closure;
+use Hypervel\Cache\ArrayStore;
use Hypervel\Cache\CacheManager;
use Hypervel\Cache\CacheServiceProvider;
+use Hypervel\Cache\NullStore;
use Hypervel\Config\Repository as ConfigRepository;
use Hypervel\Container\Container;
use Hypervel\Contracts\Events\Dispatcher;
use Hypervel\Contracts\Foundation\Application;
use Hypervel\Core\Events\AfterWorkerStart;
use Hypervel\Core\Events\BeforeServerStart;
+use Hypervel\Foundation\Application as FoundationApplication;
use Hypervel\Support\Facades\Cache;
use Hypervel\Tests\TestCase;
use LogicException;
@@ -21,6 +24,36 @@
class CacheServiceProviderTest extends TestCase
{
+ public function testReloadConfigurationRebuildsResolvedStoresFromCurrentConfiguration(): void
+ {
+ $application = new FoundationApplication;
+ $config = new ConfigRepository([
+ 'cache' => [
+ 'default' => 'array',
+ 'serializable_classes' => false,
+ 'stores' => [
+ 'array' => ['driver' => 'array'],
+ 'null' => ['driver' => 'null'],
+ ],
+ ],
+ ]);
+ $application->instance('config', $config);
+ $provider = new CacheServiceProvider($application);
+ $provider->register();
+
+ $manager = $application->make('cache');
+ $store = $application->make('cache.store');
+ $this->assertInstanceOf(ArrayStore::class, $store->getStore());
+
+ $config->set('cache.default', 'null');
+ $provider->reloadConfiguration();
+
+ $refreshedStore = $application->make('cache.store');
+ $this->assertSame($manager, $application->make('cache'));
+ $this->assertNotSame($store, $refreshedStore);
+ $this->assertInstanceOf(NullStore::class, $refreshedStore->getStore());
+ }
+
public function testConsoleFinalizationRunsAfterEveryProviderCanContribute(): void
{
$manager = $this->manager();
diff --git a/tests/Cache/CacheSwooleStoreTest.php b/tests/Cache/CacheSwooleStoreTest.php
index bd6e663e5..b89d71a5a 100644
--- a/tests/Cache/CacheSwooleStoreTest.php
+++ b/tests/Cache/CacheSwooleStoreTest.php
@@ -20,6 +20,7 @@
use Hypervel\Support\Str;
use Hypervel\Tests\TestCase;
use InvalidArgumentException;
+use LogicException;
use Mockery as m;
use ReflectionMethod;
use stdClass;
@@ -61,6 +62,34 @@ public function testMissingSwooleTableConfigThrowsTableNotDefinedException(): vo
(new SwooleTableManager($container))->get('missing');
}
+ public function testSealingRetainsExistingTablesAndRejectsLateCreation(): void
+ {
+ $config = m::mock(ConfigRepository::class);
+ $config->shouldReceive('get')
+ ->once()
+ ->with('cache.swoole_tables.first')
+ ->andReturn([
+ 'rows' => 64,
+ 'bytes' => 1024,
+ 'conflict_proportion' => 0.2,
+ ]);
+
+ $container = m::mock(Container::class);
+ $container->shouldReceive('make')->once()->with('config')->andReturn($config);
+
+ $manager = new SwooleTableManager($container);
+ $first = $manager->get('first');
+
+ $manager->seal();
+
+ $this->assertSame($first, $manager->get('first'));
+
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Swoole cache table [second] was not initialized before the server fork.');
+
+ $manager->get('second');
+ }
+
public function testSwooleTableRejectsStringValuesLargerThanColumnSize(): void
{
$table = $this->createState(bytes: 8)->table();
diff --git a/tests/Cache/ClearCommandTest.php b/tests/Cache/ClearCommandTest.php
index 2c1292b85..2be571590 100644
--- a/tests/Cache/ClearCommandTest.php
+++ b/tests/Cache/ClearCommandTest.php
@@ -31,7 +31,7 @@ protected function setUp(): void
parent::setUp();
$app = new Application;
- $app['path.storage'] = __DIR__;
+ $app->instance('path.storage', __DIR__);
$this->cacheManager = m::mock(CacheManager::class);
$this->files = m::mock(Filesystem::class);
diff --git a/tests/Cache/CreateSwooleTableTest.php b/tests/Cache/CreateSwooleTableTest.php
new file mode 100644
index 000000000..025a7fc9f
--- /dev/null
+++ b/tests/Cache/CreateSwooleTableTest.php
@@ -0,0 +1,73 @@
+ [
+ 'stores' => [
+ 'swoole' => [
+ 'driver' => 'swoole',
+ 'table' => 'shared',
+ ],
+ ],
+ 'swoole_tables' => [
+ 'shared' => [
+ 'rows' => 64,
+ 'bytes' => 1024,
+ 'conflict_proportion' => 0.2,
+ ],
+ ],
+ ],
+ ]);
+ $container = new Container;
+ $tables = new SwooleTableManager($container);
+ $container->instance('config', $config);
+ $container->instance(SwooleTableManager::class, $tables);
+ $listener = new CreateSwooleTable($container, $config);
+
+ $listener->handle(new BeforeServerStart('http'));
+ $state = $tables->get('shared');
+ $listener->handle(new BeforeServerStart('https'));
+
+ $this->assertSame($state, $tables->get('shared'));
+
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Swoole cache table [late] was not initialized before the server fork.');
+
+ $tables->get('late');
+ }
+
+ public function testSealsTheManagerWhenNoSwooleStoresAreConfigured(): void
+ {
+ $config = new Repository([
+ 'cache' => [
+ 'stores' => [],
+ ],
+ ]);
+ $container = new Container;
+ $tables = new SwooleTableManager($container);
+ $container->instance('config', $config);
+ $container->instance(SwooleTableManager::class, $tables);
+
+ (new CreateSwooleTable($container, $config))->handle(new BeforeServerStart('http'));
+
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Swoole cache table [late] was not initialized before the server fork.');
+
+ $tables->get('late');
+ }
+}
diff --git a/tests/Concurrency/ConcurrencyServiceProviderTest.php b/tests/Concurrency/ConcurrencyServiceProviderTest.php
new file mode 100644
index 000000000..2de72bdf7
--- /dev/null
+++ b/tests/Concurrency/ConcurrencyServiceProviderTest.php
@@ -0,0 +1,40 @@
+ [
+ 'default' => 'sync',
+ ],
+ ]);
+ $application->instance('config', $config);
+ $provider = new ConcurrencyServiceProvider($application);
+ $provider->register();
+
+ $manager = $application->make(ConcurrencyManager::class);
+ $driver = $manager->driver();
+ $this->assertInstanceOf(SyncDriver::class, $driver);
+
+ $config->set('concurrency.default', 'coroutine');
+ $provider->reloadConfiguration();
+
+ $this->assertSame($manager, $application->make(ConcurrencyManager::class));
+ $this->assertNotSame($driver, $manager->driver());
+ $this->assertInstanceOf(CoroutineDriver::class, $manager->driver());
+ }
+}
diff --git a/tests/Concurrency/ConcurrencyTest.php b/tests/Concurrency/ConcurrencyTest.php
index b078f4a04..2d71f0f4a 100644
--- a/tests/Concurrency/ConcurrencyTest.php
+++ b/tests/Concurrency/ConcurrencyTest.php
@@ -321,6 +321,19 @@ public function testManagerDefaultDriverIsCoroutine()
$this->assertSame('coroutine', $manager->getDefaultInstance());
}
+ public function testChangingDefaultDriverPreservesDriverConfiguration(): void
+ {
+ $manager = $this->app->make(ConcurrencyManager::class);
+ $config = $this->app->make('config');
+ $driverConfig = ['driver' => 'sync', 'option' => 'preserved'];
+ $config->set('concurrency.driver.sync', $driverConfig);
+
+ $manager->setDefaultInstance('sync');
+
+ $this->assertSame('sync', $manager->getDefaultInstance());
+ $this->assertSame($driverConfig, $manager->getInstanceConfig('sync'));
+ }
+
public function testManagerResolvesCoroutineDriver()
{
$manager = $this->app->make(ConcurrencyManager::class);
diff --git a/tests/Container/ContainerExtendTest.php b/tests/Container/ContainerExtendTest.php
index fe9017794..602874626 100644
--- a/tests/Container/ContainerExtendTest.php
+++ b/tests/Container/ContainerExtendTest.php
@@ -10,10 +10,10 @@
class ContainerExtendTest extends TestCase
{
- public function testExtendedBindings()
+ public function testExtendedBindings(): void
{
$container = new Container;
- $container['foo'] = 'foo';
+ $container->bind('foo', fn () => 'foo');
$container->extend('foo', function ($old, $container) {
return $old . 'bar';
});
@@ -34,7 +34,7 @@ public function testExtendedBindings()
$result = $container->make('foo');
$this->assertSame('taylor', $result->name);
- $this->assertEquals(26, $result->age);
+ $this->assertSame(26, $result->age);
$this->assertSame($result, $container->make('foo'));
}
@@ -83,13 +83,13 @@ public function testExtendIsLazyInitialized()
$this->assertTrue(ContainerLazyExtendStub::$initialized);
}
- public function testExtendCanBeCalledBeforeBind()
+ public function testExtendCanBeCalledBeforeBind(): void
{
$container = new Container;
$container->extend('foo', function ($old, $container) {
return $old . 'bar';
});
- $container['foo'] = 'foo';
+ $container->bind('foo', fn () => 'foo');
$this->assertSame('foobar', $container->make('foo'));
}
@@ -150,10 +150,10 @@ public function testExtensionWorksOnAliasedBindings()
$this->assertSame('some value extended', $container->make('something'));
}
- public function testMultipleExtends()
+ public function testMultipleExtends(): void
{
$container = new Container;
- $container['foo'] = 'foo';
+ $container->bind('foo', fn () => 'foo');
$container->extend('foo', function ($old, $container) {
return $old . 'bar';
});
@@ -164,7 +164,8 @@ public function testMultipleExtends()
$this->assertSame('foobarbaz', $container->make('foo'));
}
- public function testUnsetExtend()
+ // Upstream: testUnsetExtend; Hypervel tests forgetExtenders() without unsupported container unset.
+ public function testForgetExtenders(): void
{
$container = new Container;
$container->bind('foo', function () {
@@ -180,7 +181,6 @@ public function testUnsetExtend()
return $obj;
});
- unset($container['foo']);
$container->forgetExtenders('foo');
$container->bind('foo', function () {
diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php
index 44fceeb18..b71e34a2d 100755
--- a/tests/Container/ContainerTest.php
+++ b/tests/Container/ContainerTest.php
@@ -308,33 +308,12 @@ public function testContainerIsPassedToResolvers()
$this->assertSame($c, $container);
}
- public function testArrayAccess()
- {
- $container = new Container;
- $this->assertFalse(isset($container['something']));
- $container['something'] = function () {
- return 'foo';
- };
- $this->assertTrue(isset($container['something']));
- $this->assertNotEmpty($container['something']);
- $this->assertSame('foo', $container['something']);
- unset($container['something']);
- $this->assertFalse(isset($container['something']));
-
- // test offsetSet when it's not instanceof Closure
- $container = new Container;
- $container['something'] = 'text';
- $this->assertTrue(isset($container['something']));
- $this->assertNotEmpty($container['something']);
- $this->assertSame('text', $container['something']);
- unset($container['something']);
- $this->assertFalse(isset($container['something']));
- }
+ // REMOVED: Container ArrayAccess is intentionally unsupported; use named container methods.
- public function testAliases()
+ public function testAliases(): void
{
$container = new Container;
- $container['foo'] = 'bar';
+ $container->bind('foo', fn () => 'bar');
$container->alias('foo', 'baz');
$container->alias('baz', 'bat');
$this->assertSame('bar', $container->make('foo'));
@@ -352,12 +331,12 @@ public function testAliasesWithArrayOfParameters()
$this->assertEquals([1, 2, 3], $container->make('baz', [1, 2, 3]));
}
- public function testBindingsCanBeOverridden()
+ public function testBindingsCanBeOverridden(): void
{
$container = new Container;
- $container['foo'] = 'bar';
- $container['foo'] = 'baz';
- $this->assertSame('baz', $container['foo']);
+ $container->bind('foo', fn () => 'bar');
+ $container->bind('foo', fn () => 'baz');
+ $this->assertSame('baz', $container->make('foo'));
}
public function testBindingAnInstanceReturnsTheInstance()
@@ -533,23 +512,17 @@ public function testBound()
$this->assertFalse($container->bound(ContainerConcreteStub::class));
}
- public function testUnsetRemoveBoundInstances()
- {
- $container = new Container;
- $container->instance('object', new stdClass);
- unset($container['object']);
-
- $this->assertFalse($container->bound('object'));
- }
+ // REMOVED: Instance-forgetting coverage is consolidated in testForgetInstanceForgetsInstance.
- public function testBoundInstanceAndAliasCheckViaArrayAccess()
+ // Upstream: testBoundInstanceAndAliasCheckViaArrayAccess; Hypervel uses named binding checks.
+ public function testBoundInstanceAndAliasCheck(): void
{
$container = new Container;
$container->instance('object', new stdClass);
$container->alias('object', 'alias');
- $this->assertTrue(isset($container['object']));
- $this->assertTrue(isset($container['alias']));
+ $this->assertTrue($container->bound('object'));
+ $this->assertTrue($container->bound('alias'));
}
public function testReboundListeners()
@@ -649,16 +622,32 @@ public function testBindingResolutionExceptionMessageWhenClassDoesNotExist()
$container->build('Foo\Bar\Baz\DummyClass');
}
- public function testForgetInstanceForgetsInstance()
+ public function testForgetInstanceForgetsInstance(): void
{
$container = new Container;
$containerConcreteStub = new ContainerConcreteStub;
$container->instance(ContainerConcreteStub::class, $containerConcreteStub);
+ $this->assertTrue($container->bound(ContainerConcreteStub::class));
$this->assertTrue($container->isShared(ContainerConcreteStub::class));
$container->forgetInstance(ContainerConcreteStub::class);
+ $this->assertFalse($container->bound(ContainerConcreteStub::class));
$this->assertFalse($container->isShared(ContainerConcreteStub::class));
}
+ public function testForgetInstanceResolvesAliasBeforeForgettingCachedInstance(): void
+ {
+ $container = new Container;
+ $container->singleton(ContainerConcreteStub::class);
+ $container->alias(ContainerConcreteStub::class, 'container.stub');
+
+ $first = $container->make('container.stub');
+ $container->forgetInstance('container.stub');
+ $second = $container->make('container.stub');
+
+ $this->assertNotSame($first, $second);
+ $this->assertSame($second, $container->make(ContainerConcreteStub::class));
+ }
+
public function testForgetInstanceForgetsScopedInstance()
{
$container = new Container;
@@ -811,30 +800,6 @@ public function testRebindingScopedBindingClearsStaleContextInstance()
$this->assertInstanceOf(ContainerImplementationStubTwo::class, $second);
}
- public function testOffsetUnsetClearsScopedInstance()
- {
- $container = new Container;
- $container->scoped(ContainerConcreteStub::class);
-
- $first = $container->make(ContainerConcreteStub::class);
-
- unset($container[ContainerConcreteStub::class]);
-
- $second = $container->make(ContainerConcreteStub::class);
-
- $this->assertNotSame($first, $second);
- }
-
- public function testOffsetUnsetClearsScopedLifecycleMarker(): void
- {
- $container = new Container;
- $container->scoped(ContainerConcreteStub::class);
-
- unset($container[ContainerConcreteStub::class]);
-
- $this->assertFalse($container->isScoped(ContainerConcreteStub::class));
- }
-
public function testExtendingResolvedAutoSingletonUpdatesCachedInstance(): void
{
$container = new Container;
@@ -1121,14 +1086,7 @@ public function testContainerCanBindAnyWord()
$this->assertInstanceOf(stdClass::class, $container->get('Taylor'));
}
- public function testContainerCanDynamicallySetService()
- {
- $container = new Container;
- $this->assertFalse(isset($container['name']));
- $container['name'] = 'Taylor';
- $this->assertTrue(isset($container['name']));
- $this->assertSame('Taylor', $container['name']);
- }
+ // REMOVED: Container ArrayAccess is intentionally unsupported; use named container methods.
public function testUnknownEntryThrowsException()
{
diff --git a/tests/Cookie/CookieServiceProviderTest.php b/tests/Cookie/CookieServiceProviderTest.php
new file mode 100644
index 000000000..b1d91c6da
--- /dev/null
+++ b/tests/Cookie/CookieServiceProviderTest.php
@@ -0,0 +1,75 @@
+ [
+ 'path' => '/old',
+ 'domain' => 'old.test',
+ 'secure' => false,
+ 'same_site' => 'lax',
+ ],
+ ]);
+ $application->instance('config', $config);
+ $provider = new CookieServiceProvider($application);
+ $provider->register();
+
+ $cookie = $application->make('cookie');
+ $handler = new CookieSessionHandler($cookie, 10);
+ $middleware = new AddQueuedCookiesToResponse($cookie);
+
+ $config->set([
+ 'session.path' => '/new',
+ 'session.domain' => 'new.test',
+ 'session.secure' => true,
+ 'session.same_site' => 'strict',
+ ]);
+ $provider->reloadConfiguration();
+
+ $this->assertSame($cookie, $application->make(CookieJar::class));
+ $handler->write('session-id', 'payload');
+ $response = $middleware->handle(Request::create('/'), static fn (): Response => new Response);
+ $queuedCookie = $response->headers->getCookies()[0];
+
+ $this->assertSame('/new', $queuedCookie->getPath());
+ $this->assertSame('new.test', $queuedCookie->getDomain());
+ $this->assertTrue($queuedCookie->isSecure());
+ $this->assertSame('strict', $queuedCookie->getSameSite());
+ }
+
+ public function testReloadConfigurationDoesNotResolveAnUnusedCookieJar(): void
+ {
+ $application = new Application;
+ $application->instance('config', new Repository([
+ 'session' => [
+ 'path' => '/',
+ 'domain' => null,
+ 'secure' => false,
+ 'same_site' => 'lax',
+ ],
+ ]));
+ $provider = new CookieServiceProvider($application);
+ $provider->register();
+
+ $provider->reloadConfiguration();
+
+ $this->assertFalse($application->resolved('cookie'));
+ }
+}
diff --git a/tests/Core/Bootstrap/WorkerStartCallbackTest.php b/tests/Core/Bootstrap/WorkerStartCallbackTest.php
index cdc75ddaf..168dd03cc 100644
--- a/tests/Core/Bootstrap/WorkerStartCallbackTest.php
+++ b/tests/Core/Bootstrap/WorkerStartCallbackTest.php
@@ -23,49 +23,80 @@
class WorkerStartCallbackTest extends TestCase
{
- public function testDefaultLoggerReloadsAfterBootConfigurationMutationAndBeforeReadiness(): void
+ public function testLifecycleEventsAndStartupLoggingRemainOrdered(): void
{
- $config = new Repository([
- 'app' => ['stdout_log' => ['level' => [LogLevel::ERROR], 'format' => 'line']],
- ]);
- $output = new BufferedOutput;
- $logger = new StdoutLogger($config, $output);
+ $sequence = [];
$server = m::mock(Server::class);
$server->taskworker = false;
- $captured = null;
$dispatcher = m::mock(Dispatcher::class);
$dispatcher->shouldReceive('dispatch')
->once()
->ordered()
->with(m::type(BeforeWorkerStart::class))
- ->andReturnUsing(function () use ($config): void {
- $config->set('app.stdout_log.level', [LogLevel::INFO]);
- $config->set('app.stdout_log.format', 'json');
+ ->andReturnUsing(function () use (&$sequence): void {
+ $sequence[] = 'before';
});
$dispatcher->shouldReceive('dispatch')
->once()
->ordered()
- ->with(m::type(MainWorkerStart::class));
+ ->with(m::type(MainWorkerStart::class))
+ ->andReturnUsing(function () use (&$sequence): void {
+ $sequence[] = 'main';
+ });
$dispatcher->shouldReceive('dispatch')
->once()
->ordered()
->with(m::type(AfterWorkerStart::class))
- ->andReturnUsing(function () use (&$captured, $output): void {
- $captured = $output->fetch();
- $this->assertNotSame('', $captured);
+ ->andReturnUsing(function () use (&$sequence): void {
+ $sequence[] = 'after';
+ });
+
+ $logger = m::mock(StdoutLoggerInterface::class);
+ $logger->shouldReceive('info')
+ ->once()
+ ->with('Worker#0 started.')
+ ->andReturnUsing(function () use (&$sequence): void {
+ $sequence[] = 'started';
});
$coordinator = CoordinatorManager::until(Constants::WORKER_START);
(new WorkerStartCallback($dispatcher, $logger))->onWorkerStart($server, 0);
- $data = json_decode($captured, true, flags: JSON_THROW_ON_ERROR);
-
- $this->assertSame('Worker#0 started.', $data['message']);
+ $this->assertSame(['before', 'main', 'started', 'after'], $sequence);
$this->assertTrue($coordinator->isClosing());
}
+ public function testStartupLoggingUsesConfigurationRefreshedDuringBeforeWorkerStart(): void
+ {
+ $config = new Repository([
+ 'app' => ['stdout_log' => ['level' => [LogLevel::ERROR], 'format' => 'line']],
+ ]);
+ $output = new BufferedOutput;
+ $logger = new StdoutLogger($config, $output);
+ $server = m::mock(Server::class);
+ $server->taskworker = false;
+
+ $dispatcher = m::mock(Dispatcher::class);
+ $dispatcher->shouldReceive('dispatch')
+ ->once()
+ ->with(m::type(BeforeWorkerStart::class))
+ ->andReturnUsing(function () use ($config, $logger): void {
+ $config->set('app.stdout_log.level', [LogLevel::INFO]);
+ $config->set('app.stdout_log.format', 'json');
+ $logger->reloadConfiguration();
+ });
+ $dispatcher->shouldReceive('dispatch')->once()->with(m::type(MainWorkerStart::class));
+ $dispatcher->shouldReceive('dispatch')->once()->with(m::type(AfterWorkerStart::class));
+
+ (new WorkerStartCallback($dispatcher, $logger))->onWorkerStart($server, 0);
+
+ $entry = json_decode($output->fetch(), true, flags: JSON_THROW_ON_ERROR);
+
+ $this->assertSame('Worker#0 started.', $entry['message']);
+ }
+
public function testCustomLoggerOwnsItsConfiguration(): void
{
$server = m::mock(Server::class);
diff --git a/tests/Database/DatabaseServiceProviderTest.php b/tests/Database/DatabaseServiceProviderTest.php
index 793cc4bca..2a4f272f9 100644
--- a/tests/Database/DatabaseServiceProviderTest.php
+++ b/tests/Database/DatabaseServiceProviderTest.php
@@ -4,21 +4,51 @@
namespace Hypervel\Tests\Database;
+use Hypervel\Container\Container;
use Hypervel\Contracts\Database\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract;
+use Hypervel\Contracts\Database\LostConnectionDetector as LostConnectionDetectorContract;
use Hypervel\Contracts\Queue\EntityResolver;
use Hypervel\Core\Events\BeforeServerFork;
use Hypervel\Core\Events\BeforeWorkerStart;
use Hypervel\Core\Events\TaskTerminated;
use Hypervel\Database\ConcurrencyErrorDetector;
+use Hypervel\Database\ConnectionResolver;
use Hypervel\Database\DatabaseServiceProvider;
+use Hypervel\Database\DetectsConcurrencyErrors;
+use Hypervel\Database\DetectsLostConnections;
use Hypervel\Database\Eloquent\QueueEntityResolver;
+use Hypervel\Database\LostConnectionDetector;
use Hypervel\Events\Dispatcher;
use Hypervel\Testbench\TestCase;
+use PDOException;
+use RuntimeException;
use Swoole\Constant;
use Throwable;
class DatabaseServiceProviderTest extends TestCase
{
+ public function testReloadConfigurationRebuildsTheConnectionResolverFromCurrentConfiguration(): void
+ {
+ config(['database.default' => 'first']);
+ $resolver = $this->app->make('db.resolver');
+ $directResolver = $this->app->make(ConnectionResolver::class);
+
+ $this->assertNotSame($resolver, $directResolver);
+ $this->assertSame('first', $resolver->getDefaultConnection());
+ $this->assertSame('first', $directResolver->getDefaultConnection());
+
+ config(['database.default' => 'second']);
+ $this->app->getProvider(DatabaseServiceProvider::class)->reloadConfiguration();
+
+ $refreshedResolver = $this->app->make('db.resolver');
+ $refreshedDirectResolver = $this->app->make(ConnectionResolver::class);
+ $this->assertNotSame($resolver, $refreshedResolver);
+ $this->assertNotSame($directResolver, $refreshedDirectResolver);
+ $this->assertNotSame($refreshedResolver, $refreshedDirectResolver);
+ $this->assertSame('second', $refreshedResolver->getDefaultConnection());
+ $this->assertSame('second', $refreshedDirectResolver->getDefaultConnection());
+ }
+
public function testConcurrencyErrorDetectorIsRegistered(): void
{
$this->assertInstanceOf(
@@ -32,16 +62,98 @@ public function testConcurrencyErrorDetectorCanBeOverridden(): void
$detector = new class implements ConcurrencyErrorDetectorContract {
public function causedByConcurrencyError(Throwable $e): bool
{
- return false;
+ return $e->getMessage() === 'testing override';
}
};
$this->app->instance(ConcurrencyErrorDetectorContract::class, $detector);
- // Reproduce an application binding the contract before provider registration.
+ // The provider must not overwrite an existing application binding.
(new DatabaseServiceProvider($this->app))->register();
$this->assertSame($detector, $this->app->make(ConcurrencyErrorDetectorContract::class));
+
+ $subject = new class {
+ use DetectsConcurrencyErrors;
+
+ public function detects(Throwable $exception): bool
+ {
+ return $this->causedByConcurrencyError($exception);
+ }
+ };
+
+ $this->assertTrue($subject->detects(new RuntimeException('testing override')));
+ }
+
+ public function testLostConnectionDetectorIsRegistered(): void
+ {
+ $this->assertInstanceOf(
+ LostConnectionDetector::class,
+ $this->app->make(LostConnectionDetectorContract::class),
+ );
+ }
+
+ public function testLostConnectionDetectorCanBeOverridden(): void
+ {
+ $detector = new class implements LostConnectionDetectorContract {
+ public int $calls = 0;
+
+ public function causedByLostConnection(Throwable $e): bool
+ {
+ ++$this->calls;
+
+ return $e->getMessage() === 'testing override';
+ }
+ };
+
+ $this->app->instance(LostConnectionDetectorContract::class, $detector);
+
+ // The provider must not overwrite an existing application binding.
+ (new DatabaseServiceProvider($this->app))->register();
+
+ $this->assertSame($detector, $this->app->make(LostConnectionDetectorContract::class));
+
+ $subject = new class {
+ use DetectsLostConnections;
+
+ public function detects(Throwable $exception): bool
+ {
+ return $this->causedByLostConnection($exception);
+ }
+ };
+
+ $this->assertTrue($subject->detects(new RuntimeException('testing override')));
+ $this->assertSame(1, $detector->calls);
+ }
+
+ public function testDetectorTraitsRetainTheirBareContainerFallbacks(): void
+ {
+ $originalContainer = Container::getInstance();
+ Container::setInstance(new Container);
+
+ try {
+ $concurrencySubject = new class {
+ use DetectsConcurrencyErrors;
+
+ public function detects(Throwable $exception): bool
+ {
+ return $this->causedByConcurrencyError($exception);
+ }
+ };
+ $lostConnectionSubject = new class {
+ use DetectsLostConnections;
+
+ public function detects(Throwable $exception): bool
+ {
+ return $this->causedByLostConnection($exception);
+ }
+ };
+
+ $this->assertTrue($concurrencySubject->detects(new PDOException('database is locked')));
+ $this->assertTrue($lostConnectionSubject->detects(new RuntimeException('server has gone away')));
+ } finally {
+ Container::setInstance($originalContainer);
+ }
}
public function testQueueEntityResolverIsRegistered(): void
diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php
index 822adc707..d3495a7fa 100644
--- a/tests/Filesystem/FilesystemManagerTest.php
+++ b/tests/Filesystem/FilesystemManagerTest.php
@@ -568,6 +568,25 @@ public function testForgetDiskDropsOnlyTheWrapperAndPreservesTheSharedPool(): vo
);
}
+ public function testForgetDisksClearsEveryDiskAndPreservesCustomCreators(): void
+ {
+ $container = $this->getContainer([
+ 'disks' => [
+ 'first' => ['driver' => 'custom'],
+ 'second' => ['driver' => 'custom'],
+ ],
+ ]);
+ $manager = new FilesystemManager($container);
+ $manager->extend('custom', fn () => m::mock(Filesystem::class));
+ $first = $manager->disk('first');
+ $second = $manager->disk('second');
+
+ $this->assertSame($manager, $manager->forgetDisks());
+
+ $this->assertNotSame($first, $manager->disk('first'));
+ $this->assertNotSame($second, $manager->disk('second'));
+ }
+
public function testPurgeClosesCachedAndNeverCachedClientPools(): void
{
$container = $this->getContainer([
diff --git a/tests/Filesystem/FilesystemServiceProviderTest.php b/tests/Filesystem/FilesystemServiceProviderTest.php
new file mode 100644
index 000000000..befb2ca71
--- /dev/null
+++ b/tests/Filesystem/FilesystemServiceProviderTest.php
@@ -0,0 +1,49 @@
+ [
+ 'default' => 'first',
+ 'disks' => [
+ 'first' => [
+ 'driver' => 'local',
+ 'root' => ParallelTesting::tempDir('FilesystemServiceProviderTest/first'),
+ ],
+ 'second' => [
+ 'driver' => 'local',
+ 'root' => ParallelTesting::tempDir('FilesystemServiceProviderTest/second'),
+ ],
+ ],
+ ],
+ ]);
+ $application->instance('config', $config);
+ $provider = new FilesystemServiceProvider($application);
+ $provider->register();
+
+ $manager = $application->make('filesystem');
+ $disk = $application->make('filesystem.disk');
+
+ $config->set('filesystems.default', 'second');
+ $provider->reloadConfiguration();
+
+ $refreshedDisk = $application->make('filesystem.disk');
+ $this->assertSame($manager, $application->make(FilesystemManager::class));
+ $this->assertNotSame($disk, $refreshedDisk);
+ $this->assertSame($refreshedDisk, $manager->disk('second'));
+ }
+}
diff --git a/tests/Fortify/AuthenticatedSessionControllerWithTwoFactorTest.php b/tests/Fortify/AuthenticatedSessionControllerWithTwoFactorTest.php
index e3d648ce2..70abb8fad 100644
--- a/tests/Fortify/AuthenticatedSessionControllerWithTwoFactorTest.php
+++ b/tests/Fortify/AuthenticatedSessionControllerWithTwoFactorTest.php
@@ -135,7 +135,7 @@ public function testUserCanAuthenticateWhenTwoFactorChallengeIsDisabled(): void
public function testRehashUserPasswordWhenRedirectingToTwoFactorChallengeIfRehashingOnLoginIsEnabled(): void
{
- $this->app['config']->set('hashing.rehash_on_login', true);
+ $this->app->make('config')->set('hashing.rehash_on_login', true);
$user = UserWithTwoFactor::forceCreate([
'name' => 'Taylor Otwell',
@@ -157,7 +157,7 @@ public function testRehashUserPasswordWhenRedirectingToTwoFactorChallengeIfRehas
public function testDoesNotRehashUserPasswordWhenRedirectingToTwoFactorChallengeIfRehashingOnLoginIsDisabled(): void
{
- $this->app['config']->set('hashing.rehash_on_login', false);
+ $this->app->make('config')->set('hashing.rehash_on_login', false);
$user = UserWithTwoFactor::forceCreate([
'name' => 'Taylor Otwell',
diff --git a/tests/Foundation/ApplicationRunningInConsoleTest.php b/tests/Foundation/ApplicationRunningInConsoleTest.php
index e31be6c98..aac00be72 100644
--- a/tests/Foundation/ApplicationRunningInConsoleTest.php
+++ b/tests/Foundation/ApplicationRunningInConsoleTest.php
@@ -309,7 +309,7 @@ public function testRunningConsoleCommandReturnsFalseWhenNoArgvSet()
// detectEnvironment integration
// ------------------------------------------------------------------
- public function testDetectEnvironmentUsesArgvWhenInConsole()
+ public function testDetectEnvironmentUsesArgvWhenInConsole(): void
{
$_SERVER['argv'] = ['artisan', '--env=staging'];
$app = new Application;
@@ -318,6 +318,7 @@ public function testDetectEnvironmentUsesArgvWhenInConsole()
$result = $app->detectEnvironment(fn () => 'default');
$this->assertSame('staging', $result);
+ $this->assertSame('staging', $app->environment());
}
public function testDetectEnvironmentIgnoresArgvWhenNotInConsole()
diff --git a/tests/Foundation/Bootstrap/LoadConfigurationTest.php b/tests/Foundation/Bootstrap/LoadConfigurationTest.php
index 01b7a4ae3..80a5b17e4 100644
--- a/tests/Foundation/Bootstrap/LoadConfigurationTest.php
+++ b/tests/Foundation/Bootstrap/LoadConfigurationTest.php
@@ -17,16 +17,16 @@
class LoadConfigurationTest extends TestCase
{
- public function testLoadsBaseConfiguration()
+ public function testLoadsBaseConfiguration(): void
{
$app = new Application;
(new LoadConfiguration)->bootstrap($app);
- $this->assertSame('Hypervel', $app['config']['app.name']);
+ $this->assertSame('Hypervel', $app->make('config')->string('app.name'));
}
- public function testSetsEnvironmentResolver()
+ public function testSetsEnvironmentResolver(): void
{
$app = new Application;
$this->assertNull((new ReflectionClass($app))->getProperty('environmentResolver')->getValue($app));
@@ -39,28 +39,30 @@ public function testSetsEnvironmentResolver()
);
}
- public function testDontLoadBaseConfiguration()
+ public function testDontLoadBaseConfiguration(): void
{
$app = new Application;
$app->dontMergeFrameworkConfiguration();
(new LoadConfiguration)->bootstrap($app);
- $this->assertNull($app['config']['app.name']);
+ $this->assertNull($app->make('config')->get('app.name'));
}
- public function testLoadsConfigurationInIsolation()
+ public function testLoadsConfigurationInIsolation(): void
{
$app = new Application(__DIR__ . '/../Fixtures');
$app->useConfigPath(__DIR__ . '/../Fixtures/config');
(new LoadConfiguration)->bootstrap($app);
- $this->assertNull($app['config']['bar.foo']);
- $this->assertSame('bar', $app['config']['custom.foo']);
+ $config = $app->make('config');
+
+ $this->assertNull($config->get('bar.foo'));
+ $this->assertSame('bar', $config->string('custom.foo'));
}
- public function testConfigurationArrayKeysMatchLoadedFilenames()
+ public function testConfigurationArrayKeysMatchLoadedFilenames(): void
{
$baseConfigPath = dirname((new ReflectionClass(LoadConfiguration::class))->getFileName(), 3) . '/config';
$customConfigPath = __DIR__ . '/../Fixtures/config';
@@ -71,7 +73,7 @@ public function testConfigurationArrayKeysMatchLoadedFilenames()
(new LoadConfiguration)->bootstrap($app);
$this->assertEqualsCanonicalizing(
- array_keys($app['config']->all()),
+ array_keys($app->make('config')->all()),
collect((new Filesystem)->files([
$baseConfigPath,
$customConfigPath,
@@ -79,14 +81,14 @@ public function testConfigurationArrayKeysMatchLoadedFilenames()
);
}
- public function testShouldMergeFrameworkConfigurationDefaultsToTrue()
+ public function testShouldMergeFrameworkConfigurationDefaultsToTrue(): void
{
$app = new Application;
$this->assertTrue($app->shouldMergeFrameworkConfiguration());
}
- public function testDontMergeFrameworkConfigurationReturnsSelf()
+ public function testDontMergeFrameworkConfigurationReturnsSelf(): void
{
$app = new Application;
@@ -96,22 +98,24 @@ public function testDontMergeFrameworkConfigurationReturnsSelf()
$this->assertFalse($app->shouldMergeFrameworkConfiguration());
}
- public function testBaseConfigurationIncludesCoreFrameworkConfigs()
+ public function testBaseConfigurationIncludesCoreFrameworkConfigs(): void
{
$app = new Application;
(new LoadConfiguration)->bootstrap($app);
+ $config = $app->make('config');
+
// All centralized framework configs should be loaded
foreach (['app', 'auth', 'cache', 'database', 'logging', 'session', 'view'] as $key) {
$this->assertNotNull(
- $app['config'][$key],
+ $config->get($key),
"Framework config '{$key}' should be loaded by LoadConfiguration."
);
}
}
- public function testDontMergeFrameworkConfigurationSkipsAllBaseConfigs()
+ public function testDontMergeFrameworkConfigurationSkipsAllBaseConfigs(): void
{
$app = new Application;
$app->dontMergeFrameworkConfiguration();
@@ -119,23 +123,27 @@ public function testDontMergeFrameworkConfigurationSkipsAllBaseConfigs()
(new LoadConfiguration)->bootstrap($app);
// No base config should be present (app has no config dir with files)
- $this->assertNull($app['config']['auth']);
- $this->assertNull($app['config']['cache']);
- $this->assertNull($app['config']['database']);
+ $config = $app->make('config');
+
+ $this->assertNull($config->get('auth'));
+ $this->assertNull($config->get('cache'));
+ $this->assertNull($config->get('database'));
}
- public function testAppConfigOverridesBaseConfigValues()
+ public function testAppConfigOverridesBaseConfigValues(): void
{
$app = new Application(__DIR__ . '/../Fixtures');
$app->useConfigPath(__DIR__ . '/../Fixtures/config');
(new LoadConfiguration)->bootstrap($app);
+ $config = $app->make('config');
+
// custom.php is app-specific, should be loaded
- $this->assertSame('bar', $app['config']['custom.foo']);
+ $this->assertSame('bar', $config->string('custom.foo'));
// Base configs should still be loaded for keys not in the app config dir
- $this->assertNotNull($app['config']['auth']);
+ $this->assertNotNull($config->get('auth'));
}
public function testFailedReloadRestoresThePreviousRepositoryAndException(): void
diff --git a/tests/Foundation/Console/KernelTerminateTest.php b/tests/Foundation/Console/KernelTerminateTest.php
index c027ac786..eabd0bea1 100644
--- a/tests/Foundation/Console/KernelTerminateTest.php
+++ b/tests/Foundation/Console/KernelTerminateTest.php
@@ -237,7 +237,9 @@ public function testDurationThresholdWithDateTimeInterfaceNotExceeded(): void
public function testTerminateUsesConfiguredTimezone(): void
{
- $this->app['config']->set('app.timezone', 'UTC');
+ $config = $this->app->make('config');
+
+ $config->set('app.timezone', 'UTC');
$startedAt = null;
$kernel = $this->app->make(KernelContract::class);
@@ -248,7 +250,7 @@ public function testTerminateUsesConfiguredTimezone(): void
$this->assertSame($started, $kernel->commandStartedAt());
});
- $this->app['config']->set('app.timezone', 'Australia/Melbourne');
+ $config->set('app.timezone', 'Australia/Melbourne');
CarbonImmutable::setTestNow(CarbonImmutable::now());
$input = new StringInput('foo');
diff --git a/tests/Foundation/Exceptions/Renderer/FrameTest.php b/tests/Foundation/Exceptions/Renderer/FrameTest.php
index ab28a3a77..4081583c7 100644
--- a/tests/Foundation/Exceptions/Renderer/FrameTest.php
+++ b/tests/Foundation/Exceptions/Renderer/FrameTest.php
@@ -13,7 +13,7 @@
class FrameTest extends TestCase
{
#[DataProvider('unixFileDataProvider')]
- public function testItNormalizesFilePathOnUnix($frameData, $basePath, $expected)
+ public function testItNormalizesFilePathOnUnix(array $frameData, string $basePath, string $expected): void
{
$exception = m::mock(FlattenException::class);
$classMap = [];
@@ -22,7 +22,7 @@ public function testItNormalizesFilePathOnUnix($frameData, $basePath, $expected)
$this->assertEquals($expected, $frame->file());
}
- public static function unixFileDataProvider()
+ public static function unixFileDataProvider(): iterable
{
yield 'internal function' => [
['line' => 10],
@@ -50,7 +50,7 @@ public static function unixFileDataProvider()
// REMOVED: windowsFileDataProvider - Swoole doesn't run on Windows
#[DataProvider('unixIsFromVendorDataProvider')]
- public function testItDeterminesIfFrameIsFromVendorOnUnix($frameData, $basePath, $expected)
+ public function testItDeterminesIfFrameIsFromVendorOnUnix(array $frameData, string $basePath, bool $expected): void
{
$exception = m::mock(FlattenException::class);
$classMap = [];
@@ -59,7 +59,7 @@ public function testItDeterminesIfFrameIsFromVendorOnUnix($frameData, $basePath,
$this->assertEquals($expected, $frame->isFromVendor());
}
- public static function unixIsFromVendorDataProvider()
+ public static function unixIsFromVendorDataProvider(): iterable
{
yield 'vendor file' => [
['file' => '/path/to/your-app/vendor/laravel/framework/src/File.php', 'line' => 10],
diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php
index ae69f3462..0376f99f9 100644
--- a/tests/Foundation/FoundationApplicationTest.php
+++ b/tests/Foundation/FoundationApplicationTest.php
@@ -332,7 +332,7 @@ public function testDebugHelper()
$this->assertTrue($debugOn->hasDebugModeEnabled());
}
- public function testBeforeBootstrappingAddsClosure()
+ public function testBeforeBootstrappingAddsClosure(): void
{
$app = new Application;
$eventDispatcher = new EventDispatcher($app);
@@ -340,10 +340,10 @@ public function testBeforeBootstrappingAddsClosure()
$closure = function () {};
$app->beforeBootstrapping(RegisterFacades::class, $closure);
- $this->assertArrayHasKey(0, $app['events']->getListeners('bootstrapping: Hypervel\Foundation\Bootstrap\RegisterFacades'));
+ $this->assertArrayHasKey(0, $app->make('events')->getListeners('bootstrapping: Hypervel\Foundation\Bootstrap\RegisterFacades'));
}
- public function testAfterBootstrappingAddsClosure()
+ public function testAfterBootstrappingAddsClosure(): void
{
$app = new Application;
$eventDispatcher = new EventDispatcher($app);
@@ -351,7 +351,7 @@ public function testAfterBootstrappingAddsClosure()
$closure = function () {};
$app->afterBootstrapping(RegisterFacades::class, $closure);
- $this->assertArrayHasKey(0, $app['events']->getListeners('bootstrapped: Hypervel\Foundation\Bootstrap\RegisterFacades'));
+ $this->assertArrayHasKey(0, $app->make('events')->getListeners('bootstrapped: Hypervel\Foundation\Bootstrap\RegisterFacades'));
}
public function testTerminationTests()
@@ -815,7 +815,7 @@ public function testAbortAcceptsHeaders()
}
}
- public function testMethodAfterLoadingEnvironmentAddsClosure()
+ public function testMethodAfterLoadingEnvironmentAddsClosure(): void
{
$app = new Application;
$eventDispatcher = new EventDispatcher($app);
@@ -824,7 +824,7 @@ public function testMethodAfterLoadingEnvironmentAddsClosure()
$closure = function () {};
$app->afterLoadingEnvironment($closure);
- $listeners = $app['events']->getListeners('bootstrapped: ' . LoadEnvironmentVariables::class);
+ $listeners = $app->make('events')->getListeners('bootstrapped: ' . LoadEnvironmentVariables::class);
$this->assertArrayHasKey(0, $listeners);
}
diff --git a/tests/Foundation/FoundationDevCommandsTest.php b/tests/Foundation/FoundationDevCommandsTest.php
index f3c2c9dff..0d8db6ba3 100644
--- a/tests/Foundation/FoundationDevCommandsTest.php
+++ b/tests/Foundation/FoundationDevCommandsTest.php
@@ -21,7 +21,7 @@ protected function setUp(): void
DevCommands::flushState();
$app = new Application(__DIR__);
- $app['env'] = 'testing';
+ $app->instance('env', 'testing');
$app->setRunningInConsole(true);
}
diff --git a/tests/Foundation/FoundationHelpersTest.php b/tests/Foundation/FoundationHelpersTest.php
index bde1d9f9d..c421ccd86 100644
--- a/tests/Foundation/FoundationHelpersTest.php
+++ b/tests/Foundation/FoundationHelpersTest.php
@@ -173,10 +173,10 @@ public function testTodayWithNull(): void
$this->assertSame(CarbonImmutable::class, $result::class);
}
- public function testCache()
+ public function testCache(): void
{
$cache = m::mock(CacheManager::class);
- $this->app['cache'] = $cache;
+ $this->app->instance('cache', $cache);
// cache() returns the CacheManager
$this->assertInstanceOf(CacheManager::class, cache());
diff --git a/tests/Foundation/FoundationViteTest.php b/tests/Foundation/FoundationViteTest.php
index fdaafe1de..f8afaa677 100644
--- a/tests/Foundation/FoundationViteTest.php
+++ b/tests/Foundation/FoundationViteTest.php
@@ -847,7 +847,7 @@ public function testViteCanAssetPath(): void
],
], $buildDir = Str::random());
$vite = app(Vite::class)->useBuildDirectory($buildDir);
- $this->app['config']->set('app.url', 'https://cdn.app.com');
+ $this->app->make('config')->set('app.url', 'https://cdn.app.com');
// default behaviour...
$this->assertSame("https://cdn.app.com/{$buildDir}/assets/profile.versioned.png", $vite->asset('resources/images/profile.png'));
diff --git a/tests/Foundation/Listeners/ReloadDotenvAndConfigTest.php b/tests/Foundation/Listeners/ReloadDotenvAndConfigTest.php
index c03bcedeb..20eab6b5a 100644
--- a/tests/Foundation/Listeners/ReloadDotenvAndConfigTest.php
+++ b/tests/Foundation/Listeners/ReloadDotenvAndConfigTest.php
@@ -5,11 +5,18 @@
namespace Hypervel\Tests\Foundation\Listeners;
use Hypervel\Config\Repository;
+use Hypervel\Contracts\Foundation\ReloadsConfiguration;
+use Hypervel\Contracts\Log\StdoutLoggerInterface;
use Hypervel\Core\Events\BeforeWorkerStart;
+use Hypervel\Core\Logger\StdoutLogger;
use Hypervel\Filesystem\Filesystem;
+use Hypervel\Fortify\FortifyServiceProvider;
use Hypervel\Foundation\Application;
use Hypervel\Foundation\Bootstrap\LoadConfiguration;
+use Hypervel\Foundation\Configuration\ConfigMutationTracker;
use Hypervel\Foundation\Listeners\ReloadDotenvAndConfig;
+use Hypervel\Horizon\HorizonServiceProvider;
+use Hypervel\Sentry\SentryServiceProvider;
use Hypervel\Support\DotenvManager;
use Hypervel\Support\Env;
use Hypervel\Support\Facades\Config as ConfigFacade;
@@ -17,6 +24,9 @@
use Hypervel\Testing\ParallelTesting;
use Hypervel\Tests\TestCase;
use Mockery as m;
+use Psr\Log\LogLevel;
+use RuntimeException;
+use Symfony\Component\Console\Output\BufferedOutput;
class ReloadDotenvAndConfigTest extends TestCase
{
@@ -201,6 +211,133 @@ public function register(): void
}
}
+ public function testReloadReevaluatesDerivedPackageConfigurationAgainstWorkerEnvironment(): void
+ {
+ $app = $this->createApp();
+ DotenvManager::load([$app->environmentPath()]);
+ $config = $app->make(Repository::class);
+
+ $app->make(ConfigMutationTracker::class)->applyAndRecord(
+ $config,
+ static function (Repository $config): void {
+ $environment = (string) Env::get('TEST_KEY');
+
+ $config->set([
+ 'app.url' => "https://{$environment}.example.com",
+ 'app.key' => "key-{$environment}",
+ 'app.name' => "Application {$environment}",
+ 'fortify.passkeys.timeout' => $environment === 'default_value' ? 1000 : 2000,
+ 'sentry.logs_channel_level' => "level-{$environment}",
+ 'logging.channels.sentry' => [
+ 'driver' => 'custom-sentry',
+ 'environment' => $environment,
+ ],
+ ]);
+ },
+ );
+
+ (new ReloadDerivedFortifyConfiguration($app))->configureDerivedValues();
+ (new ReloadDerivedSentryConfiguration($app))->configureDerivedValues();
+ (new ReloadDerivedHorizonConfiguration($app))->configureDerivedValues();
+
+ $this->assertSame('default_value.example.com', $config->get('passkeys.relying_party_id'));
+ $this->assertSame(['https://default_value.example.com'], $config->get('passkeys.allowed_origins'));
+ $this->assertSame('key-default_value', $config->get('passkeys.user_handle_secret'));
+ $this->assertSame(1000, $config->get('passkeys.timeout'));
+ $this->assertSame('Application default_value', $config->get('horizon.name'));
+ $this->assertSame('level-default_value', $config->get('logging.channels.sentry_logs.level'));
+ $this->assertSame('custom-sentry', $config->get('logging.channels.sentry.driver'));
+
+ $app->loadEnvironmentFrom('.env.testing');
+ $app->make(ReloadDotenvAndConfig::class)->handle(m::mock(BeforeWorkerStart::class));
+
+ $this->assertSame('testing_value.example.com', $config->get('passkeys.relying_party_id'));
+ $this->assertSame(['https://testing_value.example.com'], $config->get('passkeys.allowed_origins'));
+ $this->assertSame('key-testing_value', $config->get('passkeys.user_handle_secret'));
+ $this->assertSame(2000, $config->get('passkeys.timeout'));
+ $this->assertSame('Application testing_value', $config->get('horizon.name'));
+ $this->assertSame('level-testing_value', $config->get('logging.channels.sentry_logs.level'));
+ $this->assertSame([
+ 'driver' => 'custom-sentry',
+ 'environment' => 'testing_value',
+ ], $config->get('logging.channels.sentry'));
+ }
+
+ public function testReloadRunsConfigurationHooksInProviderOrderAfterMutationReplay(): void
+ {
+ $app = $this->createApp();
+ $config = $app->make(Repository::class);
+ $unrelatedServiceResolved = false;
+
+ $app->make(ConfigMutationTracker::class)->applyAndRecord(
+ $config,
+ static function (Repository $config): void {
+ $config->set('reload.calls', ['mutation']);
+ },
+ );
+ $app->register(new FirstReloadConfigurationProvider($app));
+ $app->register(new NonReloadConfigurationProvider($app));
+ $app->register(new SecondReloadConfigurationProvider($app));
+ $app->bind(ReloadUnrelatedService::class, function () use (&$unrelatedServiceResolved) {
+ $unrelatedServiceResolved = true;
+
+ return new ReloadUnrelatedService;
+ });
+
+ $app->make(ReloadDotenvAndConfig::class)->handle(m::mock(BeforeWorkerStart::class));
+
+ $this->assertSame(['mutation', 'first', 'second'], $config->get('reload.calls'));
+ $this->assertFalse($unrelatedServiceResolved);
+ }
+
+ public function testReloadRefreshesTheRetainedStdoutLoggerAfterMutationReplay(): void
+ {
+ $app = $this->createApp();
+ $config = $app->make(Repository::class);
+ $config->set([
+ 'app.stdout_log.level' => [LogLevel::ERROR],
+ 'app.stdout_log.format' => 'line',
+ ]);
+ $output = new BufferedOutput;
+ $logger = new StdoutLogger($config, $output);
+ $app->instance(StdoutLoggerInterface::class, $logger);
+
+ $app->make(ConfigMutationTracker::class)->applyAndRecord(
+ $config,
+ static function (Repository $config): void {
+ $config->set([
+ 'app.stdout_log.level' => [LogLevel::INFO],
+ 'app.stdout_log.format' => 'json',
+ ]);
+ },
+ );
+
+ $app->make(ReloadDotenvAndConfig::class)->handle(m::mock(BeforeWorkerStart::class));
+ $logger->info('Refreshed.');
+
+ $entry = json_decode($output->fetch(), true, flags: JSON_THROW_ON_ERROR);
+
+ $this->assertSame('Refreshed.', $entry['message']);
+ }
+
+ public function testReloadStopsAtTheFirstFailingConfigurationHook(): void
+ {
+ $app = $this->createApp();
+ $config = $app->make(Repository::class);
+
+ $app->register(new FailingReloadConfigurationProvider($app));
+ $app->register(new SkippedReloadConfigurationProvider($app));
+
+ try {
+ $app->make(ReloadDotenvAndConfig::class)->handle(m::mock(BeforeWorkerStart::class));
+ $this->fail('Expected configuration refresh to stop at the failing provider.');
+ } catch (RuntimeException $exception) {
+ $this->assertSame('Configuration refresh failed.', $exception->getMessage());
+ }
+
+ $this->assertSame(['failing'], $config->get('reload.calls'));
+ }
+
protected function createApp(): Application
{
$app = new Application(__DIR__ . '/../Fixtures/envs');
@@ -224,3 +361,80 @@ protected function restoreAppName(): void
$_SERVER['APP_NAME'] = $this->originalAppName;
}
}
+
+class ReloadDerivedFortifyConfiguration extends FortifyServiceProvider
+{
+ public function configureDerivedValues(): void
+ {
+ $this->mergeConfigFrom(dirname(__DIR__, 3) . '/src/fortify/config/fortify.php', 'fortify');
+ $this->configurePasskeys();
+ }
+}
+
+class ReloadDerivedSentryConfiguration extends SentryServiceProvider
+{
+ public function configureDerivedValues(): void
+ {
+ $this->mergeConfigFrom(dirname(__DIR__, 3) . '/src/sentry/config/sentry.php', 'sentry');
+ $this->registerLogChannels();
+ }
+}
+
+class ReloadDerivedHorizonConfiguration extends HorizonServiceProvider
+{
+ public function configureDerivedValues(): void
+ {
+ $this->mergeConfigFrom(dirname(__DIR__, 3) . '/src/horizon/config/horizon.php', 'horizon');
+ $this->normalizeConfig();
+ }
+}
+
+class FirstReloadConfigurationProvider extends ServiceProvider implements ReloadsConfiguration
+{
+ public function reloadConfiguration(): void
+ {
+ $config = $this->app->make(Repository::class);
+ $config->set('reload.calls', [...$config->array('reload.calls', []), 'first']);
+ }
+}
+
+class SecondReloadConfigurationProvider extends ServiceProvider implements ReloadsConfiguration
+{
+ public function reloadConfiguration(): void
+ {
+ $config = $this->app->make(Repository::class);
+ $config->set('reload.calls', [...$config->array('reload.calls', []), 'second']);
+ }
+}
+
+class FailingReloadConfigurationProvider extends ServiceProvider implements ReloadsConfiguration
+{
+ public function reloadConfiguration(): void
+ {
+ $config = $this->app->make(Repository::class);
+ $config->set('reload.calls', [...$config->array('reload.calls', []), 'failing']);
+
+ throw new RuntimeException('Configuration refresh failed.');
+ }
+}
+
+class SkippedReloadConfigurationProvider extends ServiceProvider implements ReloadsConfiguration
+{
+ public function reloadConfiguration(): void
+ {
+ $config = $this->app->make(Repository::class);
+ $config->set('reload.calls', [...$config->array('reload.calls', []), 'skipped']);
+ }
+}
+
+class NonReloadConfigurationProvider extends ServiceProvider
+{
+ public function reloadConfiguration(): never
+ {
+ throw new RuntimeException('Non-reload provider was invoked.');
+ }
+}
+
+class ReloadUnrelatedService
+{
+}
diff --git a/tests/Foundation/Providers/FoundationServiceProviderTest.php b/tests/Foundation/Providers/FoundationServiceProviderTest.php
index 3cd6852be..7300526b8 100644
--- a/tests/Foundation/Providers/FoundationServiceProviderTest.php
+++ b/tests/Foundation/Providers/FoundationServiceProviderTest.php
@@ -8,16 +8,22 @@
use Hypervel\Contracts\Console\Kernel;
use Hypervel\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract;
use Hypervel\Foundation\ArrayMaintenanceMode;
+use Hypervel\Foundation\Console\CliDumper;
use Hypervel\Foundation\DevCommands;
+use Hypervel\Foundation\Http\HtmlDumper;
use Hypervel\Foundation\MaintenanceModeManager;
+use Hypervel\Foundation\Providers\FoundationServiceProvider;
use Hypervel\Foundation\WorkerCachedMaintenanceMode;
use Hypervel\Http\Request;
use Hypervel\Support\Carbon;
use Hypervel\Support\CarbonImmutable;
use Hypervel\Support\Facades\Date;
use Hypervel\Testbench\TestCase;
+use PHPUnit\Framework\Attributes\DataProvider;
use Psr\Clock\ClockInterface;
use ReflectionClass;
+use ReflectionProperty;
+use Symfony\Component\VarDumper\VarDumper;
class FoundationServiceProviderTest extends TestCase
{
@@ -143,4 +149,97 @@ public function testArrayMaintenanceModeDriverIsAvailable(): void
$this->assertInstanceOf(ArrayMaintenanceMode::class, $driver);
}
+
+ public function testReloadConfigurationRefreshesTimezoneAndMaintenanceModeState(): void
+ {
+ $manager = $this->app->make(MaintenanceModeManager::class);
+ $initialDriver = new ArrayMaintenanceMode;
+ $refreshedDriver = new ArrayMaintenanceMode;
+ $refreshedDriver->activate(['message' => 'refreshed']);
+ $manager->extend('initial', fn () => $initialDriver);
+ $manager->extend('refreshed', fn () => $refreshedDriver);
+ config([
+ 'app.maintenance.driver' => 'initial',
+ 'app.maintenance.refresh_interval' => 0,
+ ]);
+
+ $initialMode = $this->app->make(MaintenanceModeContract::class);
+
+ $this->assertFalse($initialMode->active());
+
+ config([
+ 'app.maintenance.driver' => 'refreshed',
+ 'app.timezone' => 'Pacific/Auckland',
+ ]);
+
+ $this->app->getProvider(FoundationServiceProvider::class)->reloadConfiguration();
+
+ $refreshedMode = $this->app->make(MaintenanceModeContract::class);
+
+ $this->assertSame('Pacific/Auckland', date_default_timezone_get());
+ $this->assertNotSame($initialMode, $refreshedMode);
+ $this->assertTrue($refreshedMode->active());
+ $this->assertSame(['message' => 'refreshed'], $refreshedMode->data());
+ }
+
+ #[DataProvider('explicitDumperFormats')]
+ public function testExplicitDumperFormatInstallsHypervelHandlerAndRestoresEnvironment(
+ string $format,
+ string $expectedDumper,
+ ): void {
+ $handlerProperty = new ReflectionProperty(VarDumper::class, 'handler');
+ $originalHandler = $handlerProperty->getValue();
+ $originalFormatExists = array_key_exists('VAR_DUMPER_FORMAT', $_SERVER);
+ $originalFormat = $_SERVER['VAR_DUMPER_FORMAT'] ?? null;
+ $sentinelHandler = static function (): void {
+ };
+
+ unset($_SERVER['VAR_DUMPER_FORMAT']);
+ VarDumper::setHandler($sentinelHandler);
+ $_SERVER['VAR_DUMPER_FORMAT'] = $format;
+
+ try {
+ $provider = new FoundationServiceProvider($this->app);
+ (new ReflectionClass($provider))->getMethod('registerDumper')->invoke($provider);
+
+ $this->assertSame($format, $_SERVER['VAR_DUMPER_FORMAT']);
+ $this->assertNotSame($sentinelHandler, $handlerProperty->getValue());
+ $this->assertInstanceOf(
+ $expectedDumper,
+ (new ReflectionClass($provider))->getProperty('dumper')->getValue($provider),
+ );
+ } finally {
+ unset($_SERVER['VAR_DUMPER_FORMAT']);
+ VarDumper::setHandler($originalHandler);
+
+ if ($originalFormatExists) {
+ $_SERVER['VAR_DUMPER_FORMAT'] = $originalFormat;
+ }
+ }
+ }
+
+ public static function explicitDumperFormats(): array
+ {
+ return [
+ 'CLI' => ['cli', CliDumper::class],
+ 'HTML' => ['html', HtmlDumper::class],
+ ];
+ }
+
+ public function testReloadConfigurationUpdatesRetainedDumper(): void
+ {
+ $provider = $this->app->getProvider(FoundationServiceProvider::class);
+ $reflection = new ReflectionClass($provider);
+ $dumper = $reflection->getProperty('dumper')->getValue($provider);
+
+ config(['view.compiled' => '/tmp/reloaded-compiled-views']);
+
+ $provider->reloadConfiguration();
+
+ $this->assertSame($dumper, $reflection->getProperty('dumper')->getValue($provider));
+ $this->assertSame(
+ '/tmp/reloaded-compiled-views',
+ (new ReflectionClass($dumper))->getProperty('compiledViewPath')->getValue($dumper),
+ );
+ }
}
diff --git a/tests/Foundation/StaticStateTest.php b/tests/Foundation/StaticStateTest.php
index e1ae215bf..250bb5036 100644
--- a/tests/Foundation/StaticStateTest.php
+++ b/tests/Foundation/StaticStateTest.php
@@ -7,16 +7,16 @@
use Hypervel\Container\Container;
use Hypervel\Foundation\Application;
use Hypervel\Foundation\Bootstrap\LoadConfiguration;
+use Hypervel\Foundation\Console\CliDumper;
use Hypervel\Foundation\Console\EventListCommand;
use Hypervel\Foundation\Console\VendorPublishCommand;
-use Hypervel\Foundation\Exceptions\Renderer\Frame;
use Hypervel\Tests\TestCase;
use ReflectionClass;
-use Symfony\Component\ErrorHandler\Exception\FlattenException;
+use Symfony\Component\Console\Output\BufferedOutput;
class StaticStateTest extends TestCase
{
- public function testApplicationFlushStateClearsMacros()
+ public function testApplicationFlushStateClearsMacros(): void
{
Application::macro('testMacro', function () {
return 'test';
@@ -29,7 +29,7 @@ public function testApplicationFlushStateClearsMacros()
$this->assertFalse(Application::hasMacro('testMacro'));
}
- public function testApplicationFlushStatePreservesContainerStaticCleanup()
+ public function testApplicationFlushStatePreservesContainerStaticCleanup(): void
{
$container = new class extends Application {
public function fillBuildRecipeCache(string $concrete): void
@@ -54,38 +54,38 @@ public function buildRecipeCache(): array
$this->assertSame([], $container->buildRecipeCache());
}
- public function testLoadConfigurationFlushStateClearsAlwaysUseConfig()
+ public function testLoadConfigurationFlushStateClearsAlwaysUseConfig(): void
{
LoadConfiguration::alwaysUse(fn () => ['app' => ['name' => 'Static Test']]);
$app = new Application;
(new LoadConfiguration)->bootstrap($app);
- $this->assertSame('Static Test', $app['config']['app.name']);
+ $this->assertSame('Static Test', $app->make('config')->string('app.name'));
LoadConfiguration::flushState();
$app = new Application;
(new LoadConfiguration)->bootstrap($app);
- $this->assertSame('Hypervel', $app['config']['app.name']);
+ $this->assertSame('Hypervel', $app->make('config')->string('app.name'));
}
- public function testFrameFlushStateClearsDumpSourceResolver()
+ public function testCliDumperFlushStateClearsDumpSourceResolver(): void
{
- Frame::resolveDumpSourceUsing(fn () => ['/tmp/example.php', 'example.php', 1]);
+ CliDumper::resolveDumpSourceUsing(fn () => ['/tmp/example.php', 'example.php', 1]);
$this->assertSame(
['/tmp/example.php', 'example.php', 1],
- $this->newFrame()->resolveDumpSource()
+ $this->newCliDumper()->resolveDumpSource()
);
- Frame::flushState();
+ CliDumper::flushState();
- $this->assertNull($this->newFrame()->resolveDumpSource());
+ $this->assertNull($this->newCliDumper()->resolveDumpSource());
}
- public function testVendorPublishCommandFlushStateRestoresMigrationDateUpdates()
+ public function testVendorPublishCommandFlushStateRestoresMigrationDateUpdates(): void
{
$property = (new ReflectionClass(VendorPublishCommand::class))->getProperty('updateMigrationDates');
@@ -98,7 +98,7 @@ public function testVendorPublishCommandFlushStateRestoresMigrationDateUpdates()
$this->assertTrue($property->getValue());
}
- public function testEventListCommandFlushStateClearsEventsResolver()
+ public function testEventListCommandFlushStateClearsEventsResolver(): void
{
$property = (new ReflectionClass(EventListCommand::class))->getProperty('eventsResolver');
@@ -111,14 +111,9 @@ public function testEventListCommandFlushStateClearsEventsResolver()
$this->assertNull($property->getValue());
}
- protected function newFrame(): Frame
+ protected function newCliDumper(): CliDumper
{
- return new Frame(
- $this->createStub(FlattenException::class),
- [],
- ['file' => __FILE__, 'line' => 1],
- __DIR__,
- );
+ return new CliDumper(new BufferedOutput, __DIR__, '');
}
}
diff --git a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php
index 68705fc2b..181da9b9c 100644
--- a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php
+++ b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php
@@ -24,15 +24,15 @@
class MakesHttpRequestsTest extends TestCase
{
- public function testFromSetsHeaderAndSession()
+ public function testFromSetsHeaderAndSession(): void
{
$this->from('previous/url');
$this->assertSame('previous/url', $this->defaultHeaders['referer']);
- $this->assertSame('previous/url', $this->app['session']->previousUrl());
+ $this->assertSame('previous/url', $this->app->make('session')->previousUrl());
}
- public function testFromRouteSetsHeaderAndSession()
+ public function testFromRouteSetsHeaderAndSession(): void
{
$router = $this->app->make(Registrar::class);
@@ -41,7 +41,7 @@ public function testFromRouteSetsHeaderAndSession()
$this->fromRoute('previous-url');
$this->assertSame('http://localhost/previous/url', $this->defaultHeaders['referer']);
- $this->assertSame('http://localhost/previous/url', $this->app['session']->previousUrl());
+ $this->assertSame('http://localhost/previous/url', $this->app->make('session')->previousUrl());
}
public function testFromRemoveHeader()
@@ -148,6 +148,27 @@ public function testWithoutAndWithMiddlewareWithParameter()
);
}
+ public function testWithMiddlewareRestoresExistingBinding(): void
+ {
+ $next = fn (string $request): string => $request;
+
+ $this->app->bind(
+ BoundMiddleware::class,
+ fn () => new BoundMiddleware('FromBinding')
+ );
+
+ $this->withoutMiddleware(BoundMiddleware::class);
+ $this->assertInstanceOf(FakeMiddleware::class, $this->app->make(BoundMiddleware::class));
+
+ $this->withMiddleware(BoundMiddleware::class);
+
+ $this->assertTrue($this->app->bound(BoundMiddleware::class));
+ $this->assertSame(
+ 'fooFromBinding',
+ $this->app->make(BoundMiddleware::class)->handle('foo', $next)
+ );
+ }
+
public function testWithCookieSetCookie()
{
$this->withCookie('foo', 'bar');
@@ -614,6 +635,18 @@ public function handle($request, $next)
}
}
+class BoundMiddleware
+{
+ public function __construct(private readonly string $suffix)
+ {
+ }
+
+ public function handle(string $request, callable $next): mixed
+ {
+ return $next($request . $this->suffix);
+ }
+}
+
class TerminatingMiddleware
{
public static $callback;
diff --git a/tests/Foundation/Testing/DatabaseTruncationTest.php b/tests/Foundation/Testing/DatabaseTruncationTest.php
index b186f9b12..9211f423a 100644
--- a/tests/Foundation/Testing/DatabaseTruncationTest.php
+++ b/tests/Foundation/Testing/DatabaseTruncationTest.php
@@ -5,6 +5,7 @@
namespace Hypervel\Tests\Foundation\Testing;
use Hypervel\Config\Repository;
+use Hypervel\Container\Container;
use Hypervel\Contracts\Events\Dispatcher;
use Hypervel\Database\Connection;
use Hypervel\Database\Query\Builder as QueryBuilder;
@@ -18,7 +19,7 @@ class DatabaseTruncationTest extends TestCase
{
use DatabaseTruncation;
- private ?array $app;
+ private ?Container $app;
private ?array $tablesToTruncate = null;
@@ -28,13 +29,14 @@ protected function setUp(): void
{
parent::setUp();
- $this->app['config'] = new Repository([
+ $this->app = new Container;
+ $this->app->instance('config', new Repository([
'database' => [
'migrations' => [
'table' => 'migrations',
],
],
- ]);
+ ]));
}
protected function tearDown(): void
diff --git a/tests/Hashing/HashingServiceProviderTest.php b/tests/Hashing/HashingServiceProviderTest.php
new file mode 100644
index 000000000..56cfeb48e
--- /dev/null
+++ b/tests/Hashing/HashingServiceProviderTest.php
@@ -0,0 +1,43 @@
+ [
+ 'driver' => 'bcrypt',
+ 'bcrypt' => [],
+ 'argon' => [],
+ ],
+ ]);
+ $application->instance('config', $config);
+ $provider = new HashingServiceProvider($application);
+ $provider->register();
+
+ $manager = $application->make('hash');
+ $driver = $application->make('hash.driver');
+ $this->assertInstanceOf(BcryptHasher::class, $driver);
+
+ $config->set('hashing.driver', 'argon2id');
+ $provider->reloadConfiguration();
+
+ $refreshedDriver = $application->make('hash.driver');
+ $this->assertSame($manager, $application->make(HashManager::class));
+ $this->assertNotSame($driver, $refreshedDriver);
+ $this->assertInstanceOf(Argon2IdHasher::class, $refreshedDriver);
+ }
+}
diff --git a/tests/Horizon/HorizonConfigTest.php b/tests/Horizon/HorizonConfigTest.php
index e11467934..4d8f6c2cf 100644
--- a/tests/Horizon/HorizonConfigTest.php
+++ b/tests/Horizon/HorizonConfigTest.php
@@ -57,7 +57,7 @@ public function testOnlyMissingAndBlankNamesUseTheApplicationName(): void
'horizon' => ['name' => $name],
]);
$app = m::mock(Application::class)->makePartial();
- $app->shouldReceive('make')->with('config')->andReturn($config);
+ $app->shouldReceive('make')->with(ConfigRepository::class)->andReturn($config);
(new HorizonServiceProviderForTesting($app))->normalize();
diff --git a/tests/Http/HttpConnectionTest.php b/tests/Http/HttpConnectionTest.php
index a31cf11e6..fe7a39714 100644
--- a/tests/Http/HttpConnectionTest.php
+++ b/tests/Http/HttpConnectionTest.php
@@ -330,6 +330,28 @@ public function testReregisteringAConnectionReplacesItsSharedHandler(): void
$this->assertCount(2, $factory->createdHandlerOptions);
}
+ public function testForgettingConnectionHandlersPreservesPresetsAndRebuildsEveryHandler(): void
+ {
+ $factory = new RecordingHttpConnectionFactory;
+ $apiConfig = ['timeout' => 12];
+ $reportingConfig = ['timeout' => 30];
+ $factory->registerConnection('api', $apiConfig);
+ $factory->registerConnection('reporting', $reportingConfig);
+ $oldApiHandler = $factory->getConnectionHandler('api');
+ $oldReportingHandler = $factory->getConnectionHandler('reporting');
+
+ $this->assertSame($factory, $factory->forgetConnectionHandlers());
+ $this->assertSame($apiConfig, $factory->getConnectionConfig('api'));
+ $this->assertSame($reportingConfig, $factory->getConnectionConfig('reporting'));
+
+ $newApiHandler = $factory->getConnectionHandler('api');
+ $newReportingHandler = $factory->getConnectionHandler('reporting');
+
+ $this->assertNotSame($oldApiHandler, $newApiHandler);
+ $this->assertNotSame($oldReportingHandler, $newReportingHandler);
+ $this->assertCount(4, $factory->createdHandlerOptions);
+ }
+
public function testConcurrentRequestsOwnIsolatedCookieJars(): void
{
$factory = new RecordingHttpConnectionFactory;
diff --git a/tests/Http/HttpServiceProviderTest.php b/tests/Http/HttpServiceProviderTest.php
new file mode 100644
index 000000000..d5e3a9c10
--- /dev/null
+++ b/tests/Http/HttpServiceProviderTest.php
@@ -0,0 +1,56 @@
+shouldReceive('listen')
+ ->once()
+ ->andReturnUsing(function (string $event, callable $listener) use (&$listeners): void {
+ $listeners[$event] = $listener;
+ });
+ $factory = m::mock(Factory::class);
+ $factory->shouldReceive('forgetConnectionHandlers')->once()->andReturnSelf();
+ $application = m::mock(Application::class);
+ $application->shouldReceive('make')->once()->with('events')->andReturn($events);
+ $application->shouldReceive('resolved')->once()->with(Factory::class)->andReturnTrue();
+ $application->shouldReceive('make')->once()->with(Factory::class)->andReturn($factory);
+
+ (new HttpServiceProvider($application))->boot();
+
+ $listeners[BeforeServerFork::class](new BeforeServerFork(m::mock(Server::class)));
+ }
+
+ public function testBeforeForkDoesNotResolveAnUnusedFactory(): void
+ {
+ $listeners = [];
+ $events = m::mock(Dispatcher::class);
+ $events->shouldReceive('listen')
+ ->once()
+ ->andReturnUsing(function (string $event, callable $listener) use (&$listeners): void {
+ $listeners[$event] = $listener;
+ });
+ $application = m::mock(Application::class);
+ $application->shouldReceive('make')->once()->with('events')->andReturn($events);
+ $application->shouldReceive('resolved')->once()->with(Factory::class)->andReturnFalse();
+
+ (new HttpServiceProvider($application))->boot();
+
+ $listeners[BeforeServerFork::class](new BeforeServerFork(m::mock(Server::class)));
+ }
+}
diff --git a/tests/Inertia/ComponentTest.php b/tests/Inertia/ComponentTest.php
index 8afa46453..d01b7b41b 100644
--- a/tests/Inertia/ComponentTest.php
+++ b/tests/Inertia/ComponentTest.php
@@ -201,7 +201,7 @@ public function testComponentsDoNotCreateCachedViewFilesPerRequest(): void
{
Config::set(['inertia.ssr.enabled' => true]);
- $viewCachePath = $this->app['config']['view.compiled'];
+ $viewCachePath = $this->app->make('config')->string('view.compiled');
$view = 'Fallback';
$this->renderView($view, ['page' => self::EXAMPLE_PAGE_OBJECT]);
diff --git a/tests/Inertia/InertiaServiceProviderTest.php b/tests/Inertia/InertiaServiceProviderTest.php
index 1267a6cc5..6287bb788 100644
--- a/tests/Inertia/InertiaServiceProviderTest.php
+++ b/tests/Inertia/InertiaServiceProviderTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Inertia;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Http\Kernel as HttpKernelContract;
use Hypervel\Filesystem\Filesystem;
use Hypervel\Http\Request;
@@ -96,6 +97,40 @@ public function testInertiaViewFinderReusesSuccessfulLookupsWithConfiguredPathsA
$this->assertSame('/inertia-pages/Dashboard.vue', $finder->find('Dashboard'));
}
+ public function testReloadConfigurationRebuildsAResolvedViewFinderFromCurrentConfig(): void
+ {
+ config()->set('inertia.pages.paths', ['/before']);
+ config()->set('inertia.pages.extensions', ['vue']);
+
+ $files = m::mock(Filesystem::class);
+ $files->shouldReceive('exists')->once()->with('/before/Dashboard.vue')->andReturn(true);
+ $files->shouldReceive('exists')->once()->with('/after/Dashboard.jsx')->andReturn(true);
+ $this->app->instance('files', $files);
+ $gateway = $this->app->make(Gateway::class);
+ $finder = $this->app->make('inertia.view-finder');
+
+ $this->assertSame('/before/Dashboard.vue', $finder->find('Dashboard'));
+
+ config()->set('inertia.pages.paths', ['/after']);
+ config()->set('inertia.pages.extensions', ['jsx']);
+
+ (new InertiaServiceProvider($this->app))->reloadConfiguration();
+
+ $reloadedFinder = $this->app->make('inertia.view-finder');
+ $this->assertNotSame($finder, $reloadedFinder);
+ $this->assertSame('/after/Dashboard.jsx', $reloadedFinder->find('Dashboard'));
+ $this->assertSame($gateway, $this->app->make(Gateway::class));
+ }
+
+ public function testReloadConfigurationDoesNotResolveAnUnusedViewFinder(): void
+ {
+ $app = m::mock(ApplicationContract::class);
+ $app->shouldReceive('forgetInstance')->once()->with('inertia.view-finder');
+ $app->shouldNotReceive('make');
+
+ (new InertiaServiceProvider($app))->reloadConfiguration();
+ }
+
public function testInertiaViewFinderDoesNotCacheMisses(): void
{
config()->set('inertia.pages.paths', ['/inertia-pages']);
diff --git a/tests/Integration/Cache/Redis/PhpRedisCacheFunnelTest.php b/tests/Integration/Cache/Redis/PhpRedisCacheFunnelTest.php
index f655c08ab..a4a5d3dcf 100644
--- a/tests/Integration/Cache/Redis/PhpRedisCacheFunnelTest.php
+++ b/tests/Integration/Cache/Redis/PhpRedisCacheFunnelTest.php
@@ -148,14 +148,15 @@ public function testFunnelReleasesSlotWithSerializationAndCompression(): void
*/
protected function configureLockConnection(array $options): void
{
- $baseConfig = $this->app['config']->get('database.redis.default');
+ $config = $this->app->make('config');
+ $baseConfig = $config->array('database.redis.default');
- $this->app['config']->set('database.redis.lock-test', array_merge($baseConfig, [
+ $config->set('database.redis.lock-test', array_merge($baseConfig, [
'options' => $options,
]));
- $this->app['config']->set('cache.stores.redis.connection', 'default');
- $this->app['config']->set('cache.stores.redis.lock_connection', 'lock-test');
+ $config->set('cache.stores.redis.connection', 'default');
+ $config->set('cache.stores.redis.lock_connection', 'lock-test');
Cache::forgetDriver('redis');
}
diff --git a/tests/Integration/Cache/Redis/PhpRedisCacheLockTest.php b/tests/Integration/Cache/Redis/PhpRedisCacheLockTest.php
index 978aeafb9..056d15b56 100644
--- a/tests/Integration/Cache/Redis/PhpRedisCacheLockTest.php
+++ b/tests/Integration/Cache/Redis/PhpRedisCacheLockTest.php
@@ -146,14 +146,15 @@ public function testRedisLockCanBeAcquiredAndReleasedWithSerializationAndCompres
*/
protected function configureLockConnection(array $options): void
{
- $baseConfig = $this->app['config']->get('database.redis.default');
+ $config = $this->app->make('config');
+ $baseConfig = $config->array('database.redis.default');
- $this->app['config']->set('database.redis.lock-test', array_merge($baseConfig, [
+ $config->set('database.redis.lock-test', array_merge($baseConfig, [
'options' => $options,
]));
- $this->app['config']->set('cache.stores.redis.connection', 'default');
- $this->app['config']->set('cache.stores.redis.lock_connection', 'lock-test');
+ $config->set('cache.stores.redis.connection', 'default');
+ $config->set('cache.stores.redis.lock_connection', 'lock-test');
Cache::forgetDriver('redis');
}
diff --git a/tests/Integration/Cache/Redis/RedisCacheLockTest.php b/tests/Integration/Cache/Redis/RedisCacheLockTest.php
index 782c66015..104fc0ce5 100644
--- a/tests/Integration/Cache/Redis/RedisCacheLockTest.php
+++ b/tests/Integration/Cache/Redis/RedisCacheLockTest.php
@@ -31,7 +31,7 @@ public function testRedisLocksCanBeAcquiredAndReleased(): void
public function testRedisLockCanHaveASeparateConnection(): void
{
- $this->app['config']->set('cache.stores.redis.lock_connection', 'default');
+ $this->app->make('config')->set('cache.stores.redis.lock_connection', 'default');
$this->assertSame('default', Cache::store('redis')->lock('foo')->getConnectionName());
}
diff --git a/tests/Integration/Console/CallbackSchedulingTest.php b/tests/Integration/Console/CallbackSchedulingTest.php
index 0a7210981..62a23e694 100644
--- a/tests/Integration/Console/CallbackSchedulingTest.php
+++ b/tests/Integration/Console/CallbackSchedulingTest.php
@@ -71,9 +71,9 @@ public function testCallbacksCannotRunInBackground()
->runInBackground();
}
- public function testExceptionHandlingInCallback()
+ public function testExceptionHandlingInCallback(): void
{
- $this->app['config']->set('logging.default', 'null');
+ $this->app->make('config')->set('logging.default', 'null');
$event = $this->app->make(Schedule::class)
->call($this->logger('call'))
diff --git a/tests/Integration/Console/CommandEventsTest.php b/tests/Integration/Console/CommandEventsTest.php
index 16c404f0a..e15febc19 100644
--- a/tests/Integration/Console/CommandEventsTest.php
+++ b/tests/Integration/Console/CommandEventsTest.php
@@ -28,11 +28,11 @@ protected function setUp(): void
}
#[DataProvider('foregroundCommandEventsProvider')]
- public function testCommandEventsReceiveParsedInput($callback)
+ public function testCommandEventsReceiveParsedInput($callback): void
{
- $this->app[ConsoleKernel::class]->registerCommand(new TestCommand);
+ $this->app->make(ConsoleKernel::class)->registerCommand(new TestCommand);
- $this->app[Dispatcher::class]->listen(function (CommandStarting $event) {
+ $this->app->make(Dispatcher::class)->listen(function (CommandStarting $event) {
$this->log[] = 'CommandStarting';
$this->log[] = $event->input->getArgument('firstname');
$this->log[] = $event->input->getArgument('lastname');
@@ -54,7 +54,7 @@ public function testCommandEventsReceiveParsedInput($callback)
], $this->log);
}
- public static function foregroundCommandEventsProvider()
+ public static function foregroundCommandEventsProvider(): iterable
{
yield 'Foreground with array' => [function ($testCase) {
$testCase->artisan(TestCommand::class, [
@@ -69,23 +69,25 @@ public static function foregroundCommandEventsProvider()
}];
}
- public function testCommandEventsReceiveParsedInputViaKernelCall()
+ public function testCommandEventsReceiveParsedInputViaKernelCall(): void
{
- $this->app[Dispatcher::class]->listen(function (CommandStarting $event) {
+ $events = $this->app->make(Dispatcher::class);
+
+ $events->listen(function (CommandStarting $event) {
$this->log[] = 'CommandStarting';
$this->log[] = $event->input->getArgument('firstname');
$this->log[] = $event->input->getArgument('lastname');
$this->log[] = $event->input->getOption('occupation');
});
- $this->app[Dispatcher::class]->listen(function (CommandFinished $event) {
+ $events->listen(function (CommandFinished $event) {
$this->log[] = 'CommandFinished';
$this->log[] = $event->input->getArgument('firstname');
$this->log[] = $event->input->getArgument('lastname');
$this->log[] = $event->input->getOption('occupation');
});
- $kernel = $this->app[ConsoleKernel::class];
+ $kernel = $this->app->make(ConsoleKernel::class);
$kernel->registerCommand(new TestCommand);
$kernel->call(TestCommand::class, [
@@ -105,8 +107,7 @@ class TestCommand extends Command
{
protected ?string $signature = 'command-events-test-command {firstname} {lastname} {--occupation=cook}';
- public function handle()
+ public function handle(): void
{
- // ...
}
}
diff --git a/tests/Integration/Console/ConsoleApplicationTest.php b/tests/Integration/Console/ConsoleApplicationTest.php
index b84ac61dc..2aad3d1af 100644
--- a/tests/Integration/Console/ConsoleApplicationTest.php
+++ b/tests/Integration/Console/ConsoleApplicationTest.php
@@ -85,11 +85,11 @@ public function testArtisanWithMockCallAfterCallNow()
$mock->assertExitCode(0);
}
- public function testArtisanInstantiateScheduleWhenNeed()
+ public function testArtisanInstantiateScheduleWhenNeed(): void
{
$this->assertFalse($this->app->resolved(Schedule::class));
- $this->app[Kernel::class]->registerCommand(new ScheduleCommand);
+ $this->app->make(Kernel::class)->registerCommand(new ScheduleCommand);
$this->assertFalse($this->app->resolved(Schedule::class));
@@ -98,11 +98,11 @@ public function testArtisanInstantiateScheduleWhenNeed()
$this->assertTrue($this->app->resolved(Schedule::class));
}
- public function testArtisanQueue()
+ public function testArtisanQueue(): void
{
Queue::fake();
- $this->app[Kernel::class]->queue('foo:bar', [
+ $this->app->make(Kernel::class)->queue('foo:bar', [
'id' => 1,
]);
diff --git a/tests/Integration/Console/PromptsAssertionTest.php b/tests/Integration/Console/PromptsAssertionTest.php
index 95fc71b3d..8059d0c22 100644
--- a/tests/Integration/Console/PromptsAssertionTest.php
+++ b/tests/Integration/Console/PromptsAssertionTest.php
@@ -23,7 +23,7 @@ class PromptsAssertionTest extends TestCase
{
public function testAssertionForTextPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:text';
@@ -44,7 +44,7 @@ public function handle(): void
public function testAssertionForPausePrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class($this) extends Command {
protected ?string $signature = 'test:pause';
@@ -68,7 +68,7 @@ public function handle(): void
public function testAssertionForTextareaPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:textarea';
@@ -89,7 +89,7 @@ public function handle(): void
public function testAssertionForSuggestPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:suggest';
@@ -110,7 +110,7 @@ public function handle(): void
public function testAssertionForPasswordPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:password';
@@ -131,7 +131,7 @@ public function handle(): void
public function testAssertionForConfirmPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:confirm';
@@ -161,7 +161,7 @@ public function handle(): void
public function testAssertionForSelectPromptWithAList(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:select';
@@ -185,7 +185,7 @@ public function handle(): void
public function testAssertionForSelectPromptWithAnAssociativeArray(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:select';
@@ -209,7 +209,7 @@ public function handle(): void
public function testAlternativeAssertionForSelectPromptWithAnAssociativeArray(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:select';
@@ -233,7 +233,7 @@ public function handle(): void
public function testAssertionForRequiredMultiselectPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:multiselect';
@@ -258,7 +258,7 @@ public function handle(): void
public function testAssertionForOptionalMultiselectPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:multiselect';
@@ -291,7 +291,7 @@ public function handle(): void
public function testAssertionForSearchPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:search';
@@ -319,7 +319,7 @@ public function handle(): void
public function testAssertionForMultisearchPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:multisearch';
@@ -356,7 +356,7 @@ public function handle(): void
public function testAssertionForSelectPromptFollowedByMultisearchPrompt(): void
{
- $this->app[Kernel::class]->registerCommand(
+ $this->app->make(Kernel::class)->registerCommand(
new class extends Command {
protected ?string $signature = 'test:select';
diff --git a/tests/Integration/Console/PromptsValidationTest.php b/tests/Integration/Console/PromptsValidationTest.php
index 829b48990..17d417c1c 100644
--- a/tests/Integration/Console/PromptsValidationTest.php
+++ b/tests/Integration/Console/PromptsValidationTest.php
@@ -16,10 +16,12 @@ protected function setUp(): void
{
parent::setUp();
- $this->app[Kernel::class]->registerCommand(new ClosureValidationCommand);
- $this->app[Kernel::class]->registerCommand(new LaravelRulesCommand);
- $this->app[Kernel::class]->registerCommand(new MethodMessagesCommand);
- $this->app[Kernel::class]->registerCommand(new InlineMessagesCommand);
+ $kernel = $this->app->make(Kernel::class);
+
+ $kernel->registerCommand(new ClosureValidationCommand);
+ $kernel->registerCommand(new LaravelRulesCommand);
+ $kernel->registerCommand(new MethodMessagesCommand);
+ $kernel->registerCommand(new InlineMessagesCommand);
}
public function testValidationForPrompts(): void
diff --git a/tests/Integration/Database/DatabaseLockTest.php b/tests/Integration/Database/DatabaseLockTest.php
index 8f93d7eb8..c124fbd6a 100644
--- a/tests/Integration/Database/DatabaseLockTest.php
+++ b/tests/Integration/Database/DatabaseLockTest.php
@@ -22,8 +22,10 @@ class DatabaseLockTest extends DatabaseTestCase
{
public function testLockCanHaveASeparateConnection(): void
{
- $this->app['config']->set('cache.stores.database.lock_connection', 'test');
- $this->app['config']->set('database.connections.test', $this->app['config']->get('database.connections.testing'));
+ $config = $this->app->make('config');
+
+ $config->set('cache.stores.database.lock_connection', 'test');
+ $config->set('database.connections.test', $config->array('database.connections.testing'));
$this->assertSame('test', Cache::driver('database')->lock('foo')->getConnectionName());
}
diff --git a/tests/Integration/Database/EloquentStrictLoadingTest.php b/tests/Integration/Database/EloquentStrictLoadingTest.php
index 91e299216..504473d63 100644
--- a/tests/Integration/Database/EloquentStrictLoadingTest.php
+++ b/tests/Integration/Database/EloquentStrictLoadingTest.php
@@ -71,10 +71,8 @@ public function testStrictModeDoesntThrowAnExceptionOnAttributes()
$this->assertNull($models[0]->number);
}
- public function testStrictModeDoesntThrowAnExceptionOnEagerLoading()
+ public function testStrictModeDoesntThrowAnExceptionOnEagerLoading(): void
{
- $this->app['config']->set('database.connections.testing.zxc', false);
-
EloquentStrictLoadingTestModel1::create();
EloquentStrictLoadingTestModel1::create();
diff --git a/tests/Integration/Database/MariaDb/DatabaseEmulatePreparesMariaDbConnectionTest.php b/tests/Integration/Database/MariaDb/DatabaseEmulatePreparesMariaDbConnectionTest.php
index e851d3a57..b5d897992 100755
--- a/tests/Integration/Database/MariaDb/DatabaseEmulatePreparesMariaDbConnectionTest.php
+++ b/tests/Integration/Database/MariaDb/DatabaseEmulatePreparesMariaDbConnectionTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database\MariaDb;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use PDO;
use PHPUnit\Framework\Attributes\RequiresOperatingSystem;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
@@ -12,11 +13,11 @@
#[RequiresPhpExtension('pdo_mysql')]
class DatabaseEmulatePreparesMariaDbConnectionTest extends DatabaseMariaDbConnectionTest
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('database.connections.mariadb.options', [
+ $app->make('config')->set('database.connections.mariadb.options', [
PDO::ATTR_EMULATE_PREPARES => true,
]);
}
diff --git a/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php b/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php
index 674353cc5..ec8b6ff67 100644
--- a/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php
+++ b/tests/Integration/Database/MariaDb/DatabaseMariaDbSchemaBuilderTest.php
@@ -14,7 +14,7 @@
#[RequiresPhpExtension('pdo_mysql')]
class DatabaseMariaDbSchemaBuilderTest extends MariaDbTestCase
{
- public function testAddCommentToTable()
+ public function testAddCommentToTable(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
@@ -22,7 +22,7 @@ public function testAddCommentToTable()
});
$tableInfo = DB::table('information_schema.tables')
- ->where('table_schema', $this->app['config']->get('database.connections.mariadb.database'))
+ ->where('table_schema', $this->app->make('config')->string('database.connections.mariadb.database'))
->where('table_name', 'users')
->select('table_comment as table_comment')
->first();
diff --git a/tests/Integration/Database/MariaDb/EscapeTest.php b/tests/Integration/Database/MariaDb/EscapeTest.php
index 04b419c3b..6cbc070e3 100644
--- a/tests/Integration/Database/MariaDb/EscapeTest.php
+++ b/tests/Integration/Database/MariaDb/EscapeTest.php
@@ -12,62 +12,72 @@
#[RequiresPhpExtension('pdo_mysql')]
class EscapeTest extends MariaDbTestCase
{
- public function testEscapeInt()
+ public function testEscapeInt(): void
{
- $this->assertSame('42', $this->app['db']->escape(42));
- $this->assertSame('-6', $this->app['db']->escape(-6));
+ $database = $this->app->make('db');
+
+ $this->assertSame('42', $database->escape(42));
+ $this->assertSame('-6', $database->escape(-6));
}
- public function testEscapeFloat()
+ public function testEscapeFloat(): void
{
- $this->assertSame('3.14159', $this->app['db']->escape(3.14159));
- $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159));
+ $database = $this->app->make('db');
+
+ $this->assertSame('3.14159', $database->escape(3.14159));
+ $this->assertSame('-3.14159', $database->escape(-3.14159));
}
- public function testEscapeBool()
+ public function testEscapeBool(): void
{
- $this->assertSame('1', $this->app['db']->escape(true));
- $this->assertSame('0', $this->app['db']->escape(false));
+ $database = $this->app->make('db');
+
+ $this->assertSame('1', $database->escape(true));
+ $this->assertSame('0', $database->escape(false));
}
- public function testEscapeNull()
+ public function testEscapeNull(): void
{
- $this->assertSame('null', $this->app['db']->escape(null));
- $this->assertSame('null', $this->app['db']->escape(null, true));
+ $database = $this->app->make('db');
+
+ $this->assertSame('null', $database->escape(null));
+ $this->assertSame('null', $database->escape(null, true));
}
- public function testEscapeBinary()
+ public function testEscapeBinary(): void
{
- $this->assertSame("x'dead00beef'", $this->app['db']->escape(hex2bin('dead00beef'), true));
+ $this->assertSame("x'dead00beef'", $this->app->make('db')->escape(hex2bin('dead00beef'), true));
}
- public function testEscapeString()
+ public function testEscapeString(): void
{
- $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647'));
- $this->assertSame("'true'", $this->app['db']->escape('true'));
- $this->assertSame("'false'", $this->app['db']->escape('false'));
- $this->assertSame("'null'", $this->app['db']->escape('null'));
- $this->assertSame("'Hello\\'World'", $this->app['db']->escape("Hello'World"));
+ $database = $this->app->make('db');
+
+ $this->assertSame("'2147483647'", $database->escape('2147483647'));
+ $this->assertSame("'true'", $database->escape('true'));
+ $this->assertSame("'false'", $database->escape('false'));
+ $this->assertSame("'null'", $database->escape('null'));
+ $this->assertSame("'Hello\\'World'", $database->escape("Hello'World"));
}
- public function testEscapeStringInvalidUtf8()
+ public function testEscapeStringInvalidUtf8(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte");
+ $this->app->make('db')->escape("I am hiding an invalid \x80 utf-8 continuation byte");
}
- public function testEscapeStringNullByte()
+ public function testEscapeStringNullByte(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding a \00 byte");
+ $this->app->make('db')->escape("I am hiding a \00 byte");
}
- public function testEscapeArray()
+ public function testEscapeArray(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape(['a', 'b']);
+ $this->app->make('db')->escape(['a', 'b']);
}
}
diff --git a/tests/Integration/Database/MigrateWithRealpathTest.php b/tests/Integration/Database/MigrateWithRealpathTest.php
index 356a345a5..85aff2f08 100644
--- a/tests/Integration/Database/MigrateWithRealpathTest.php
+++ b/tests/Integration/Database/MigrateWithRealpathTest.php
@@ -13,7 +13,7 @@ protected function setUp(): void
{
parent::setUp();
- if ($this->app['config']->get('database.default') !== 'testing') {
+ if ($this->app->make('config')->string('database.default') !== 'testing') {
$this->artisan('db:wipe', ['--drop-views' => true]);
}
diff --git a/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php b/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php
index 07611b2fe..73797c4b1 100755
--- a/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php
+++ b/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database\MySql;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use PDO;
use PHPUnit\Framework\Attributes\RequiresOperatingSystem;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
@@ -12,11 +13,11 @@
#[RequiresPhpExtension('pdo_mysql')]
class DatabaseEmulatePreparesMySqlConnectionTest extends DatabaseMySqlConnectionTest
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('database.connections.mysql.options', [
+ $app->make('config')->set('database.connections.mysql.options', [
PDO::ATTR_EMULATE_PREPARES => true,
]);
}
diff --git a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php
index d2b4dcd8c..1fe3c7d19 100644
--- a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php
+++ b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php
@@ -15,7 +15,7 @@
#[RequiresPhpExtension('pdo_mysql')]
class DatabaseMySqlSchemaBuilderTest extends MySqlTestCase
{
- public function testAddCommentToTable()
+ public function testAddCommentToTable(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
@@ -23,7 +23,7 @@ public function testAddCommentToTable()
});
$tableInfo = DB::table('information_schema.tables')
- ->where('table_schema', $this->app['config']->get('database.connections.mysql.database'))
+ ->where('table_schema', $this->app->make('config')->string('database.connections.mysql.database'))
->where('table_name', 'users')
->select('table_comment as table_comment')
->first();
diff --git a/tests/Integration/Database/MySql/EscapeTest.php b/tests/Integration/Database/MySql/EscapeTest.php
index b655944bd..496835cd8 100644
--- a/tests/Integration/Database/MySql/EscapeTest.php
+++ b/tests/Integration/Database/MySql/EscapeTest.php
@@ -12,62 +12,72 @@
#[RequiresPhpExtension('pdo_mysql')]
class EscapeTest extends MySqlTestCase
{
- public function testEscapeInt()
+ public function testEscapeInt(): void
{
- $this->assertSame('42', $this->app['db']->escape(42));
- $this->assertSame('-6', $this->app['db']->escape(-6));
+ $database = $this->app->make('db');
+
+ $this->assertSame('42', $database->escape(42));
+ $this->assertSame('-6', $database->escape(-6));
}
- public function testEscapeFloat()
+ public function testEscapeFloat(): void
{
- $this->assertSame('3.14159', $this->app['db']->escape(3.14159));
- $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159));
+ $database = $this->app->make('db');
+
+ $this->assertSame('3.14159', $database->escape(3.14159));
+ $this->assertSame('-3.14159', $database->escape(-3.14159));
}
- public function testEscapeBool()
+ public function testEscapeBool(): void
{
- $this->assertSame('1', $this->app['db']->escape(true));
- $this->assertSame('0', $this->app['db']->escape(false));
+ $database = $this->app->make('db');
+
+ $this->assertSame('1', $database->escape(true));
+ $this->assertSame('0', $database->escape(false));
}
- public function testEscapeNull()
+ public function testEscapeNull(): void
{
- $this->assertSame('null', $this->app['db']->escape(null));
- $this->assertSame('null', $this->app['db']->escape(null, true));
+ $database = $this->app->make('db');
+
+ $this->assertSame('null', $database->escape(null));
+ $this->assertSame('null', $database->escape(null, true));
}
- public function testEscapeBinary()
+ public function testEscapeBinary(): void
{
- $this->assertSame("x'dead00beef'", $this->app['db']->escape(hex2bin('dead00beef'), true));
+ $this->assertSame("x'dead00beef'", $this->app->make('db')->escape(hex2bin('dead00beef'), true));
}
- public function testEscapeString()
+ public function testEscapeString(): void
{
- $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647'));
- $this->assertSame("'true'", $this->app['db']->escape('true'));
- $this->assertSame("'false'", $this->app['db']->escape('false'));
- $this->assertSame("'null'", $this->app['db']->escape('null'));
- $this->assertSame("'Hello\\'World'", $this->app['db']->escape("Hello'World"));
+ $database = $this->app->make('db');
+
+ $this->assertSame("'2147483647'", $database->escape('2147483647'));
+ $this->assertSame("'true'", $database->escape('true'));
+ $this->assertSame("'false'", $database->escape('false'));
+ $this->assertSame("'null'", $database->escape('null'));
+ $this->assertSame("'Hello\\'World'", $database->escape("Hello'World"));
}
- public function testEscapeStringInvalidUtf8()
+ public function testEscapeStringInvalidUtf8(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte");
+ $this->app->make('db')->escape("I am hiding an invalid \x80 utf-8 continuation byte");
}
- public function testEscapeStringNullByte()
+ public function testEscapeStringNullByte(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding a \00 byte");
+ $this->app->make('db')->escape("I am hiding a \00 byte");
}
- public function testEscapeArray()
+ public function testEscapeArray(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape(['a', 'b']);
+ $this->app->make('db')->escape(['a', 'b']);
}
}
diff --git a/tests/Integration/Database/Postgres/EscapeTest.php b/tests/Integration/Database/Postgres/EscapeTest.php
index 120dc46c6..7ccf7981f 100644
--- a/tests/Integration/Database/Postgres/EscapeTest.php
+++ b/tests/Integration/Database/Postgres/EscapeTest.php
@@ -12,62 +12,72 @@
#[RequiresPhpExtension('pdo_pgsql')]
class EscapeTest extends PostgresTestCase
{
- public function testEscapeInt()
+ public function testEscapeInt(): void
{
- $this->assertSame('42', $this->app['db']->escape(42));
- $this->assertSame('-6', $this->app['db']->escape(-6));
+ $database = $this->app->make('db');
+
+ $this->assertSame('42', $database->escape(42));
+ $this->assertSame('-6', $database->escape(-6));
}
- public function testEscapeFloat()
+ public function testEscapeFloat(): void
{
- $this->assertSame('3.14159', $this->app['db']->escape(3.14159));
- $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159));
+ $database = $this->app->make('db');
+
+ $this->assertSame('3.14159', $database->escape(3.14159));
+ $this->assertSame('-3.14159', $database->escape(-3.14159));
}
- public function testEscapeBool()
+ public function testEscapeBool(): void
{
- $this->assertSame('true', $this->app['db']->escape(true));
- $this->assertSame('false', $this->app['db']->escape(false));
+ $database = $this->app->make('db');
+
+ $this->assertSame('true', $database->escape(true));
+ $this->assertSame('false', $database->escape(false));
}
- public function testEscapeNull()
+ public function testEscapeNull(): void
{
- $this->assertSame('null', $this->app['db']->escape(null));
- $this->assertSame('null', $this->app['db']->escape(null, true));
+ $database = $this->app->make('db');
+
+ $this->assertSame('null', $database->escape(null));
+ $this->assertSame('null', $database->escape(null, true));
}
- public function testEscapeBinary()
+ public function testEscapeBinary(): void
{
- $this->assertSame("'\\xdead00beef'::bytea", $this->app['db']->escape(hex2bin('dead00beef'), true));
+ $this->assertSame("'\\xdead00beef'::bytea", $this->app->make('db')->escape(hex2bin('dead00beef'), true));
}
- public function testEscapeString()
+ public function testEscapeString(): void
{
- $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647'));
- $this->assertSame("'true'", $this->app['db']->escape('true'));
- $this->assertSame("'false'", $this->app['db']->escape('false'));
- $this->assertSame("'null'", $this->app['db']->escape('null'));
- $this->assertSame("'Hello''World'", $this->app['db']->escape("Hello'World"));
+ $database = $this->app->make('db');
+
+ $this->assertSame("'2147483647'", $database->escape('2147483647'));
+ $this->assertSame("'true'", $database->escape('true'));
+ $this->assertSame("'false'", $database->escape('false'));
+ $this->assertSame("'null'", $database->escape('null'));
+ $this->assertSame("'Hello''World'", $database->escape("Hello'World"));
}
- public function testEscapeStringInvalidUtf8()
+ public function testEscapeStringInvalidUtf8(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte");
+ $this->app->make('db')->escape("I am hiding an invalid \x80 utf-8 continuation byte");
}
- public function testEscapeStringNullByte()
+ public function testEscapeStringNullByte(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding a \00 byte");
+ $this->app->make('db')->escape("I am hiding a \00 byte");
}
- public function testEscapeArray()
+ public function testEscapeArray(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape(['a', 'b']);
+ $this->app->make('db')->escape(['a', 'b']);
}
}
diff --git a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php
index aaf964e2a..4faab8c6f 100644
--- a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php
+++ b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php
@@ -18,11 +18,11 @@
#[RequiresPhpExtension('pdo_pgsql')]
class PostgresSchemaBuilderTest extends PostgresTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(Application $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('database.connections.pgsql.search_path', 'public,private');
+ $app->make('config')->set('database.connections.pgsql.search_path', 'public,private');
}
/**
diff --git a/tests/Integration/Database/Postgres/PostgresStartupOptionsTest.php b/tests/Integration/Database/Postgres/PostgresStartupOptionsTest.php
index c82ae3850..737ecaf90 100644
--- a/tests/Integration/Database/Postgres/PostgresStartupOptionsTest.php
+++ b/tests/Integration/Database/Postgres/PostgresStartupOptionsTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database\Postgres;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Support\Facades\DB;
use PHPUnit\Framework\Attributes\RequiresOperatingSystem;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
@@ -28,21 +29,22 @@
#[RequiresPhpExtension('pdo_pgsql')]
class PostgresStartupOptionsTest extends PostgresTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $base = $app['config']->get('database.connections.pgsql');
+ $config = $app->make('config');
+ $base = $config->array('database.connections.pgsql');
- $app['config']->set('database.connections.pgsql_startup_search_path', array_merge($base, [
+ $config->set('database.connections.pgsql_startup_search_path', array_merge($base, [
'search_path' => 'public,private',
]));
- $app['config']->set('database.connections.pgsql_startup_isolation', array_merge($base, [
+ $config->set('database.connections.pgsql_startup_isolation', array_merge($base, [
'isolation_level' => 'read committed',
]));
- $app['config']->set('database.connections.pgsql_startup_combined', array_merge($base, [
+ $config->set('database.connections.pgsql_startup_combined', array_merge($base, [
'search_path' => 'public,private',
'timezone' => 'UTC',
'isolation_level' => 'read committed',
diff --git a/tests/Integration/Database/RefreshCommandTest.php b/tests/Integration/Database/RefreshCommandTest.php
index c15c3176d..ab58cb0be 100644
--- a/tests/Integration/Database/RefreshCommandTest.php
+++ b/tests/Integration/Database/RefreshCommandTest.php
@@ -29,9 +29,9 @@ public function testRefreshWithRealpath()
$this->migrateRefreshWith($options);
}
- private function migrateRefreshWith(array $options)
+ private function migrateRefreshWith(array $options): void
{
- if ($this->app['config']->get('database.default') !== 'testing') {
+ if ($this->app->make('config')->get('database.default') !== 'testing') {
$this->artisan('db:wipe', ['--drop-views' => true]);
}
@@ -41,9 +41,9 @@ private function migrateRefreshWith(array $options)
$this->artisan('migrate:refresh', $options);
DB::table('members')->insert(['name' => 'foo', 'email' => 'foo@bar', 'password' => 'secret']);
- $this->assertEquals(1, DB::table('members')->count());
+ $this->assertSame(1, DB::table('members')->count());
$this->artisan('migrate:refresh', $options);
- $this->assertEquals(0, DB::table('members')->count());
+ $this->assertSame(0, DB::table('members')->count());
}
}
diff --git a/tests/Integration/Database/SchemaBuilderSchemaNameTest.php b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php
index c35a07254..fa008ce77 100644
--- a/tests/Integration/Database/SchemaBuilderSchemaNameTest.php
+++ b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\Schema\Blueprint;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Schema;
@@ -45,17 +46,18 @@ protected function destroyDatabaseMigrations(): void
}
}
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $connection = $app['config']->get('database.default');
+ $config = $app->make('config');
+ $connection = $config->string('database.default');
- $app['config']->set("database.connections.{$connection}.prefix_indexes", true);
- $app['config']->set('database.connections.pgsql.search_path', 'public,my_schema');
- $app['config']->set('database.connections.without-prefix', $app['config']->get('database.connections.' . $connection));
- $app['config']->set('database.connections.with-prefix', $app['config']->get('database.connections.without-prefix'));
- $app['config']->set('database.connections.with-prefix.prefix', 'example_');
+ $config->set("database.connections.{$connection}.prefix_indexes", true);
+ $config->set('database.connections.pgsql.search_path', 'public,my_schema');
+ $config->set('database.connections.without-prefix', $config->array('database.connections.' . $connection));
+ $config->set('database.connections.with-prefix', $config->array('database.connections.without-prefix'));
+ $config->set('database.connections.with-prefix.prefix', 'example_');
}
#[DataProvider('connectionProvider')]
diff --git a/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php b/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php
index 0a0bcba98..788aa23f8 100644
--- a/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php
+++ b/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php
@@ -6,6 +6,7 @@
use Closure;
use Exception;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\QueryException;
use Hypervel\Database\Schema\Blueprint;
use Hypervel\Support\Facades\DB;
@@ -15,9 +16,9 @@
class DatabaseSchemaBlueprintTest extends SqliteTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('database.connections.sqlite.foreign_key_constraints', false);
+ $app->make('config')->set('database.connections.sqlite.foreign_key_constraints', false);
}
protected function setUpInCoroutine(): void
diff --git a/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php b/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php
index 9640b6d52..212e73ed2 100644
--- a/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php
+++ b/tests/Integration/Database/Sqlite/DatabaseSchemaBuilderTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database\Sqlite;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\Query\Expression;
use Hypervel\Database\Schema\Blueprint;
use Hypervel\Support\Facades\DB;
@@ -22,9 +23,9 @@ protected function setUpInCoroutine(): void
$this->artisan('migrate:install', ['--database' => 'sqlite-with-indexed-prefix']);
}
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set([
+ $app->make('config')->set([
'database.connections.sqlite-with-prefix' => [
'driver' => 'sqlite',
'database' => ':memory:',
diff --git a/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php b/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php
index 6683f7fc5..a8b760b67 100644
--- a/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php
+++ b/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database\Sqlite;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\Schema\Blueprint;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Schema;
@@ -11,13 +12,14 @@
class DatabaseSqliteConnectionTest extends SqliteTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('database.default', 'conn1');
+ $config = $app->make('config');
+ $config->set('database.default', 'conn1');
- $app['config']->set('database.connections.conn1', [
+ $config->set('database.connections.conn1', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
diff --git a/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php b/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php
index c715cf03c..c4cf4ac84 100644
--- a/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php
+++ b/tests/Integration/Database/Sqlite/DatabaseSqliteSchemaBuilderTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database\Sqlite;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\Schema\Blueprint;
use Hypervel\Database\SQLiteConnection;
use Hypervel\Filesystem\Filesystem;
@@ -18,13 +19,14 @@
class DatabaseSqliteSchemaBuilderTest extends SqliteTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('database.default', 'conn1');
+ $config = $app->make('config');
+ $config->set('database.default', 'conn1');
- $app['config']->set('database.connections.conn1', [
+ $config->set('database.connections.conn1', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
diff --git a/tests/Integration/Database/Sqlite/EloquentModelConnectionsTest.php b/tests/Integration/Database/Sqlite/EloquentModelConnectionsTest.php
index dbc9b5214..068d3bcf4 100644
--- a/tests/Integration/Database/Sqlite/EloquentModelConnectionsTest.php
+++ b/tests/Integration/Database/Sqlite/EloquentModelConnectionsTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Database\Sqlite\EloquentModelConnectionsTest;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Database\Eloquent\Relations\BelongsTo;
use Hypervel\Database\Eloquent\Relations\HasMany;
@@ -15,17 +16,18 @@
class EloquentModelConnectionsTest extends SqliteTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('database.default', 'conn1');
+ $config = $app->make('config');
+ $config->set('database.default', 'conn1');
- $app['config']->set('database.connections.conn1', [
+ $config->set('database.connections.conn1', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
- $app['config']->set('database.connections.conn2', [
+ $config->set('database.connections.conn2', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
diff --git a/tests/Integration/Database/Sqlite/EscapeTest.php b/tests/Integration/Database/Sqlite/EscapeTest.php
index cb1f87db4..c989c2681 100644
--- a/tests/Integration/Database/Sqlite/EscapeTest.php
+++ b/tests/Integration/Database/Sqlite/EscapeTest.php
@@ -4,79 +4,91 @@
namespace Hypervel\Tests\Integration\Database\Sqlite;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use RuntimeException;
class EscapeTest extends SqliteTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('database.default', 'conn1');
+ $config = $app->make('config');
+ $config->set('database.default', 'conn1');
- $app['config']->set('database.connections.conn1', [
+ $config->set('database.connections.conn1', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
- public function testEscapeInt()
+ public function testEscapeInt(): void
{
- $this->assertSame('42', $this->app['db']->escape(42));
- $this->assertSame('-6', $this->app['db']->escape(-6));
+ $database = $this->app->make('db');
+
+ $this->assertSame('42', $database->escape(42));
+ $this->assertSame('-6', $database->escape(-6));
}
- public function testEscapeFloat()
+ public function testEscapeFloat(): void
{
- $this->assertSame('3.14159', $this->app['db']->escape(3.14159));
- $this->assertSame('-3.14159', $this->app['db']->escape(-3.14159));
+ $database = $this->app->make('db');
+
+ $this->assertSame('3.14159', $database->escape(3.14159));
+ $this->assertSame('-3.14159', $database->escape(-3.14159));
}
- public function testEscapeBool()
+ public function testEscapeBool(): void
{
- $this->assertSame('1', $this->app['db']->escape(true));
- $this->assertSame('0', $this->app['db']->escape(false));
+ $database = $this->app->make('db');
+
+ $this->assertSame('1', $database->escape(true));
+ $this->assertSame('0', $database->escape(false));
}
- public function testEscapeNull()
+ public function testEscapeNull(): void
{
- $this->assertSame('null', $this->app['db']->escape(null));
- $this->assertSame('null', $this->app['db']->escape(null, true));
+ $database = $this->app->make('db');
+
+ $this->assertSame('null', $database->escape(null));
+ $this->assertSame('null', $database->escape(null, true));
}
- public function testEscapeBinary()
+ public function testEscapeBinary(): void
{
- $this->assertSame("x'dead00beef'", $this->app['db']->escape(hex2bin('dead00beef'), true));
+ $this->assertSame("x'dead00beef'", $this->app->make('db')->escape(hex2bin('dead00beef'), true));
}
- public function testEscapeString()
+ public function testEscapeString(): void
{
- $this->assertSame("'2147483647'", $this->app['db']->escape('2147483647'));
- $this->assertSame("'true'", $this->app['db']->escape('true'));
- $this->assertSame("'false'", $this->app['db']->escape('false'));
- $this->assertSame("'null'", $this->app['db']->escape('null'));
- $this->assertSame("'Hello''World'", $this->app['db']->escape("Hello'World"));
+ $database = $this->app->make('db');
+
+ $this->assertSame("'2147483647'", $database->escape('2147483647'));
+ $this->assertSame("'true'", $database->escape('true'));
+ $this->assertSame("'false'", $database->escape('false'));
+ $this->assertSame("'null'", $database->escape('null'));
+ $this->assertSame("'Hello''World'", $database->escape("Hello'World"));
}
- public function testEscapeStringInvalidUtf8()
+ public function testEscapeStringInvalidUtf8(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding an invalid \x80 utf-8 continuation byte");
+ $this->app->make('db')->escape("I am hiding an invalid \x80 utf-8 continuation byte");
}
- public function testEscapeStringNullByte()
+ public function testEscapeStringNullByte(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape("I am hiding a \00 byte");
+ $this->app->make('db')->escape("I am hiding a \00 byte");
}
- public function testEscapeArray()
+ public function testEscapeArray(): void
{
$this->expectException(RuntimeException::class);
- $this->app['db']->escape(['a', 'b']);
+ $this->app->make('db')->escape(['a', 'b']);
}
}
diff --git a/tests/Integration/Encryption/EncryptionTest.php b/tests/Integration/Encryption/EncryptionTest.php
index ac5a37479..d02034d55 100644
--- a/tests/Integration/Encryption/EncryptionTest.php
+++ b/tests/Integration/Encryption/EncryptionTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Encryption;
+use Hypervel\Contracts\Encryption\DecryptException;
use Hypervel\Encryption\Encrypter;
use Hypervel\Encryption\EncryptionServiceProvider;
use Hypervel\Encryption\MissingAppKeyException;
@@ -83,12 +84,38 @@ public function testEncryptionProviderConfiguresSerializableClosureSigner(): voi
$this->assertInstanceOf(Signed::class, $serializable->__serialize()['serializable']);
}
- public function testEncryptionProviderClearsStaleSerializableClosureSignerWhenKeyIsMissing(): void
+ public function testReloadConfigurationReplacesKeyDerivedEncryptionState(): void
+ {
+ $provider = new EncryptionServiceProvider($this->app);
+ $encrypter = $this->app->make('encrypter');
+ $encrypted = $encrypter->encryptString('value');
+ $closure = static fn (): string => 'value';
+ $signedClosure = serialize(new SerializableClosure($closure));
+
+ config([
+ 'app.key' => 'base64:' . base64_encode(str_repeat('b', 32)),
+ 'app.previous_keys' => [],
+ ]);
+ $provider->reloadConfiguration();
+
+ $refreshedEncrypter = $this->app->make('encrypter');
+ $this->assertNotSame($encrypter, $refreshedEncrypter);
+ $this->assertNotSame($signedClosure, serialize(new SerializableClosure($closure)));
+ $this->assertSame('fresh', $refreshedEncrypter->decryptString(
+ $refreshedEncrypter->encryptString('fresh'),
+ ));
+
+ $this->expectException(DecryptException::class);
+
+ $refreshedEncrypter->decryptString($encrypted);
+ }
+
+ public function testReloadConfigurationClearsStaleSerializableClosureSignerWhenKeyIsMissing(): void
{
SerializableClosure::setSecretKey('stale-key');
$this->app->make('config')->set('app.key', null);
- (new EncryptionServiceProvider($this->app))->register();
+ (new EncryptionServiceProvider($this->app))->reloadConfiguration();
$serializable = new SerializableClosure(static fn (): string => 'value');
diff --git a/tests/Integration/Encryption/KeyGenerateCommandTest.php b/tests/Integration/Encryption/KeyGenerateCommandTest.php
index cf3694208..148925ca7 100644
--- a/tests/Integration/Encryption/KeyGenerateCommandTest.php
+++ b/tests/Integration/Encryption/KeyGenerateCommandTest.php
@@ -43,12 +43,12 @@ protected function tearDown(): void
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('app.cipher', 'aes-128-cbc');
+ $app->make('config')->set('app.cipher', 'aes-128-cbc');
}
- public function testShowOptionDisplaysKeyWithoutModifyingFiles()
+ public function testShowOptionDisplaysKeyWithoutModifyingFiles(): void
{
- $this->app['config']->set('app.key', '');
+ $this->app->make('config')->set('app.key', '');
file_put_contents($this->envDir . '/.env', 'APP_KEY=');
$this->app->useEnvironmentPath($this->envDir);
@@ -61,9 +61,10 @@ public function testShowOptionDisplaysKeyWithoutModifyingFiles()
$this->assertSame('APP_KEY=', file_get_contents($this->envDir . '/.env'));
}
- public function testKeyIsWrittenToEnvFile()
+ public function testKeyIsWrittenToEnvFile(): void
{
- $this->app['config']->set('app.key', '');
+ $config = $this->app->make('config');
+ $config->set('app.key', '');
file_put_contents($this->envDir . '/.env', 'APP_KEY=');
$this->app->useEnvironmentPath($this->envDir);
@@ -76,12 +77,13 @@ public function testKeyIsWrittenToEnvFile()
$this->assertStringStartsWith('APP_KEY=base64:', $envContents);
// Config should also be updated
- $this->assertStringStartsWith('base64:', $this->app['config']['app.key']);
+ $this->assertStringStartsWith('base64:', $config->get('app.key'));
}
public function testKeyIsWrittenToEnvFileWhenCurrentConfigKeyIsNull(): void
{
- $this->app['config']->set('app.key', null);
+ $config = $this->app->make('config');
+ $config->set('app.key', null);
file_put_contents($this->envDir . '/.env', 'APP_KEY=');
$this->app->useEnvironmentPath($this->envDir);
@@ -92,13 +94,13 @@ public function testKeyIsWrittenToEnvFileWhenCurrentConfigKeyIsNull(): void
$envContents = file_get_contents($this->envDir . '/.env');
$this->assertStringStartsWith('APP_KEY=base64:', $envContents);
- $this->assertStringStartsWith('base64:', $this->app['config']['app.key']);
+ $this->assertStringStartsWith('base64:', $config->get('app.key'));
}
- public function testForceOptionBypassesConfirmationInProduction()
+ public function testForceOptionBypassesConfirmationInProduction(): void
{
- $this->app['env'] = 'production';
- $this->app['config']->set('app.key', 'base64:' . base64_encode(str_repeat('a', 16)));
+ $this->app->instance('env', 'production');
+ $this->app->make('config')->set('app.key', 'base64:' . base64_encode(str_repeat('a', 16)));
file_put_contents($this->envDir . '/.env', 'APP_KEY=base64:' . base64_encode(str_repeat('a', 16)));
$this->app->useEnvironmentPath($this->envDir);
@@ -113,9 +115,9 @@ public function testForceOptionBypassesConfirmationInProduction()
$this->assertStringNotContainsString(base64_encode(str_repeat('a', 16)), $envContents);
}
- public function testErrorWhenEnvFileHasNoAppKeyLine()
+ public function testErrorWhenEnvFileHasNoAppKeyLine(): void
{
- $this->app['config']->set('app.key', '');
+ $this->app->make('config')->set('app.key', '');
file_put_contents($this->envDir . '/.env', 'APP_NAME=Hypervel');
$this->app->useEnvironmentPath($this->envDir);
@@ -125,10 +127,11 @@ public function testErrorWhenEnvFileHasNoAppKeyLine()
->assertSuccessful();
}
- public function testGeneratedKeyHasCorrectLengthForCipher()
+ public function testGeneratedKeyHasCorrectLengthForCipher(): void
{
- $this->app['config']->set('app.key', '');
- $this->app['config']->set('app.cipher', 'aes-256-cbc');
+ $config = $this->app->make('config');
+ $config->set('app.key', '');
+ $config->set('app.cipher', 'aes-256-cbc');
file_put_contents($this->envDir . '/.env', 'APP_KEY=');
$this->app->useEnvironmentPath($this->envDir);
@@ -146,7 +149,8 @@ public function testGeneratedKeyHasCorrectLengthForCipher()
public function testProhibitedCommandDoesNotGenerateOrPublishAKey(): void
{
- $this->app['config']->set('app.key', '');
+ $config = $this->app->make('config');
+ $config->set('app.key', '');
$path = $this->envDir . '/.env';
file_put_contents($path, 'APP_KEY=');
KeyGenerateCommand::prohibit();
@@ -156,13 +160,14 @@ public function testProhibitedCommandDoesNotGenerateOrPublishAKey(): void
->assertSuccessful();
$this->assertSame('APP_KEY=', file_get_contents($path));
- $this->assertSame('', $this->app['config']->get('app.key'));
+ $this->assertSame('', $config->get('app.key'));
}
#[DataProvider('quotedKeyLines')]
public function testExactQuotedKeyLinesAreReplaced(string $configuredKey, string $line, string $suffix): void
{
- $this->app['config']->set('app.key', $configuredKey);
+ $config = $this->app->make('config');
+ $config->set('app.key', $configuredKey);
$path = $this->envDir . '/.env';
file_put_contents($path, $line);
@@ -171,7 +176,7 @@ public function testExactQuotedKeyLinesAreReplaced(string $configuredKey, string
->assertSuccessful();
$contents = file_get_contents($path);
- $generatedKey = $this->app['config']->get('app.key');
+ $generatedKey = $config->get('app.key');
$this->assertIsString($generatedKey);
$this->assertStringStartsWith('base64:', $generatedKey);
@@ -196,7 +201,8 @@ public static function quotedKeyLines(): array
#[DataProvider('nonMatchingKeyLines')]
public function testNonMatchingKeyLinesAreNotReplaced(string $line): void
{
- $this->app['config']->set('app.key', 'base64:current');
+ $config = $this->app->make('config');
+ $config->set('app.key', 'base64:current');
$path = $this->envDir . '/.env';
file_put_contents($path, $line);
@@ -204,7 +210,7 @@ public function testNonMatchingKeyLinesAreNotReplaced(string $line): void
->assertSuccessful();
$this->assertSame($line, file_get_contents($path));
- $this->assertSame('base64:current', $this->app['config']->get('app.key'));
+ $this->assertSame('base64:current', $config->get('app.key'));
}
/**
@@ -222,7 +228,7 @@ public static function nonMatchingKeyLines(): array
public function testMissingEnvironmentFileThrowsTheFilesystemException(): void
{
- $this->app['config']->set('app.key', '');
+ $this->app->make('config')->set('app.key', '');
$this->expectException(FileNotFoundException::class);
$this->expectExceptionMessage('File does not exist at path');
@@ -232,7 +238,7 @@ public function testMissingEnvironmentFileThrowsTheFilesystemException(): void
public function testEnvironmentReadFailureRemainsAFileNotFoundException(): void
{
- $this->app['config']->set('app.key', '');
+ $this->app->make('config')->set('app.key', '');
$path = $this->envDir . '/.env';
file_put_contents($path, 'APP_KEY=');
$filesystem = new FaultingKeyEnvironmentFilesystem;
@@ -247,7 +253,8 @@ public function testEnvironmentReadFailureRemainsAFileNotFoundException(): void
public function testEnvironmentReplacementFailureDoesNotPublishPartialState(): void
{
- $this->app['config']->set('app.key', '');
+ $config = $this->app->make('config');
+ $config->set('app.key', '');
$path = $this->envDir . '/.env';
file_put_contents($path, 'APP_KEY=');
$filesystem = new FaultingKeyEnvironmentFilesystem;
@@ -263,12 +270,12 @@ public function testEnvironmentReplacementFailureDoesNotPublishPartialState(): v
}
$this->assertSame('APP_KEY=', file_get_contents($path));
- $this->assertSame('', $this->app['config']->get('app.key'));
+ $this->assertSame('', $config->get('app.key'));
}
public function testEnvironmentFileModeIsPreservedWhenTheKeyIsReplaced(): void
{
- $this->app['config']->set('app.key', '');
+ $this->app->make('config')->set('app.key', '');
$path = $this->envDir . '/.env';
file_put_contents($path, 'APP_KEY=');
chmod($path, 0640);
diff --git a/tests/Integration/Foundation/Console/RouteCacheCommandTest.php b/tests/Integration/Foundation/Console/RouteCacheCommandTest.php
index e76c56879..a7dc5ba53 100644
--- a/tests/Integration/Foundation/Console/RouteCacheCommandTest.php
+++ b/tests/Integration/Foundation/Console/RouteCacheCommandTest.php
@@ -109,7 +109,7 @@ public function testCachedRoutesAreLoadable(): void
require $this->app->getCachedRoutesPath();
- $this->assertInstanceOf(CompiledRouteCollection::class, $this->app['router']->getRoutes());
+ $this->assertInstanceOf(CompiledRouteCollection::class, $this->app->make('router')->getRoutes());
}
public function testNamedRoutesSurviveCache(): void
@@ -125,7 +125,7 @@ public function testNamedRoutesSurviveCache(): void
require $this->app->getCachedRoutesPath();
- $routes = $this->app['router']->getRoutes();
+ $routes = $this->app->make('router')->getRoutes();
$this->assertSame('users', $routes->getByName('users.index')?->uri());
$this->assertSame('posts', $routes->getByName('posts.index')?->uri());
@@ -147,7 +147,7 @@ public function testRoutesWithMiddlewareDomainPrefixAndMultipleMethodsSurviveCac
require $this->app->getCachedRoutesPath();
- $route = $this->app['router']->getRoutes()->getByName('api.users');
+ $route = $this->app->make('router')->getRoutes()->getByName('api.users');
$this->assertNotNull($route);
$this->assertSame('api.example.com', $route->getDomain());
@@ -213,7 +213,7 @@ public function testRouteCacheRebuildsFromSourceWhenApplicationBootedWithExistin
require $this->app->getCachedRoutesPath();
- $route = $this->app['router']->getRoutes()->getByName('source.route');
+ $route = $this->app->make('router')->getRoutes()->getByName('source.route');
$this->assertNotNull($route);
$this->assertSame('beta', $route->uri());
diff --git a/tests/Integration/Foundation/ExceptionHandlerTest.php b/tests/Integration/Foundation/ExceptionHandlerTest.php
index 6da2c8fb9..c9668dacf 100644
--- a/tests/Integration/Foundation/ExceptionHandlerTest.php
+++ b/tests/Integration/Foundation/ExceptionHandlerTest.php
@@ -47,11 +47,11 @@ public function testItRendersAuthorizationExceptions()
]);
}
- public function testItDoesntReportExceptionsWithShouldntReportInterface()
+ public function testItDoesntReportExceptionsWithShouldntReportInterface(): void
{
Config::set('app.debug', true);
$reported = [];
- $this->app[ExceptionHandler::class]->reportable(function (Throwable $e) use (&$reported) {
+ $this->app->make(ExceptionHandler::class)->reportable(function (Throwable $e) use (&$reported) {
$reported[] = $e;
});
@@ -168,12 +168,12 @@ public function testItReturns400CodeOnMalformedRequests()
]);
}
- public function testItHandlesMalformedErrorViewsInProduction()
+ public function testItHandlesMalformedErrorViewsInProduction(): void
{
Config::set('view.paths', [__DIR__ . '/Fixtures/MalformedErrorViews']);
Config::set('app.debug', false);
$reported = [];
- $this->app[ExceptionHandler::class]->reportable(function (Throwable $e) use (&$reported) {
+ $this->app->make(ExceptionHandler::class)->reportable(function (Throwable $e) use (&$reported) {
$reported[] = $e;
});
@@ -189,12 +189,12 @@ public function testItHandlesMalformedErrorViewsInProduction()
$response->assertStatus(404);
}
- public function testItHandlesMalformedErrorViewsInDevelopment()
+ public function testItHandlesMalformedErrorViewsInDevelopment(): void
{
Config::set('view.paths', [__DIR__ . '/Fixtures/MalformedErrorViews']);
Config::set('app.debug', true);
$reported = [];
- $this->app[ExceptionHandler::class]->reportable(function (Throwable $e) use (&$reported) {
+ $this->app->make(ExceptionHandler::class)->reportable(function (Throwable $e) use (&$reported) {
$reported[] = $e;
});
@@ -210,10 +210,10 @@ public function testItHandlesMalformedErrorViewsInDevelopment()
$response->assertStatus(500);
}
- public function testItUseCustomJsonResponseFactoryInExceptionHandler()
+ public function testItUseCustomJsonResponseFactoryInExceptionHandler(): void
{
$this->app->singleton(ResponseFactoryContract::class, function ($app) {
- return new class($app['view'], $app['redirect']) extends ResponseFactory {
+ return new class($app->make('view'), $app->make('redirect')) extends ResponseFactory {
public function json(mixed $data = [], int $status = 200, array $headers = [], int $options = 0): JsonResponse
{
$msg = $data['message'] ?? $data['msg'] ?? null;
@@ -315,7 +315,7 @@ public function testItReportsRequestExceptions()
}
#[DataProvider('exitCodesProvider')]
- public function testItReturnsNonZeroExitCodesForUncaughtExceptions($providers, $successful)
+ public function testItReturnsNonZeroExitCodesForUncaughtExceptions(array $providers, bool $successful): void
{
$basePath = static::applicationBasePath();
$providers = json_encode($providers);
@@ -328,7 +328,7 @@ public function testItReturnsNonZeroExitCodesForUncaughtExceptions($providers, $
\$app = Hypervel\\Testbench\\Foundation\\Application::create(basePath: '{$basePath}', options: ['extra' => ['providers' => {$providers}]]);
\$app->singleton('Hypervel\\Contracts\\Debug\\ExceptionHandler', 'Hypervel\\Foundation\\Exceptions\\Handler');
-\$kernel = \$app[Hypervel\\Contracts\\Console\\Kernel::class];
+\$kernel = \$app->make(Hypervel\\Contracts\\Console\\Kernel::class);
return \$kernel->call('throw-exception-command');
EOF, __DIR__ . '/../../../', ['APP_RUNNING_IN_CONSOLE' => true]);
diff --git a/tests/Integration/Foundation/Exceptions/RenderBladeFilesTest.php b/tests/Integration/Foundation/Exceptions/RenderBladeFilesTest.php
index 080ab22c6..127ce2d99 100644
--- a/tests/Integration/Foundation/Exceptions/RenderBladeFilesTest.php
+++ b/tests/Integration/Foundation/Exceptions/RenderBladeFilesTest.php
@@ -52,7 +52,7 @@ public function source()
$path = package_path('src/foundation/resources/exceptions/renderer/components/formatted-source.blade.php');
- $html = (string) $this->app['view']->file($path, ['frame' => $frame])->render();
+ $html = (string) $this->app->make('view')->file($path, ['frame' => $frame])->render();
$this->assertStringContainsString('data-tippy-content="', $html);
$this->assertStringNotContainsString('
app['view']->file($path, ['queries' => $queries])->render();
+ $html = (string) $this->app->make('view')->file($path, ['queries' => $queries])->render();
$this->assertStringContainsString('data-tippy-content="', $html);
$this->assertMatchesRegularExpression('/<br\s*\/?>/', $html);
@@ -77,7 +77,7 @@ public function testRequestHeaderTooltipRendersMultilineSafely(): void
$path = package_path('src/foundation/resources/exceptions/renderer/components/request-header.blade.php');
- $html = (string) $this->app['view']->file($path, ['headers' => $headers])->render();
+ $html = (string) $this->app->make('view')->file($path, ['headers' => $headers])->render();
$this->assertStringContainsString('data-tippy-content="', $html);
$this->assertStringNotContainsString('
app['view']->file($path, ['routing' => $routing])->render();
+ $html = (string) $this->app->make('view')->file($path, ['routing' => $routing])->render();
$this->assertStringContainsString('data-tippy-content="', $html);
$this->assertStringNotContainsString('
app['config'];
+ $config = $this->app->make('config');
$config->set('logging.default', 'throw_exception');
diff --git a/tests/Integration/Foundation/FoundationServiceProvidersTest.php b/tests/Integration/Foundation/FoundationServiceProvidersTest.php
index 6a840be42..0b29f4a44 100644
--- a/tests/Integration/Foundation/FoundationServiceProvidersTest.php
+++ b/tests/Integration/Foundation/FoundationServiceProvidersTest.php
@@ -4,20 +4,21 @@
namespace Hypervel\Tests\Integration\Foundation;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Support\ServiceProvider;
use Hypervel\Testbench\TestCase;
class FoundationServiceProvidersTest extends TestCase
{
- protected function getPackageProviders($app): array
+ protected function getPackageProviders(ApplicationContract $app): array
{
return [HeadServiceProvider::class];
}
- public function testItCanBootServiceProviderRegisteredFromAnotherServiceProvider()
+ public function testItCanBootServiceProviderRegisteredFromAnotherServiceProvider(): void
{
- $this->assertTrue($this->app['tail.registered']);
- $this->assertTrue($this->app['tail.booted']);
+ $this->assertTrue($this->app->make('tail.registered'));
+ $this->assertTrue($this->app->make('tail.booted'));
}
}
@@ -37,11 +38,11 @@ class TailServiceProvider extends ServiceProvider
{
public function register(): void
{
- $this->app['tail.registered'] = true;
+ $this->app->instance('tail.registered', true);
}
public function boot(): void
{
- $this->app['tail.booted'] = true;
+ $this->app->instance('tail.booted', true);
}
}
diff --git a/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php b/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php
index d9e1821e6..46df7bbc4 100644
--- a/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php
+++ b/tests/Integration/Foundation/Support/Providers/RouteServiceProviderHealthTest.php
@@ -30,9 +30,9 @@ protected function resolveApplication(): ApplicationContract
)->create();
}
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('app.key', Str::random(32));
+ $app->make('config')->set('app.key', Str::random(32));
}
public function testItCanLoadHealthPage(): void
diff --git a/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php b/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php
index de8fdd3bd..cf190767a 100644
--- a/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php
+++ b/tests/Integration/Foundation/Testing/Concerns/InteractsWithAuthenticationTest.php
@@ -8,6 +8,7 @@
use Hypervel\Context\CoroutineContext;
use Hypervel\Contracts\Auth\Authenticatable as UserContract;
use Hypervel\Contracts\Auth\Guard;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\Schema\Blueprint;
use Hypervel\Foundation\Auth\User;
use Hypervel\Foundation\Testing\RefreshDatabase;
@@ -24,9 +25,9 @@ class InteractsWithAuthenticationTest extends TestCase
{
use RefreshDatabase;
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('auth.guards.api', [
+ $app->make('config')->set('auth.guards.api', [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
diff --git a/tests/Integration/Generators/EnumMakeCommandTest.php b/tests/Integration/Generators/EnumMakeCommandTest.php
index 29b64fad1..09ce4def2 100644
--- a/tests/Integration/Generators/EnumMakeCommandTest.php
+++ b/tests/Integration/Generators/EnumMakeCommandTest.php
@@ -6,7 +6,7 @@
class EnumMakeCommandTest extends TestCase
{
- protected $files = [
+ protected array $files = [
'app/IntEnum.php',
'app/StatusEnum.php',
'app/StringEnum.php',
@@ -46,12 +46,12 @@ public function testItCanGenerateEnumFileWithInt()
], 'app/IntEnum.php');
}
- public function testItCanGenerateEnumFileInEnumsFolder()
+ public function testItCanGenerateEnumFileInEnumsFolder(): void
{
$enumsFolderPath = app_path('Enums');
/** @var \Hypervel\Filesystem\Filesystem $files */
- $files = $this->app['files'];
+ $files = $this->app->make('files');
$files->ensureDirectoryExists($enumsFolderPath);
@@ -66,12 +66,12 @@ public function testItCanGenerateEnumFileInEnumsFolder()
$files->deleteDirectory($enumsFolderPath);
}
- public function testItCanGenerateEnumFileInEnumerationsFolder()
+ public function testItCanGenerateEnumFileInEnumerationsFolder(): void
{
$enumerationsFolderPath = app_path('Enumerations');
/** @var \Hypervel\Filesystem\Filesystem $files */
- $files = $this->app['files'];
+ $files = $this->app->make('files');
$files->ensureDirectoryExists($enumerationsFolderPath);
diff --git a/tests/Integration/Generators/InterfaceMakeCommandTest.php b/tests/Integration/Generators/InterfaceMakeCommandTest.php
index 7135774f6..085f0c939 100644
--- a/tests/Integration/Generators/InterfaceMakeCommandTest.php
+++ b/tests/Integration/Generators/InterfaceMakeCommandTest.php
@@ -6,7 +6,7 @@
class InterfaceMakeCommandTest extends TestCase
{
- protected $files = [
+ protected array $files = [
'app/Gateway.php',
'app/Contracts/Gateway.php',
'app/Interfaces/Gateway.php',
@@ -23,12 +23,12 @@ public function testItCanGenerateInterfaceFile()
], 'app/Gateway.php');
}
- public function testItCanGenerateInterfaceFileWhenContractsFolderExists()
+ public function testItCanGenerateInterfaceFileWhenContractsFolderExists(): void
{
$interfacesFolderPath = app_path('Contracts');
/** @var \Hypervel\Filesystem\Filesystem $files */
- $files = $this->app['files'];
+ $files = $this->app->make('files');
$files->ensureDirectoryExists($interfacesFolderPath);
@@ -43,12 +43,12 @@ public function testItCanGenerateInterfaceFileWhenContractsFolderExists()
$files->deleteDirectory($interfacesFolderPath);
}
- public function testItCanGenerateInterfaceFileWhenInterfacesFolderExists()
+ public function testItCanGenerateInterfaceFileWhenInterfacesFolderExists(): void
{
$interfacesFolderPath = app_path('Interfaces');
/** @var \Hypervel\Filesystem\Filesystem $files */
- $files = $this->app['files'];
+ $files = $this->app->make('files');
$files->ensureDirectoryExists($interfacesFolderPath);
diff --git a/tests/Integration/Generators/MailMakeCommandTest.php b/tests/Integration/Generators/MailMakeCommandTest.php
index 57d6b6cc1..04cd6db37 100644
--- a/tests/Integration/Generators/MailMakeCommandTest.php
+++ b/tests/Integration/Generators/MailMakeCommandTest.php
@@ -14,7 +14,7 @@
class MailMakeCommandTest extends TestCase
{
- protected $files = [
+ protected array $files = [
'app/Mail/*.php',
'resources/views/foo-mail.blade.php',
'resources/views/mail/*.blade.php',
@@ -60,7 +60,7 @@ public function testItCanGenerateMailFileWithMarkdownOption(): void
public function testErrorsWillBeDisplayedWhenMarkdownsAlreadyExist(): void
{
$existingMarkdownPath = 'resources/views/existing-markdown.blade.php';
- $this->app['files']
+ $this->app->make('files')
->put(
$this->app->basePath($existingMarkdownPath),
'My existing markdown'
@@ -102,7 +102,7 @@ public function testItCanGenerateMailFileWithViewOption(): void
public function testErrorsWillBeDisplayedWhenViewsAlreadyExist(): void
{
$existingViewPath = 'resources/views/existing-template.blade.php';
- $this->app['files']
+ $this->app->make('files')
->put(
$this->app->basePath($existingViewPath),
'My existing template
'
diff --git a/tests/Integration/Generators/TraitMakeCommandTest.php b/tests/Integration/Generators/TraitMakeCommandTest.php
index cf9e8a84c..1494ee9f7 100644
--- a/tests/Integration/Generators/TraitMakeCommandTest.php
+++ b/tests/Integration/Generators/TraitMakeCommandTest.php
@@ -6,7 +6,7 @@
class TraitMakeCommandTest extends TestCase
{
- protected $files = [
+ protected array $files = [
'app/FooTrait.php',
'app/Traits/FooTrait.php',
'app/Concerns/FooTrait.php',
@@ -23,12 +23,12 @@ public function testItCanGenerateTraitFile()
], 'app/FooTrait.php');
}
- public function testItCanGenerateTraitFileWhenTraitsFolderExists()
+ public function testItCanGenerateTraitFileWhenTraitsFolderExists(): void
{
$traitsFolderPath = app_path('Traits');
/** @var \Hypervel\Filesystem\Filesystem $files */
- $files = $this->app['files'];
+ $files = $this->app->make('files');
$files->ensureDirectoryExists($traitsFolderPath);
@@ -43,12 +43,12 @@ public function testItCanGenerateTraitFileWhenTraitsFolderExists()
$files->deleteDirectory($traitsFolderPath);
}
- public function testItCanGenerateTraitFileWhenConcernsFolderExists()
+ public function testItCanGenerateTraitFileWhenConcernsFolderExists(): void
{
$traitsFolderPath = app_path('Concerns');
/** @var \Hypervel\Filesystem\Filesystem $files */
- $files = $this->app['files'];
+ $files = $this->app->make('files');
$files->ensureDirectoryExists($traitsFolderPath);
diff --git a/tests/Integration/Horizon/Controller/BatchesControllerTest.php b/tests/Integration/Horizon/Controller/BatchesControllerTest.php
index 3c38356ee..4cf0d5b3f 100644
--- a/tests/Integration/Horizon/Controller/BatchesControllerTest.php
+++ b/tests/Integration/Horizon/Controller/BatchesControllerTest.php
@@ -10,7 +10,7 @@
class BatchesControllerTest extends ControllerTestCase
{
- public function testBatchesCanBeSearchedByName()
+ public function testBatchesCanBeSearchedByName(): void
{
$this->setupBatchTable();
$this->seedBatches();
@@ -26,7 +26,7 @@ public function testBatchesCanBeSearchedByName()
$this->assertSame('Import Users', $batches[0]->name);
}
- public function testBatchesCanBeSearchedByNameCaseInsensitively()
+ public function testBatchesCanBeSearchedByNameCaseInsensitively(): void
{
$this->setupBatchTable();
$this->seedBatches();
@@ -42,7 +42,7 @@ public function testBatchesCanBeSearchedByNameCaseInsensitively()
$this->assertSame('Import Users', $batches[0]->name);
}
- public function testBatchesCanBeSearchedById()
+ public function testBatchesCanBeSearchedById(): void
{
$this->setupBatchTable();
$this->seedBatches();
@@ -58,7 +58,7 @@ public function testBatchesCanBeSearchedById()
$this->assertSame('Send Emails', $batches[0]->name);
}
- public function testSearchEscapesLikeWildcards()
+ public function testSearchEscapesLikeWildcards(): void
{
$this->setupBatchTable();
$this->seedBatches();
@@ -88,7 +88,7 @@ public function testSearchMatchesLiteralUnderscores(): void
$this->assertSame('batch_under_score', $batches[0]->id);
}
- public function testSearchSupportsCursorPagination()
+ public function testSearchSupportsCursorPagination(): void
{
$this->setupBatchTable();
@@ -122,9 +122,11 @@ public function testSearchAppliesAZeroCursor(): void
private function setupBatchTable(): void
{
- $this->app['config']->set('queue.batching.database', 'testing');
- $this->app['config']->set('queue.batching.table', 'job_batches');
- $this->app['config']->set('database.connections.testing', [
+ $config = $this->app->make('config');
+
+ $config->set('queue.batching.database', 'testing');
+ $config->set('queue.batching.table', 'job_batches');
+ $config->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
diff --git a/tests/Integration/Horizon/Controller/DashboardStatsControllerTest.php b/tests/Integration/Horizon/Controller/DashboardStatsControllerTest.php
index 9b4af7367..5d9425765 100644
--- a/tests/Integration/Horizon/Controller/DashboardStatsControllerTest.php
+++ b/tests/Integration/Horizon/Controller/DashboardStatsControllerTest.php
@@ -14,7 +14,7 @@
class DashboardStatsControllerTest extends ControllerTestCase
{
- public function testAllStatsAreCorrectlyReturned()
+ public function testAllStatsAreCorrectlyReturned(): void
{
// Setup supervisor data...
$supervisors = m::mock(SupervisorRepository::class);
@@ -53,8 +53,10 @@ public function testAllStatsAreCorrectlyReturned()
]);
$this->app->instance(WaitTimeCalculator::class, $wait);
- $this->app['config']->set('horizon.trim.recent_failed', 10080);
- $this->app['config']->set('horizon.trim.recent', 60);
+ $config = $this->app->make('config');
+
+ $config->set('horizon.trim.recent_failed', 10080);
+ $config->set('horizon.trim.recent', 60);
$response = $this->actingAs(new Fakes\User)
->get('/horizon/api/stats');
@@ -75,7 +77,7 @@ public function testAllStatsAreCorrectlyReturned()
]);
}
- public function testPausedStatusIsReflectedIfAllMasterSupervisorsArePaused()
+ public function testPausedStatusIsReflectedIfAllMasterSupervisorsArePaused(): void
{
$masters = m::mock(MasterSupervisorRepository::class);
$masters->shouldReceive('all')->andReturn([
@@ -96,7 +98,7 @@ public function testPausedStatusIsReflectedIfAllMasterSupervisorsArePaused()
]);
}
- public function testPausedStatusIsntReflectedIfNotAllMasterSupervisorsArePaused()
+ public function testPausedStatusIsntReflectedIfNotAllMasterSupervisorsArePaused(): void
{
$masters = m::mock(MasterSupervisorRepository::class);
$masters->shouldReceive('all')->andReturn([
diff --git a/tests/Integration/Horizon/ControllerTestCase.php b/tests/Integration/Horizon/ControllerTestCase.php
index f3e0becdf..c73e6588c 100644
--- a/tests/Integration/Horizon/ControllerTestCase.php
+++ b/tests/Integration/Horizon/ControllerTestCase.php
@@ -4,16 +4,22 @@
namespace Hypervel\Tests\Integration\Horizon;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Horizon\Horizon;
abstract class ControllerTestCase extends IntegrationTestCase
{
+ protected function defineEnvironment(ApplicationContract $app): void
+ {
+ parent::defineEnvironment($app);
+
+ $app->make('config')->set('app.key', 'base64:UTyp33UhGolgzCK5CJmT+hNHcA+dJyp3+oINtX+VoPI=');
+ }
+
protected function setUp(): void
{
parent::setUp();
- $this->app['config']->set('app.key', 'base64:UTyp33UhGolgzCK5CJmT+hNHcA+dJyp3+oINtX+VoPI=');
-
Horizon::auth(function () {
return true;
});
diff --git a/tests/Integration/Horizon/Feature/ClearCommandTest.php b/tests/Integration/Horizon/Feature/ClearCommandTest.php
index d845ce68a..12a6fa157 100644
--- a/tests/Integration/Horizon/Feature/ClearCommandTest.php
+++ b/tests/Integration/Horizon/Feature/ClearCommandTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Horizon\Feature;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ClearableQueue;
use Hypervel\Contracts\Queue\Queue;
use Hypervel\Horizon\Console\ClearCommand;
@@ -18,15 +19,17 @@
class ClearCommandTest extends IntegrationTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('horizon.defaults', [
+ $config = $app->make('config');
+
+ $config->set('horizon.defaults', [
'supervisor-1' => ['connection' => 'redis'],
]);
- $app['config']->set('queue.connections.redis.queue', 'default');
- $app['config']->set('queue.connections.0.queue', 'zero-default');
+ $config->set('queue.connections.redis.queue', 'default');
+ $config->set('queue.connections.0.queue', 'zero-default');
}
#[DataProvider('queueIdentifierProvider')]
diff --git a/tests/Integration/Horizon/Feature/MonitorMasterSupervisorMemoryTest.php b/tests/Integration/Horizon/Feature/MonitorMasterSupervisorMemoryTest.php
index 032aa193f..e6f7e39df 100644
--- a/tests/Integration/Horizon/Feature/MonitorMasterSupervisorMemoryTest.php
+++ b/tests/Integration/Horizon/Feature/MonitorMasterSupervisorMemoryTest.php
@@ -16,10 +16,10 @@ protected function setUp(): void
{
parent::setUp();
- $this->app['env'] = 'production';
+ $this->app->instance('env', 'production');
}
- public function testSupervisorIsTerminatedWhenUsingTooMuchMemory()
+ public function testSupervisorIsTerminatedWhenUsingTooMuchMemory(): void
{
$monitor = new MonitorMasterSupervisorMemory;
@@ -32,7 +32,7 @@ public function testSupervisorIsTerminatedWhenUsingTooMuchMemory()
$monitor->handle(new MasterSupervisorLooped($master));
}
- public function testSupervisorIsNotTerminatedWhenUsingLowMemory()
+ public function testSupervisorIsNotTerminatedWhenUsingLowMemory(): void
{
$monitor = new MonitorMasterSupervisorMemory;
diff --git a/tests/Integration/Horizon/Feature/MonitorSupervisorMemoryTest.php b/tests/Integration/Horizon/Feature/MonitorSupervisorMemoryTest.php
index e7e7c6144..1287bdc16 100644
--- a/tests/Integration/Horizon/Feature/MonitorSupervisorMemoryTest.php
+++ b/tests/Integration/Horizon/Feature/MonitorSupervisorMemoryTest.php
@@ -17,10 +17,10 @@ protected function setUp(): void
{
parent::setUp();
- $this->app['env'] = 'production';
+ $this->app->instance('env', 'production');
}
- public function testSupervisorIsTerminatedWhenUsingTooMuchMemory()
+ public function testSupervisorIsTerminatedWhenUsingTooMuchMemory(): void
{
$monitor = new MonitorSupervisorMemory;
@@ -33,7 +33,7 @@ public function testSupervisorIsTerminatedWhenUsingTooMuchMemory()
$monitor->handle(new SupervisorLooped($supervisor));
}
- public function testSupervisorIsNotTerminatedWhenUsingLowMemory()
+ public function testSupervisorIsNotTerminatedWhenUsingLowMemory(): void
{
$monitor = new MonitorSupervisorMemory;
diff --git a/tests/Integration/Http/Middleware/HandleCorsTest.php b/tests/Integration/Http/Middleware/HandleCorsTest.php
index c21c6fe58..dde5d72b4 100644
--- a/tests/Integration/Http/Middleware/HandleCorsTest.php
+++ b/tests/Integration/Http/Middleware/HandleCorsTest.php
@@ -20,7 +20,7 @@ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('cors', [
+ $app->make('config')->set('cors', [
'paths' => ['api/*'],
'supports_credentials' => false,
'allowed_origins' => ['http://localhost'],
diff --git a/tests/Integration/Http/Middleware/PreventRequestForgeryServerRuntimeTest.php b/tests/Integration/Http/Middleware/PreventRequestForgeryServerRuntimeTest.php
index a2b217c01..380e63ee5 100644
--- a/tests/Integration/Http/Middleware/PreventRequestForgeryServerRuntimeTest.php
+++ b/tests/Integration/Http/Middleware/PreventRequestForgeryServerRuntimeTest.php
@@ -55,7 +55,7 @@ public function testServerRuntimeDoesNotBypassCsrfProtectionDuringTests(): void
$response = $this->get('/csrf-cookie')->assertOk();
- $sessionCookie = $this->cookieFromResponse($response->headers->getCookies(), $this->app['config']->get('session.cookie'));
+ $sessionCookie = $this->cookieFromResponse($response->headers->getCookies(), $this->app->make('config')->string('session.cookie'));
$this->withUnencryptedCookie($sessionCookie->getName(), $sessionCookie->getValue())
->post('/csrf-protected')
@@ -67,7 +67,7 @@ public function testServerRuntimeAcceptsMatchingCsrfToken(): void
$response = $this->get('/csrf-cookie')->assertOk();
$cookies = $response->headers->getCookies();
- $sessionCookie = $this->cookieFromResponse($cookies, $this->app['config']->get('session.cookie'));
+ $sessionCookie = $this->cookieFromResponse($cookies, $this->app->make('config')->string('session.cookie'));
$xsrfCookie = $this->cookieFromResponse($cookies, 'XSRF-TOKEN');
$this->withUnencryptedCookie($sessionCookie->getName(), $sessionCookie->getValue())
diff --git a/tests/Integration/Http/RequestBindingTest.php b/tests/Integration/Http/RequestBindingTest.php
index 030f8eba9..cc10bb9f1 100644
--- a/tests/Integration/Http/RequestBindingTest.php
+++ b/tests/Integration/Http/RequestBindingTest.php
@@ -22,6 +22,19 @@ public function testFallbackRequestUsesTheConfiguredApplicationUrl(): void
$this->assertSame('https://example.test/base', $request->getUri());
}
+ public function testFallbackRequestIsFreshForEveryResolution(): void
+ {
+ RequestContext::forget();
+
+ $firstRequest = $this->app->make('request');
+ $firstRequest->merge(['name' => 'John']);
+ $secondRequest = $this->app->make('request');
+
+ $this->assertNotSame($firstRequest, $secondRequest);
+ $this->assertSame('John', $firstRequest->input('name'));
+ $this->assertNull($secondRequest->input('name'));
+ }
+
public function testFallbackRequestRequiresTheApplicationUrlConfiguration(): void
{
$app = config()->array('app');
@@ -34,4 +47,13 @@ public function testFallbackRequestRequiresTheApplicationUrlConfiguration(): voi
$this->app->make('request');
}
+
+ public function testContextualRequestIsReturnedForEveryResolution(): void
+ {
+ $request = RequestContext::set(Request::create('/?name=John'));
+
+ $this->assertSame($request, $this->app->make('request'));
+ $this->assertSame($request, $this->app->make('request'));
+ $this->assertSame('John', request('name'));
+ }
}
diff --git a/tests/Integration/Log/ContextLoggingIntegrationTest.php b/tests/Integration/Log/ContextLoggingIntegrationTest.php
index 0d6bbf2c9..e082f9ff7 100644
--- a/tests/Integration/Log/ContextLoggingIntegrationTest.php
+++ b/tests/Integration/Log/ContextLoggingIntegrationTest.php
@@ -15,7 +15,7 @@
class ContextLoggingIntegrationTest extends TestCase
{
- public function testContextIsNotUsedAsMessageParameters()
+ public function testContextIsNotUsedAsMessageParameters(): void
{
$path = $this->app->storagePath() . '/logs/hypervel.log';
file_put_contents($path, '');
@@ -30,7 +30,7 @@ public function testContextIsNotUsedAsMessageParameters()
file_put_contents($path, '');
}
- public function testUsesClosureForContextProcessor()
+ public function testUsesClosureForContextProcessor(): void
{
$path = $this->app->storagePath() . '/logs/hypervel.log';
file_put_contents($path, '');
@@ -60,7 +60,7 @@ public function testUsesClosureForContextProcessor()
file_put_contents($path, '');
}
- public function testCanRebindToSeparateClass()
+ public function testCanRebindToSeparateClass(): void
{
TestAddContextProcessor::$wasConstructed = false;
@@ -82,7 +82,7 @@ public function testCanRebindToSeparateClass()
file_put_contents($path, '');
}
- public function testItAddsContextToLoggedExceptions()
+ public function testItAddsContextToLoggedExceptions(): void
{
$path = $this->app->storagePath() . '/logs/hypervel.log';
file_put_contents($path, '');
@@ -93,7 +93,7 @@ public function testItAddsContextToLoggedExceptions()
Context::push('bar.baz', 456);
Context::push('bar.baz', 789);
- $this->app[ExceptionHandler::class]->report(new Exception('Whoops!'));
+ $this->app->make(ExceptionHandler::class)->report(new Exception('Whoops!'));
$log = Str::after(file_get_contents($path), '] ');
$this->assertStringEndsWith(' {"trace_id":"550e8400-e29b-41d4-a716-446655440000","foo.bar":123,"bar.baz":[456,789]}', Str::trim($log));
@@ -102,7 +102,7 @@ public function testItAddsContextToLoggedExceptions()
Str::createUuidsNormally();
}
- public function testClosureBoundProcessorRunsOnceOnStackedLogger()
+ public function testClosureBoundProcessorRunsOnceOnStackedLogger(): void
{
$invocationCount = 0;
diff --git a/tests/Integration/Notifications/SendingMailNotificationsTest.php b/tests/Integration/Notifications/SendingMailNotificationsTest.php
index 5893e3b63..1a99f336a 100644
--- a/tests/Integration/Notifications/SendingMailNotificationsTest.php
+++ b/tests/Integration/Notifications/SendingMailNotificationsTest.php
@@ -24,11 +24,11 @@
class SendingMailNotificationsTest extends TestCase
{
- public $mailFactory;
+ public MailFactory $mailFactory;
- public $mailer;
+ public Mailer $mailer;
- public $markdown;
+ public Markdown $markdown;
protected function defineEnvironment(ApplicationContract $app): void
{
@@ -49,7 +49,7 @@ protected function defineEnvironment(ApplicationContract $app): void
return $this->mailFactory;
});
- $app['view']->addLocation(__DIR__ . '/Fixtures');
+ $app->make('view')->addLocation(__DIR__ . '/Fixtures');
}
protected function setUp(): void
diff --git a/tests/Integration/Notifications/SendingMailableNotificationsTest.php b/tests/Integration/Notifications/SendingMailableNotificationsTest.php
index a42ca9e78..e5236b717 100644
--- a/tests/Integration/Notifications/SendingMailableNotificationsTest.php
+++ b/tests/Integration/Notifications/SendingMailableNotificationsTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Notifications;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Database\Schema\Blueprint;
use Hypervel\Foundation\Testing\RefreshDatabase;
@@ -18,16 +19,16 @@ class SendingMailableNotificationsTest extends TestCase
{
use RefreshDatabase;
- protected function defineEnvironment(\Hypervel\Contracts\Foundation\Application $app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('mail.default', 'array');
- $app['config']->set('mail.mailers.array', ['transport' => 'array']);
+ $config = $app->make('config');
- $app['config']->set('app.locale', 'en');
+ $config->set('mail.default', 'array');
+ $config->set('mail.mailers.array', ['transport' => 'array']);
+ $config->set('app.locale', 'en');
+ $config->set('mail.markdown.theme', 'blank');
- $app['config']->set('mail.markdown.theme', 'blank');
-
- $app['view']->addLocation(__DIR__ . '/Fixtures');
+ $app->make('view')->addLocation(__DIR__ . '/Fixtures');
}
protected function afterRefreshingDatabase()
diff --git a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php
index 736667875..b7239b483 100644
--- a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php
+++ b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php
@@ -25,14 +25,15 @@ class SendingNotificationsWithLocaleTest extends TestCase
{
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('mail.default', 'array');
- $app['config']->set('mail.mailers.array', ['transport' => 'array']);
+ $config = $app->make('config');
- $app['config']->set('app.locale', 'en');
+ $config->set('mail.default', 'array');
+ $config->set('mail.mailers.array', ['transport' => 'array']);
+ $config->set('app.locale', 'en');
- $app['view']->addLocation(__DIR__ . '/Fixtures');
+ $app->make('view')->addLocation(__DIR__ . '/Fixtures');
- $app['translator']->setLoaded([
+ $app->make('translator')->setLoaded([
'*' => [
'*' => [
'en' => ['hi' => 'hello'],
diff --git a/tests/Integration/Queue/CustomPayloadTest.php b/tests/Integration/Queue/CustomPayloadTest.php
index f01afb3df..5f46294a6 100644
--- a/tests/Integration/Queue/CustomPayloadTest.php
+++ b/tests/Integration/Queue/CustomPayloadTest.php
@@ -21,7 +21,7 @@ protected function getPackageProviders(ApplicationContract $app): array
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('queue.default', 'sync');
+ $app->make('config')->set('queue.default', 'sync');
}
#[DataProvider('websites')]
@@ -44,12 +44,12 @@ class QueueServiceProvider extends ServiceProvider
{
public function register(): void
{
- $this->app->bind('one.time.password', fn () => random_int(1, 10));
+ $this->app->instance('one.time.password', random_int(1, 10));
Queue::createPayloadUsing(function () {
$password = $this->app->make('one.time.password');
- $this->app->offsetUnset('one.time.password');
+ $this->app->forgetInstance('one.time.password');
return ['password' => $password];
});
diff --git a/tests/Integration/Queue/DebouncedJobTest.php b/tests/Integration/Queue/DebouncedJobTest.php
index fe541e55a..fad0537ee 100644
--- a/tests/Integration/Queue/DebouncedJobTest.php
+++ b/tests/Integration/Queue/DebouncedJobTest.php
@@ -11,6 +11,7 @@
use Hypervel\Container\Container;
use Hypervel\Contracts\Cache\Factory as CacheFactory;
use Hypervel\Contracts\Cache\Repository as Cache;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldBeUnique;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Foundation\Bus\Dispatchable;
@@ -30,12 +31,13 @@
#[WithMigration('queue')]
class DebouncedJobTest extends QueueTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('cache.default', 'database');
- $app['config']->set('queue.default', 'database');
+ $config = $app->make('config');
+ $config->set('cache.default', 'database');
+ $config->set('queue.default', 'database');
}
public function testDebouncedJobDispatchesAndExecutes(): void
diff --git a/tests/Integration/Queue/DeleteModelWhenMissingTest.php b/tests/Integration/Queue/DeleteModelWhenMissingTest.php
index fdc226258..f25fd7980 100644
--- a/tests/Integration/Queue/DeleteModelWhenMissingTest.php
+++ b/tests/Integration/Queue/DeleteModelWhenMissingTest.php
@@ -5,6 +5,7 @@
namespace Hypervel\Tests\Integration\Queue\DeleteModelWhenMissingTest;
use DB;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Database\Schema\Blueprint;
@@ -20,10 +21,10 @@
#[WithMigration('queue')]
class DeleteModelWhenMissingTest extends QueueTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('queue.default', 'database');
+ $app->make('config')->set('queue.default', 'database');
}
protected function defineDatabaseMigrationsAfterDatabaseRefreshed(): void
diff --git a/tests/Integration/Queue/DeleteNotificationWhenMissingModelTest.php b/tests/Integration/Queue/DeleteNotificationWhenMissingModelTest.php
index 763a465df..bbf8cc7f7 100644
--- a/tests/Integration/Queue/DeleteNotificationWhenMissingModelTest.php
+++ b/tests/Integration/Queue/DeleteNotificationWhenMissingModelTest.php
@@ -6,6 +6,7 @@
use DB;
use Hypervel\Bus\Queueable;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Database\Schema\Blueprint;
@@ -24,10 +25,10 @@
#[WithMigration('queue')]
class DeleteNotificationWhenMissingModelTest extends QueueTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('queue.default', 'database');
+ $app->make('config')->set('queue.default', 'database');
}
protected function defineDatabaseMigrationsAfterDatabaseRefreshed(): void
diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php
index 2c6a5d573..a42113b48 100644
--- a/tests/Integration/Queue/JobChainingTest.php
+++ b/tests/Integration/Queue/JobChainingTest.php
@@ -9,6 +9,7 @@
use Hypervel\Bus\Batchable;
use Hypervel\Bus\PendingBatch;
use Hypervel\Bus\Queueable;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Foundation\Bus\Dispatchable;
use Hypervel\Foundation\Bus\PendingChain;
@@ -27,11 +28,11 @@ class JobChainingTest extends QueueTestCase
public static bool $catchCallbackRan = false;
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set([
+ $app->make('config')->set([
'queue.connections.sync1' => ['driver' => 'sync'],
'queue.connections.sync2' => ['driver' => 'sync'],
]);
diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php
index 32719cfd4..35b23b9fc 100644
--- a/tests/Integration/Queue/JobDispatchingTest.php
+++ b/tests/Integration/Queue/JobDispatchingTest.php
@@ -227,10 +227,12 @@ public function testQueueMayBeNullForJobQueueingAndJobQueuedEvent(): void
{
Config::set('queue.default', 'database');
$events = [];
- $this->app['events']->listen(function (JobQueueing $e) use (&$events) {
+ $dispatcher = $this->app->make('events');
+
+ $dispatcher->listen(function (JobQueueing $e) use (&$events) {
$events[] = $e;
});
- $this->app['events']->listen(function (JobQueued $e) use (&$events) {
+ $dispatcher->listen(function (JobQueued $e) use (&$events) {
$events[] = $e;
});
@@ -253,7 +255,7 @@ public function testQueuedClosureCanBeNamed(): void
{
Config::set('queue.default', 'database');
$events = [];
- $this->app['events']->listen(function (JobQueued $e) use (&$events) {
+ $this->app->make('events')->listen(function (JobQueued $e) use (&$events) {
$events[] = $e;
});
diff --git a/tests/Integration/Queue/JobEncryptionTest.php b/tests/Integration/Queue/JobEncryptionTest.php
index 47c5998f4..84bbdbb07 100644
--- a/tests/Integration/Queue/JobEncryptionTest.php
+++ b/tests/Integration/Queue/JobEncryptionTest.php
@@ -6,6 +6,7 @@
use Hypervel\Bus\Queueable;
use Hypervel\Contracts\Encryption\DecryptException;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldBeEncrypted;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Foundation\Bus\Dispatchable;
@@ -21,12 +22,13 @@
#[WithMigration('queue')]
class JobEncryptionTest extends QueueTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('app.key', Str::random(32));
- $app['config']->set('queue.default', 'database');
+ $config = $app->make('config');
+ $config->set('app.key', Str::random(32));
+ $config->set('queue.default', 'database');
}
#[Override]
diff --git a/tests/Integration/Queue/ModelSerializationTest.php b/tests/Integration/Queue/ModelSerializationTest.php
index 4f66918a5..4c7d950fe 100644
--- a/tests/Integration/Queue/ModelSerializationTest.php
+++ b/tests/Integration/Queue/ModelSerializationTest.php
@@ -32,7 +32,7 @@ class ModelSerializationTest extends TestCase
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('database.connections.custom', [
+ $app->make('config')->set('database.connections.custom', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
diff --git a/tests/Integration/Queue/QueueConnectionTest.php b/tests/Integration/Queue/QueueConnectionTest.php
index d48e7f00f..ab3ee50a6 100644
--- a/tests/Integration/Queue/QueueConnectionTest.php
+++ b/tests/Integration/Queue/QueueConnectionTest.php
@@ -62,9 +62,9 @@ public function testJobWillGetDispatchedInsideATransactionWhenExplicitlyIndicate
}
}
- public function testJobWontGetDispatchedInsideATransactionWhenExplicitlyIndicated()
+ public function testJobWontGetDispatchedInsideATransactionWhenExplicitlyIndicated(): void
{
- $this->app['config']->set('queue.connections.sqs.after_commit', false);
+ $this->app->make('config')->set('queue.connections.sqs.after_commit', false);
$this->app->singleton('db.transactions', function () {
$transactionManager = m::mock(DatabaseTransactionsManager::class);
@@ -117,9 +117,9 @@ public function testUniqueJobWillGetDispatchedInsideATransactionWhenExplicitlyIn
}
}
- public function testUniqueJobWontGetDispatchedInsideATransactionWhenExplicitlyIndicated()
+ public function testUniqueJobWontGetDispatchedInsideATransactionWhenExplicitlyIndicated(): void
{
- $this->app['config']->set('queue.connections.sqs.after_commit', false);
+ $this->app->make('config')->set('queue.connections.sqs.after_commit', false);
$this->app->singleton('db.transactions', function () {
$transactionManager = m::mock(DatabaseTransactionsManager::class);
diff --git a/tests/Integration/Queue/QueueFakeTest.php b/tests/Integration/Queue/QueueFakeTest.php
index 236be183a..9869e987b 100644
--- a/tests/Integration/Queue/QueueFakeTest.php
+++ b/tests/Integration/Queue/QueueFakeTest.php
@@ -15,7 +15,7 @@ class QueueFakeTest extends TestCase
{
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('queue.default', 'sync');
+ $app->make('config')->set('queue.default', 'sync');
}
public function testFakeFor()
diff --git a/tests/Integration/Queue/Redis/RedisQueueTest.php b/tests/Integration/Queue/Redis/RedisQueueTest.php
index 0d1ea9096..cd8ea5a69 100644
--- a/tests/Integration/Queue/Redis/RedisQueueTest.php
+++ b/tests/Integration/Queue/Redis/RedisQueueTest.php
@@ -35,9 +35,9 @@ class RedisQueueTest extends TestCase
private RedisQueue $queue;
- public function testExpiredJobsArePopped()
+ public function testExpiredJobsArePopped(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
@@ -63,9 +63,9 @@ public function testExpiredJobsArePopped()
$this->assertSame(3, $this->redisConnection()->zcard("{$redisKey}:reserved"));
}
- public function testPopProperlyPopsJobOffOfRedis()
+ public function testPopProperlyPopsJobOffOfRedis(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
@@ -99,7 +99,7 @@ public function testInvalidRawPayloadIsReservedWithoutMutation(
?string $expectedId,
string $expectedMessage,
): void {
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->pushRaw($payload);
@@ -145,7 +145,7 @@ public static function invalidRawPayloads(): array
public function testNumericStringAttemptsAreIncrementedAtomically(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->pushRaw('{"id":"job-id","job":"foo","data":[],"attempts":"2"}');
@@ -161,7 +161,7 @@ public function testNumericStringAttemptsAreIncrementedAtomically(): void
public function testFractionalAttemptsReachPhpAsAnInteger(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->pushRaw('{"id":"job-id","job":"foo","data":[],"attempts":1.5}');
@@ -173,9 +173,9 @@ public function testFractionalAttemptsReachPhpAsAnInteger(): void
$this->assertSame(2.5, json_decode($job->getReservedJob(), true, flags: JSON_THROW_ON_ERROR)['attempts']);
}
- public function testPopProperlyPopsDelayedJobOffOfRedis()
+ public function testPopProperlyPopsDelayedJobOffOfRedis(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
@@ -196,9 +196,9 @@ public function testPopProperlyPopsDelayedJobOffOfRedis()
$this->assertEquals($job, unserialize(json_decode($reservedJob)->data->command));
}
- public function testPopPopsDelayedJobOffOfRedisWhenExpireNull()
+ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default, retryAfter: null);
@@ -219,9 +219,9 @@ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull()
$this->assertEquals($job, unserialize(json_decode($reservedJob)->data->command));
}
- public function testBlockingPopProperlyPopsJobOffOfRedis()
+ public function testBlockingPopProperlyPopsJobOffOfRedis(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default, blockFor: 5);
@@ -235,11 +235,11 @@ public function testBlockingPopProperlyPopsJobOffOfRedis()
$this->assertEquals($job, unserialize(json_decode($redisJob->getReservedJob())->data->command));
}
- public function testBlockingPopProperlyPopsExpiredJobs()
+ public function testBlockingPopProperlyPopsExpiredJobs(): void
{
Str::createUuidsUsing(fn () => '00000000-0000-0000-0000-000000000000');
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default, blockFor: 5);
@@ -264,9 +264,9 @@ public function testBlockingPopProperlyPopsExpiredJobs()
}
}
- public function testNotExpireJobsWhenExpireNull()
+ public function testNotExpireJobsWhenExpireNull(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default, retryAfter: null);
@@ -306,9 +306,9 @@ public function testNotExpireJobsWhenExpireNull()
}
}
- public function testExpireJobsWhenExpireSet()
+ public function testExpireJobsWhenExpireSet(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default, retryAfter: 30);
@@ -329,9 +329,9 @@ public function testExpireJobsWhenExpireSet()
$this->assertEquals($job, unserialize(json_decode($reservedJob)->data->command));
}
- public function testRelease()
+ public function testRelease(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
@@ -361,9 +361,9 @@ public function testRelease()
$this->assertNull($this->queue->pop());
}
- public function testReleaseInThePast()
+ public function testReleaseInThePast(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
@@ -377,9 +377,9 @@ public function testReleaseInThePast()
$this->assertInstanceOf(RedisJob::class, $this->queue->pop());
}
- public function testDelete()
+ public function testDelete(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
@@ -397,9 +397,9 @@ public function testDelete()
$this->assertNull($this->queue->pop());
}
- public function testClear()
+ public function testClear(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
@@ -415,9 +415,9 @@ public function testClear()
$this->assertSame(0, $this->redisConnection()->llen("{$redisKey}:notify"));
}
- public function testSize()
+ public function testSize(): void
{
- $this->setQueue($this->app['config']->get('queue.connections.redis.queue'));
+ $this->setQueue($this->defaultQueueName());
$this->assertSame(0, $this->queue->size());
$this->queue->push(new RedisQueueIntegrationTestJob(1));
@@ -434,7 +434,7 @@ public function testSize()
$this->assertSame(2, $this->queue->size());
}
- public function testPushJobQueueingAndJobQueuedEvents()
+ public function testPushJobQueueingAndJobQueuedEvents(): void
{
$events = m::mock(Dispatcher::class);
$events->shouldReceive('hasListeners')->with(JobQueueing::class)->andReturn(true)->once();
@@ -455,14 +455,14 @@ public function testPushJobQueueingAndJobQueuedEvents()
$container->shouldReceive('bound')->with('events')->andReturn(true)->twice();
$container->shouldReceive('make')->with('events')->andReturn($events)->twice();
- $queue = new RedisQueue($this->app->make(RedisFactory::class), $this->app['config']->get('queue.connections.redis.queue'));
+ $queue = new RedisQueue($this->app->make(RedisFactory::class), $this->defaultQueueName());
$queue->setContainer($container);
$queue->setConnectionName('redis');
$queue->push(new RedisQueueIntegrationTestJob(5));
}
- public function testBulkJobQueuedEvent()
+ public function testBulkJobQueuedEvent(): void
{
$events = m::mock(Dispatcher::class);
$events->shouldReceive('hasListeners')->with(JobQueueing::class)->andReturn(true)->times(3);
@@ -474,7 +474,7 @@ public function testBulkJobQueuedEvent()
$container->shouldReceive('bound')->with('events')->andReturn(true)->times(6);
$container->shouldReceive('make')->with('events')->andReturn($events)->times(6);
- $queue = new RedisQueue($this->app->make(RedisFactory::class), $this->app['config']->get('queue.connections.redis.queue'));
+ $queue = new RedisQueue($this->app->make(RedisFactory::class), $this->defaultQueueName());
$queue->setContainer($container);
$queue->setConnectionName('redis');
@@ -485,7 +485,7 @@ public function testBulkJobQueuedEvent()
]);
}
- public function testDelayedJobsWorkWithPhpRedisSerializationEnabled()
+ public function testDelayedJobsWorkWithPhpRedisSerializationEnabled(): void
{
$connection = Redis::connection('default');
@@ -498,7 +498,7 @@ public function testDelayedJobsWorkWithPhpRedisSerializationEnabled()
$client->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_PHP);
try {
- $this->setQueue($this->app['config']->get('queue.connections.redis.queue'));
+ $this->setQueue($this->defaultQueueName());
$job = new RedisQueueIntegrationTestJob(42);
$this->queue->later(-10, $job);
@@ -524,7 +524,7 @@ public function testDelayedJobsWorkWithPhpRedisSerializationEnabled()
public function testPendingJobs(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->push(new RedisQueueIntegrationTestJob(99));
@@ -535,7 +535,7 @@ public function testPendingJobs(): void
public function testDelayedJobs(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->later(60, new RedisQueueIntegrationTestJob(99));
@@ -546,7 +546,7 @@ public function testDelayedJobs(): void
public function testReservedJobs(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->push(new RedisQueueIntegrationTestJob(99));
$this->queue->pop();
@@ -558,7 +558,7 @@ public function testReservedJobs(): void
public function testAllPendingJobs(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->push(new RedisQueueIntegrationTestJob(1));
$this->queue->pushOn('emails', new RedisQueueIntegrationTestJob(2));
@@ -585,7 +585,7 @@ public function testAllPendingJobsReportExplicitHashTaggedNamesByTopology(): voi
public function testAllDelayedJobs(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->later(60, new RedisQueueIntegrationTestJob(1));
$this->queue->laterOn('emails', 60, new RedisQueueIntegrationTestJob(2));
@@ -599,7 +599,7 @@ public function testAllDelayedJobs(): void
public function testAllReservedJobs(): void
{
- $default = $this->app['config']->get('queue.connections.redis.queue');
+ $default = $this->defaultQueueName();
$this->setQueue($default);
$this->queue->push(new RedisQueueIntegrationTestJob(1));
$this->queue->pushOn('emails', new RedisQueueIntegrationTestJob(2));
@@ -642,11 +642,16 @@ private function assertInspectedJob(InspectedJob $job, ?string $queue, int $atte
$this->assertInstanceOf(CarbonImmutable::class, $job->createdAt);
}
+ private function defaultQueueName(): string
+ {
+ return $this->app->make('config')->string('queue.connections.redis.queue');
+ }
+
private function setQueue(?string $default = null, ?string $connection = null, ?int $retryAfter = 60, ?int $blockFor = null): void
{
$this->queue = new RedisQueue(
$this->app->make(RedisFactory::class),
- $default ?? $this->app['config']->get('queue.connections.redis.queue'),
+ $default ?? $this->defaultQueueName(),
$connection,
$retryAfter,
$blockFor,
diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php
index fd11847f4..8c3df090f 100644
--- a/tests/Integration/Queue/UniqueJobTest.php
+++ b/tests/Integration/Queue/UniqueJobTest.php
@@ -9,6 +9,7 @@
use Hypervel\Bus\UniqueLock;
use Hypervel\Container\Container;
use Hypervel\Contracts\Cache\Repository as Cache;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldBeUnique;
use Hypervel\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Hypervel\Contracts\Queue\ShouldQueue;
@@ -28,12 +29,13 @@
#[WithMigration('queue')]
class UniqueJobTest extends QueueTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('cache.default', 'database');
- $app['config']->set('queue.default', 'database');
+ $config = $app->make('config');
+ $config->set('cache.default', 'database');
+ $config->set('queue.default', 'database');
}
public function testUniqueJobsAreNotDispatched()
diff --git a/tests/Integration/Queue/UniqueUntilProcessingJobTest.php b/tests/Integration/Queue/UniqueUntilProcessingJobTest.php
index 7f2bbf5d9..c51c60cc9 100644
--- a/tests/Integration/Queue/UniqueUntilProcessingJobTest.php
+++ b/tests/Integration/Queue/UniqueUntilProcessingJobTest.php
@@ -5,6 +5,7 @@
namespace Hypervel\Tests\Integration\Queue\UniqueUntilProcessingJobTest;
use Hypervel\Bus\Queueable;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Foundation\Bus\Dispatchable;
@@ -18,11 +19,13 @@
#[WithMigration('queue')]
class UniqueUntilProcessingJobTest extends QueueTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('queue.default', 'database');
- $app['config']->set('cache.default', 'database');
+
+ $config = $app->make('config');
+ $config->set('queue.default', 'database');
+ $config->set('cache.default', 'database');
}
public function testShouldBeUniqueUntilProcessingReleasesLockWhenJobIsReleasedByAMiddleware()
diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php
index 25520ab70..4edbbbf5d 100644
--- a/tests/Integration/Queue/WorkCommandTest.php
+++ b/tests/Integration/Queue/WorkCommandTest.php
@@ -7,6 +7,7 @@
use Hypervel\Bus\Queueable;
use Hypervel\Cache\CacheManager;
use Hypervel\Cache\Repository;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ShouldQueue;
use Hypervel\Database\UniqueConstraintViolationException;
use Hypervel\Foundation\Bus\Dispatchable;
@@ -27,11 +28,11 @@ class WorkCommandTest extends QueueTestCase
{
use DatabaseMigrations;
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('queue.default', 'database');
+ $app->make('config')->set('queue.default', 'database');
}
protected function setUp(): void
@@ -87,9 +88,11 @@ public function testQueueOptionPreservesZeroAndDefaultsEmptyString(): void
public function testConnectionArgumentPreservesZero(): void
{
- $this->app['config']->set(
+ $config = $this->app->make('config');
+
+ $config->set(
'queue.connections.0',
- $this->app['config']->get('queue.connections.database'),
+ $config->get('queue.connections.database'),
);
Queue::connection('0')->push(new FirstJob);
@@ -150,7 +153,7 @@ public function testRunTimestampOutputWithDefaultAppTimezone(): void
public function testRunTimestampOutputWithDifferentLogTimezone(): void
{
- $this->app['config']->set('queue.output_timezone', 'Europe/Helsinki');
+ $this->app->make('config')->set('queue.output_timezone', 'Europe/Helsinki');
$this->travelTo(CarbonImmutable::create(2023, 1, 18, 10, 10, 11));
Queue::push(new FirstJob);
@@ -164,7 +167,7 @@ public function testRunTimestampOutputWithDifferentLogTimezone(): void
public function testRunTimestampOutputWithSameAppDefaultAndQueueLogDefault(): void
{
- $this->app['config']->set('queue.output_timezone', 'UTC');
+ $this->app->make('config')->set('queue.output_timezone', 'UTC');
$this->travelTo(CarbonImmutable::create(2023, 1, 18, 10, 10, 11));
Queue::push(new FirstJob);
diff --git a/tests/Integration/Routing/CompiledRouteCollectionTest.php b/tests/Integration/Routing/CompiledRouteCollectionTest.php
index e1f735f57..120dbfb94 100644
--- a/tests/Integration/Routing/CompiledRouteCollectionTest.php
+++ b/tests/Integration/Routing/CompiledRouteCollectionTest.php
@@ -6,29 +6,25 @@
use ArrayIterator;
use Hypervel\Http\Request;
+use Hypervel\Routing\CompiledRouteCollection;
use Hypervel\Routing\Route;
use Hypervel\Routing\RouteCollection;
+use Hypervel\Routing\Router;
use Hypervel\Support\Arr;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class CompiledRouteCollectionTest extends RoutingTestCase
{
- /**
- * @var \Hypervel\Routing\RouteCollection
- */
- protected $routeCollection;
+ protected RouteCollection $routeCollection;
- /**
- * @var \Hypervel\Routing\Router
- */
- protected $router;
+ protected Router $router;
protected function setUp(): void
{
parent::setUp();
- $this->router = $this->app['router'];
+ $this->router = $this->app->make('router');
$this->routeCollection = new RouteCollection;
}
@@ -40,10 +36,7 @@ protected function tearDown(): void
parent::tearDown();
}
- /**
- * @return \Hypervel\Routing\CompiledRouteCollection
- */
- protected function collection()
+ protected function collection(): CompiledRouteCollection
{
return $this->routeCollection->toCompiledRouteCollection($this->router, $this->app);
}
@@ -95,7 +88,7 @@ public function testRouteCollectionCanRetrieveByAction()
$this->assertSame($action, Arr::except($route->getAction(), 'as'));
}
- public function testCompiledAndNonCompiledUrlResolutionHasSamePrecedenceForActions()
+ public function testCompiledAndNonCompiledUrlResolutionHasSamePrecedenceForActions(): void
{
$this->router->get('/foo/{bar}', ['FooController', 'show']);
$this->router->get('/foo/{bar}/{baz}', ['FooController', 'show']);
@@ -104,7 +97,7 @@ public function testCompiledAndNonCompiledUrlResolutionHasSamePrecedenceForActio
$this->assertSame('foo/{bar}', $this->router->getRoutes()->getByAction('FooController@show')->uri);
$this->router->setCompiledRoutes($this->router->getRoutes()->compile());
- $this->assertSame('foo/{bar}', $this->app['router']->getRoutes()->getByAction('FooController@show')->uri);
+ $this->assertSame('foo/{bar}', $this->app->make('router')->getRoutes()->getByAction('FooController@show')->uri);
}
public function testCompiledAndNonCompiledUrlResolutionHasSamePrecedenceForNames()
diff --git a/tests/Integration/Routing/ImplicitBackedEnumRouteBindingTest.php b/tests/Integration/Routing/ImplicitBackedEnumRouteBindingTest.php
index aa0f1f4d6..801a80684 100644
--- a/tests/Integration/Routing/ImplicitBackedEnumRouteBindingTest.php
+++ b/tests/Integration/Routing/ImplicitBackedEnumRouteBindingTest.php
@@ -4,14 +4,15 @@
namespace Hypervel\Tests\Integration\Routing;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Support\Facades\Route;
use Hypervel\Tests\Integration\Routing\Fixtures\CategoryBackedEnum;
class ImplicitBackedEnumRouteBindingTest extends RoutingTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set(['app.key' => 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF']);
+ $app->make('config')->set(['app.key' => 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF']);
}
public function testWithRouteCachingEnabled()
diff --git a/tests/Integration/Routing/PrecognitionTest.php b/tests/Integration/Routing/PrecognitionTest.php
index 7cc35c92f..9ab2765b1 100644
--- a/tests/Integration/Routing/PrecognitionTest.php
+++ b/tests/Integration/Routing/PrecognitionTest.php
@@ -28,7 +28,7 @@ function fail()
class PrecognitionTest extends RoutingTestCase
{
- public function testItDoesntInvokeControllerMethodByDefault()
+ public function testItDoesntInvokeControllerMethodByDefault(): void
{
Route::get('test-route', [PrecognitionTestController::class, 'methodThatFails'])
->middleware(HandlePrecognitiveRequests::class);
@@ -37,12 +37,11 @@ public function testItDoesntInvokeControllerMethodByDefault()
$response->assertNoContent();
$response->assertHeader('Precognition-Success', 'true');
- $this->assertTrue($this->app['ClassWasInstantiated']);
+ $this->assertTrue($this->app->make('ClassWasInstantiated'));
}
- public function testItDoesntInvokeCallableControllerByDefault()
+ public function testItDoesntInvokeCallableControllerByDefault(): void
{
- $resolved = false;
Route::get('test-route', fn (ClassThatBindsOnInstantiation $foo) => fail())
->middleware(HandlePrecognitiveRequests::class);
@@ -50,7 +49,7 @@ public function testItDoesntInvokeCallableControllerByDefault()
$response->assertNoContent();
$response->assertHeader('Precognition-Success', 'true');
- $this->assertTrue($this->app['ClassWasInstantiated']);
+ $this->assertTrue($this->app->make('ClassWasInstantiated'));
}
public function testItCanCheckPrecognitiveStateOnTheRequest()
@@ -667,7 +666,7 @@ public function testVaryHeaderIsAppliedToNonPrecognitionResponses()
$response->assertHeaderMissing('Precognition-Success');
}
- public function testItStopsExecutionAfterSuccessfulValidationWithValidationFilteringAndFormRequest()
+ public function testItStopsExecutionAfterSuccessfulValidationWithValidationFilteringAndFormRequest(): void
{
Route::post('test-route', function (PrecognitionTestRequest $request, ClassThatBindsOnInstantiation $foo) {
fail();
@@ -682,7 +681,7 @@ public function testItStopsExecutionAfterSuccessfulValidationWithValidationFilte
'Precognition-Validate-Only' => 'optional_integer_1',
]);
- $this->assertFalse($this->app['ClassWasInstantiated']);
+ $this->assertFalse($this->app->make('ClassWasInstantiated'));
$response->assertNoContent();
$response->assertHeader('Precognition', 'true');
$response->assertHeader('Precognition-Success', 'true');
@@ -933,7 +932,7 @@ public function testItCanFilterRulesWithEscapedDotsWhenUsingControllerValidateWi
$response->assertHeader('Precognition', 'true');
}
- public function testItContinuesExecutionAfterSuccessfulValidationWithoutValidationFilteringAndFormRequest()
+ public function testItContinuesExecutionAfterSuccessfulValidationWithoutValidationFilteringAndFormRequest(): void
{
Route::post('test-route', function (PrecognitionTestRequest $request, ClassThatBindsOnInstantiation $foo) {
precognitive(function ($bail) {
@@ -950,7 +949,7 @@ public function testItContinuesExecutionAfterSuccessfulValidationWithoutValidati
'Precognition' => 'true',
]);
- $this->assertTrue($this->app['ClassWasInstantiated']);
+ $this->assertTrue($this->app->make('ClassWasInstantiated'));
$response->assertOk();
$this->assertSame('expected response', $response->content());
$response->assertHeader('Precognition', 'true');
@@ -1159,11 +1158,11 @@ public function testItContinuesExecutionAfterSuccessfulValidationWithoutValidati
$response->assertHeaderMissing('Precognition-Success');
}
- public function testItDoesNotSetLastUrl()
+ public function testItDoesNotSetLastUrl(): void
{
// Force the session manager to use the array driver and flush any cached driver
// so the config change takes effect.
- $this->app['config']->set('session.driver', 'array');
+ $this->app->make('config')->set('session.driver', 'array');
$this->app->make('session')->forgetDrivers();
// Capture previousUrl inside route handlers since session() is coroutine-scoped.
diff --git a/tests/Integration/Routing/UrlSigningTest.php b/tests/Integration/Routing/UrlSigningTest.php
index 7e3360475..744c9fe1d 100644
--- a/tests/Integration/Routing/UrlSigningTest.php
+++ b/tests/Integration/Routing/UrlSigningTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Routing\UrlSigningTest;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Routing\UrlRoutable;
use Hypervel\Http\Request;
use Hypervel\Routing\Exceptions\InvalidSignatureException;
@@ -18,9 +19,9 @@
class UrlSigningTest extends RoutingTestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set(['app.key' => 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF']);
+ $app->make('config')->set(['app.key' => 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF']);
}
public function testSigningUrl()
diff --git a/tests/Integration/Session/CookieSessionHandlerTest.php b/tests/Integration/Session/CookieSessionHandlerTest.php
index 7a3676907..8a8b7157e 100644
--- a/tests/Integration/Session/CookieSessionHandlerTest.php
+++ b/tests/Integration/Session/CookieSessionHandlerTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Session;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Support\Facades\Route;
use Hypervel\Support\Str;
use Hypervel\Testbench\TestCase;
@@ -41,10 +42,11 @@ public function testCookieSessionInheritsRequestSecureState(): void
$this->assertTrue($secureSessionValueCookie->isSecure());
}
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('app.key', Str::random(32));
- $app['config']->set('session.driver', 'cookie');
- $app['config']->set('session.expire_on_close', true);
+ $config = $app->make('config');
+ $config->set('app.key', Str::random(32));
+ $config->set('session.driver', 'cookie');
+ $config->set('session.expire_on_close', true);
}
}
diff --git a/tests/Integration/Session/SessionPersistenceTest.php b/tests/Integration/Session/SessionPersistenceTest.php
index 097fce7cd..1f53e193e 100644
--- a/tests/Integration/Session/SessionPersistenceTest.php
+++ b/tests/Integration/Session/SessionPersistenceTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Session;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Session\NullSessionHandler;
use Hypervel\Session\TokenMismatchException;
use Hypervel\Support\Facades\Exceptions;
@@ -53,11 +54,12 @@ public function testPersistentSaveFailureIsRenderedWithoutRetryFailureEscaping()
$this->assertSame(2, $handler->writeCount);
}
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('app.key', Str::random(32));
- $app['config']->set('session.driver', 'fake-null');
- $app['config']->set('session.expire_on_close', true);
+ $config = $app->make('config');
+ $config->set('app.key', Str::random(32));
+ $config->set('session.driver', 'fake-null');
+ $config->set('session.expire_on_close', true);
}
}
diff --git a/tests/Integration/Support/MultipleInstanceManagerTest.php b/tests/Integration/Support/MultipleInstanceManagerTest.php
index 6824a4318..a0b43d77e 100644
--- a/tests/Integration/Support/MultipleInstanceManagerTest.php
+++ b/tests/Integration/Support/MultipleInstanceManagerTest.php
@@ -10,6 +10,7 @@
use Hypervel\Tests\Integration\Support\Fixtures\MultipleInstanceManager;
use Mockery as m;
use RuntimeException;
+use stdClass;
class MultipleInstanceManagerTest extends TestCase
{
@@ -69,4 +70,19 @@ public function testCustomDriverClosureBoundObjectIsMultipleInstanceManager()
$manager->extend('custom', fn () => $this);
$this->assertSame($manager, $manager->instance('custom'));
}
+
+ public function testForgetInstancesClearsEveryInstanceAndPreservesCustomCreators(): void
+ {
+ $manager = new MultipleInstanceManager($this->app);
+ $manager->extend('custom', fn () => new stdClass);
+ $foo = $manager->instance('foo');
+ $bar = $manager->instance('bar');
+ $custom = $manager->instance('custom');
+
+ $this->assertSame($manager, $manager->forgetInstances());
+
+ $this->assertNotSame($foo, $manager->instance('foo'));
+ $this->assertNotSame($bar, $manager->instance('bar'));
+ $this->assertNotSame($custom, $manager->instance('custom'));
+ }
}
diff --git a/tests/Integration/Translation/TranslatorTest.php b/tests/Integration/Translation/TranslatorTest.php
index e275bcad5..e08fae880 100644
--- a/tests/Integration/Translation/TranslatorTest.php
+++ b/tests/Integration/Translation/TranslatorTest.php
@@ -14,58 +14,66 @@ class TranslatorTest extends TestCase
{
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['translator']->addNamespace('tests', __DIR__ . '/Fixtures/lang');
- $app['translator']->addJsonPath(__DIR__ . '/Fixtures/lang');
+ $translator = $app->make('translator');
+
+ $translator->addNamespace('tests', __DIR__ . '/Fixtures/lang');
+ $translator->addJsonPath(__DIR__ . '/Fixtures/lang');
}
public function testItCanGetFromLocaleForJson(): void
{
- $this->assertSame('30 Days', $this->app['translator']->get('30 Days'));
+ $translator = $this->app->make('translator');
+
+ $this->assertSame('30 Days', $translator->get('30 Days'));
$this->app->setLocale('fr');
- $this->assertSame('30 jours', $this->app['translator']->get('30 Days'));
+ $this->assertSame('30 jours', $translator->get('30 Days'));
}
public function testItCanCheckLanguageExistsHasFromLocaleForJson(): void
{
- $this->assertTrue($this->app['translator']->has('1 Day'));
- $this->assertTrue($this->app['translator']->hasForLocale('1 Day'));
- $this->assertTrue($this->app['translator']->hasForLocale('30 Days'));
+ $translator = $this->app->make('translator');
+
+ $this->assertTrue($translator->has('1 Day'));
+ $this->assertTrue($translator->hasForLocale('1 Day'));
+ $this->assertTrue($translator->hasForLocale('30 Days'));
$this->app->setLocale('fr');
- $this->assertFalse($this->app['translator']->has('1 Day'));
- $this->assertFalse($this->app['translator']->hasForLocale('1 Day'));
- $this->assertTrue($this->app['translator']->hasForLocale('30 Days'));
+ $this->assertFalse($translator->has('1 Day'));
+ $this->assertFalse($translator->hasForLocale('1 Day'));
+ $this->assertTrue($translator->hasForLocale('30 Days'));
}
public function testItCanCheckKeyExistsWithoutTriggeringHandleMissingKeys(): void
{
$missingKey = null;
+ $translator = $this->app->make('translator');
- $this->app['translator']->handleMissingKeysUsing(function (string $key) use (&$missingKey): void {
+ $translator->handleMissingKeysUsing(function (string $key) use (&$missingKey): void {
$missingKey = $key;
});
- $this->assertFalse($this->app['translator']->has('Foo Bar'));
+ $this->assertFalse($translator->has('Foo Bar'));
$this->assertNull($missingKey);
- $this->assertFalse($this->app['translator']->hasForLocale('Foo Bar', 'nl'));
+ $this->assertFalse($translator->hasForLocale('Foo Bar', 'nl'));
$this->assertNull($missingKey);
}
public function testItCanHandleMissingKeysUsingCallback(): void
{
$missingKey = null;
+ $translator = $this->app->make('translator');
- $this->app['translator']->handleMissingKeysUsing(function (string $key) use (&$missingKey): string {
+ $translator->handleMissingKeysUsing(function (string $key) use (&$missingKey): string {
$missingKey = $key;
return 'callback key';
});
- $key = $this->app['translator']->get('some missing key');
+ $key = $translator->get('some missing key');
$this->assertSame('callback key', $key);
$this->assertSame('some missing key', $missingKey);
@@ -74,12 +82,13 @@ public function testItCanHandleMissingKeysUsingCallback(): void
public function testItCanHandleMissingKeysNoReturn(): void
{
$missingKey = null;
+ $translator = $this->app->make('translator');
- $this->app['translator']->handleMissingKeysUsing(function (string $key) use (&$missingKey): void {
+ $translator->handleMissingKeysUsing(function (string $key) use (&$missingKey): void {
$missingKey = $key;
});
- $key = $this->app['translator']->get('some missing key');
+ $key = $translator->get('some missing key');
$this->assertSame('some missing key', $key);
$this->assertSame('some missing key', $missingKey);
@@ -88,12 +97,13 @@ public function testItCanHandleMissingKeysNoReturn(): void
public function testItReturnsCorrectLocaleForMissingKeys(): void
{
$missingLocale = null;
+ $translator = $this->app->make('translator');
- $this->app['translator']->handleMissingKeysUsing(function (string $key, array $replacements, string $locale) use (&$missingLocale): void {
+ $translator->handleMissingKeysUsing(function (string $key, array $replacements, string $locale) use (&$missingLocale): void {
$missingLocale = $locale;
});
- $this->app['translator']->get('some missing key', [], 'ht');
+ $translator->get('some missing key', [], 'ht');
$this->assertSame('ht', $missingLocale);
}
@@ -101,12 +111,13 @@ public function testItReturnsCorrectLocaleForMissingKeys(): void
public function testFileValidationDoesNotAttemptToTranslateAlreadyTranslatedMessages(): void
{
$keysLookedUp = [];
+ $translator = $this->app->make('translator');
- $this->app['translator']->handleMissingKeysUsing(function (string $key) use (&$keysLookedUp): void {
+ $translator->handleMissingKeysUsing(function (string $key) use (&$keysLookedUp): void {
$keysLookedUp[] = $key;
});
- $validator = $this->app['validator']->make(
+ $validator = $this->app->make('validator')->make(
['file' => UploadedFile::fake()->create('file.pdf')],
['file' => [File::types(['txt'])]]
);
@@ -127,7 +138,7 @@ public function testItCanHandleChoice(int $count, string $expected, ?string $loc
$this->assertSame(
strtr($expected, [':name' => $name, ':count' => $count]),
- $this->app['translator']->choice('tests::app.greeting', $count, ['name' => $name])
+ $this->app->make('translator')->choice('tests::app.greeting', $count, ['name' => $name])
);
}
@@ -142,7 +153,7 @@ public function testItCanHandleChoiceWithChoiceSeparatorInReplaceString(int $cou
$this->assertSame(
strtr($expected, [':name' => $name, ':count' => $count]),
- $this->app['translator']->choice('tests::app.greeting', $count, ['name' => $name])
+ $this->app->make('translator')->choice('tests::app.greeting', $count, ['name' => $name])
);
}
diff --git a/tests/Integration/View/BladeAnonymousComponentTest.php b/tests/Integration/View/BladeAnonymousComponentTest.php
index 2cd81b1b9..825d87f2c 100644
--- a/tests/Integration/View/BladeAnonymousComponentTest.php
+++ b/tests/Integration/View/BladeAnonymousComponentTest.php
@@ -46,6 +46,6 @@ public function testAnonymousComponentsWithCustomPathsCantBeRenderedAsNormalView
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('view.paths', [__DIR__ . '/anonymous-components-templates']);
+ $app->make('config')->set('view.paths', [__DIR__ . '/anonymous-components-templates']);
}
}
diff --git a/tests/Integration/View/BladeTest.php b/tests/Integration/View/BladeTest.php
index c0d0ef113..d4e67fcda 100644
--- a/tests/Integration/View/BladeTest.php
+++ b/tests/Integration/View/BladeTest.php
@@ -250,7 +250,7 @@ public function testViewCacheCommandDeduplicatesPathsBeforeCompiling(): void
#[Override]
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('view.paths', [__DIR__ . '/templates']);
+ $app->make('config')->set('view.paths', [__DIR__ . '/templates']);
}
}
diff --git a/tests/Integration/View/RenderableViewExceptionTest.php b/tests/Integration/View/RenderableViewExceptionTest.php
index c2eb7e13e..812667844 100644
--- a/tests/Integration/View/RenderableViewExceptionTest.php
+++ b/tests/Integration/View/RenderableViewExceptionTest.php
@@ -27,7 +27,7 @@ public function testRenderMethodOfExceptionThrownInViewGetsHandled(): void
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('view.paths', [__DIR__ . '/templates']);
+ $app->make('config')->set('view.paths', [__DIR__ . '/templates']);
}
}
diff --git a/tests/Jwt/ClaimFactoryTest.php b/tests/Jwt/ClaimFactoryTest.php
index 84dd9ab26..92e835b31 100644
--- a/tests/Jwt/ClaimFactoryTest.php
+++ b/tests/Jwt/ClaimFactoryTest.php
@@ -199,6 +199,33 @@ public function testRejectsReservedRefreshClaims(): void
);
}
+ public function testConfigurationCanBeReloaded(): void
+ {
+ $config = new Repository([
+ 'jwt' => [
+ 'issuer' => null,
+ 'lock_subject' => false,
+ ],
+ ]);
+ $factory = new ClaimFactory($config);
+ $user = new ClaimFactoryUser(42);
+ $provider = new ClaimFactoryModelProvider(ClaimFactoryUser::class);
+
+ $claims = $factory->make($user, $provider, null);
+ $this->assertArrayNotHasKey('iss', $claims);
+ $this->assertArrayNotHasKey('prv', $claims);
+
+ $config->set([
+ 'jwt.issuer' => 'https://api.example.test',
+ 'jwt.lock_subject' => true,
+ ]);
+ $factory->reloadConfiguration();
+
+ $claims = $factory->make($user, $provider, null);
+ $this->assertSame('https://api.example.test', $claims['iss']);
+ $this->assertSame(hash('xxh128', ClaimFactoryUser::class), $claims['prv']);
+ }
+
public function testFlushStateClearsModelHashCache(): void
{
$factory = $this->factory(['jwt' => ['issuer' => null, 'lock_subject' => true]]);
diff --git a/tests/Jwt/JwtManagerTest.php b/tests/Jwt/JwtManagerTest.php
index 23a85bc47..58f60a63f 100644
--- a/tests/Jwt/JwtManagerTest.php
+++ b/tests/Jwt/JwtManagerTest.php
@@ -6,8 +6,10 @@
use Hypervel\Config\Repository;
use Hypervel\Contracts\Container\Container;
+use Hypervel\Foundation\Application;
use Hypervel\Jwt\ClaimFactory;
use Hypervel\Jwt\Contracts\BlacklistContract;
+use Hypervel\Jwt\Contracts\ProviderContract;
use Hypervel\Jwt\Exceptions\JwtException;
use Hypervel\Jwt\Exceptions\TokenBlacklistedException;
use Hypervel\Jwt\Exceptions\TokenExpiredException;
@@ -25,6 +27,7 @@
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\DataProvider;
+use ReflectionProperty;
use Symfony\Component\Uid\Uuid;
class JwtManagerTest extends TestCase
@@ -128,6 +131,40 @@ public function testConstructorDoesNotResolveBlacklistWhenBlacklistIsDisabled():
$this->assertFalse($manager->hasBlacklistEnabled());
}
+ public function testConfigurationCanBeReloadedWithoutLosingCustomCreators(): void
+ {
+ $application = new Application;
+ $config = new Repository([
+ 'jwt' => [
+ 'blacklist_enabled' => false,
+ 'driver' => 'custom',
+ 'validations' => [ValidationStub::class],
+ ],
+ ]);
+ $application->instance('config', $config);
+ $manager = new JwtManager($application, m::mock(ClaimFactory::class));
+ $manager->extend('custom', static fn (): ProviderContract => m::mock(ProviderContract::class));
+
+ $firstDriver = $manager->driver();
+ $firstDriver->shouldReceive('decode')->once()->with('token')->andReturn([]);
+ $manager->decode('token');
+
+ $validations = new ReflectionProperty(JwtManager::class, 'validations');
+ $blacklist = new ReflectionProperty(JwtManager::class, 'blacklist');
+ $this->assertNotSame([], $validations->getValue($manager));
+
+ $replacementBlacklist = m::mock(BlacklistContract::class);
+ $application->instance(BlacklistContract::class, $replacementBlacklist);
+ $config->set('jwt.blacklist_enabled', true);
+
+ $manager->reloadConfiguration();
+
+ $this->assertTrue($manager->hasBlacklistEnabled());
+ $this->assertSame($replacementBlacklist, $blacklist->getValue($manager));
+ $this->assertSame([], $validations->getValue($manager));
+ $this->assertNotSame($firstDriver, $manager->driver());
+ }
+
public function testDecodeAToken(): void
{
$token = 'foo.bar.baz';
diff --git a/tests/Jwt/JwtServiceProviderTest.php b/tests/Jwt/JwtServiceProviderTest.php
index c76e9ad01..7d07ad9a7 100644
--- a/tests/Jwt/JwtServiceProviderTest.php
+++ b/tests/Jwt/JwtServiceProviderTest.php
@@ -15,8 +15,10 @@
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Http\Request;
use Hypervel\Jwt\Blacklist;
+use Hypervel\Jwt\ClaimFactory;
use Hypervel\Jwt\Contracts\BlacklistContract;
use Hypervel\Jwt\Contracts\ManagerContract;
+use Hypervel\Jwt\Contracts\ProviderContract;
use Hypervel\Jwt\Contracts\StorageContract;
use Hypervel\Jwt\Http\Parser\Cookie;
use Hypervel\Jwt\Http\Parser\Parser;
@@ -29,6 +31,7 @@
use Hypervel\Support\Facades\Date;
use Hypervel\Testbench\TestCase;
use Mockery as m;
+use ReflectionProperty;
use RuntimeException;
class JwtServiceProviderTest extends TestCase
@@ -339,6 +342,73 @@ public function testOmittedStorageProviderUsesTheTaggedCacheDefault(): void
$this->assertInstanceOf(JwtManager::class, $this->app->make('jwt'));
}
+ public function testReloadConfigurationRefreshesResolvedJwtServices(): void
+ {
+ $config = $this->app->make('config');
+ $config->set([
+ 'jwt.issuer' => 'old-issuer',
+ 'jwt.lock_subject' => false,
+ 'jwt.blacklist_enabled' => false,
+ 'jwt.driver' => 'reload-test',
+ 'jwt.token' => 'old_token',
+ 'jwt.parser' => [Cookie::class],
+ 'jwt.providers.storage' => JwtServiceProviderCustomStorage::class,
+ 'jwt.blacklist_grace_period' => 0,
+ 'jwt.refresh_ttl' => 60,
+ 'jwt.leeway' => 0,
+ ]);
+
+ $claimFactory = $this->app->make(ClaimFactory::class);
+ $parser = $this->app->make(Parser::class);
+ $manager = $this->app->make('jwt');
+ $manager->extend('reload-test', static fn (): ProviderContract => new JwtServiceProviderDriverStub);
+ $driver = $manager->driver();
+
+ $this->assertSame('old-token', $parser->parseToken(Request::create('/', 'GET', cookies: [
+ 'old_token' => 'old-token',
+ ])));
+
+ $config->set([
+ 'jwt.issuer' => 'new-issuer',
+ 'jwt.lock_subject' => true,
+ 'jwt.blacklist_enabled' => true,
+ 'jwt.token' => 'new_token',
+ ]);
+
+ $this->app->getProvider(JwtServiceProvider::class)->reloadConfiguration();
+
+ $issuer = new ReflectionProperty(ClaimFactory::class, 'issuer');
+ $lockSubject = new ReflectionProperty(ClaimFactory::class, 'lockSubject');
+ $this->assertSame($claimFactory, $this->app->make(ClaimFactory::class));
+ $this->assertSame('new-issuer', $issuer->getValue($claimFactory));
+ $this->assertTrue($lockSubject->getValue($claimFactory));
+ $this->assertNotSame($parser, $this->app->make(Parser::class));
+ $this->assertSame('new-token', $this->app->make(Parser::class)->parseToken(Request::create('/', 'GET', cookies: [
+ 'new_token' => 'new-token',
+ ])));
+ $this->assertSame($manager, $this->app->make(JwtManager::class));
+ $this->assertTrue($manager->hasBlacklistEnabled());
+ $this->assertNotSame($driver, $manager->driver());
+ $this->assertInstanceOf(Blacklist::class, $this->app->make(BlacklistContract::class));
+ }
+
+ public function testReloadConfigurationDoesNotResolveUnusedJwtServices(): void
+ {
+ $provider = $this->app->getProvider(JwtServiceProvider::class);
+
+ $this->assertFalse($this->app->resolved(ClaimFactory::class));
+ $this->assertFalse($this->app->resolved(Parser::class));
+ $this->assertFalse($this->app->resolved(BlacklistContract::class));
+ $this->assertFalse($this->app->resolved('jwt'));
+
+ $provider->reloadConfiguration();
+
+ $this->assertFalse($this->app->resolved(ClaimFactory::class));
+ $this->assertFalse($this->app->resolved(Parser::class));
+ $this->assertFalse($this->app->resolved(BlacklistContract::class));
+ $this->assertFalse($this->app->resolved('jwt'));
+ }
+
protected function taggableStore(TagMode $mode): TaggableStore
{
/** @var TaggableStore $store */
@@ -350,6 +420,19 @@ protected function taggableStore(TagMode $mode): TaggableStore
}
}
+class JwtServiceProviderDriverStub implements ProviderContract
+{
+ public function encode(array $payload): string
+ {
+ return '';
+ }
+
+ public function decode(string $token): array
+ {
+ return [];
+ }
+}
+
class JwtServiceProviderCustomStorage implements StorageContract
{
public ?int $minutes = null;
diff --git a/tests/Log/ContextQueueTest.php b/tests/Log/ContextQueueTest.php
index 4ae5d36bb..15d8536a8 100644
--- a/tests/Log/ContextQueueTest.php
+++ b/tests/Log/ContextQueueTest.php
@@ -193,7 +193,7 @@ public function testContextIsHydratedWhenJobProcesses(): void
$job->shouldReceive('payload')->andReturn($payload);
$event = new JobProcessing('sync', $job);
- $this->app['events']->dispatch($event);
+ $this->app->make('events')->dispatch($event);
// Context should now be hydrated
$this->assertSame('abc-123', Repository::getInstance()->get('trace_id'));
@@ -206,7 +206,7 @@ public function testHydrateSkipsWhenPayloadHasNoContext(): void
$job->shouldReceive('payload')->andReturn(['job' => 'SomeJob']);
$event = new JobProcessing('sync', $job);
- $this->app['events']->dispatch($event);
+ $this->app->make('events')->dispatch($event);
// No context Repository should have been allocated
$this->assertFalse(Repository::hasInstance());
@@ -221,7 +221,7 @@ public function testPayloadWithoutContextFlushesAnExistingRepository(): void
$job = m::mock(\Hypervel\Contracts\Queue\Job::class);
$job->shouldReceive('payload')->andReturn(['job' => 'SomeJob']);
- $this->app['events']->dispatch(new JobProcessing('sync', $job));
+ $this->app->make('events')->dispatch(new JobProcessing('sync', $job));
$this->assertSame($repository, Repository::getInstance());
$this->assertSame([], $repository->all());
@@ -266,7 +266,7 @@ public function testHydratedHookFiresWhenJobProcesses(): void
$job = m::mock(\Hypervel\Contracts\Queue\Job::class);
$job->shouldReceive('payload')->andReturn($payload);
- $this->app['events']->dispatch(new JobProcessing('sync', $job));
+ $this->app->make('events')->dispatch(new JobProcessing('sync', $job));
$this->assertTrue($called);
}
@@ -311,7 +311,7 @@ public function testRoundTripPreservesVariousDataTypes(): void
// Hydrate from the payload
$job = m::mock(\Hypervel\Contracts\Queue\Job::class);
$job->shouldReceive('payload')->andReturn($payload);
- $this->app['events']->dispatch(new JobProcessing('sync', $job));
+ $this->app->make('events')->dispatch(new JobProcessing('sync', $job));
// Verify all types survived the round trip
$this->assertSame('hello', Repository::getInstance()->get('string'));
diff --git a/tests/Log/LogManagerTest.php b/tests/Log/LogManagerTest.php
index 05ffeca5d..cb1162ccd 100644
--- a/tests/Log/LogManagerTest.php
+++ b/tests/Log/LogManagerTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Log;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Log\Context\ResolvedContextLogProcessor;
use Hypervel\Log\Handlers\FingersCrossedHandler as HypervelFingersCrossedHandler;
use Hypervel\Log\Handlers\RotatingFileHandler as HypervelRotatingFileHandler;
@@ -34,7 +35,7 @@
class LogManagerTest extends TestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
$app->make('config')->set('logging.channels.single', [
'driver' => 'single',
@@ -340,7 +341,7 @@ public function testDailyDriverUsesCoroutineSafeRotatingHandler(): void
$this->assertSame(HypervelRotatingFileHandler::class, get_class($handler));
}
- public function testItUtilisesTheNullDriverDuringTestsWhenNullDriverUsed()
+ public function testItUtilizesTheNullDriverDuringTestsWhenNullDriverUsed(): void
{
$manager = new class($this->app) extends LogManager {
protected function createEmergencyLogger(): LoggerInterface
@@ -349,7 +350,7 @@ protected function createEmergencyLogger(): LoggerInterface
}
};
- $this->app['env'] = 'testing';
+ $this->app->instance('env', 'testing');
$config = $this->app->make('config');
$config->set('logging.default', null);
$config->set('logging.channels.null', [
@@ -369,7 +370,7 @@ protected function createEmergencyLogger(): LoggerInterface
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Emergency logger was created.');
- $this->app['env'] = 'production';
+ $this->app->instance('env', 'production');
$manager->info('message');
}
@@ -524,6 +525,22 @@ public function testLogManagerPurgeResolvedChannels()
$this->assertEmpty($manager->getChannels());
}
+ public function testForgetChannelsClearsEveryChannelAndPreservesCustomCreators(): void
+ {
+ $config = $this->app->make('config');
+ $config->set('logging.channels.first', ['driver' => 'custom']);
+ $config->set('logging.channels.second', ['driver' => 'custom']);
+ $manager = new LogManager($this->app);
+ $manager->extend('custom', static fn () => new LoggerSpy);
+ $first = $manager->channel('first');
+ $second = $manager->channel('second');
+
+ $this->assertSame($manager, $manager->forgetChannels());
+
+ $this->assertNotSame($first, $manager->channel('first'));
+ $this->assertNotSame($second, $manager->channel('second'));
+ }
+
public function testLogManagerCanBuildOnDemandChannel()
{
$manager = new LogManager($this->app);
diff --git a/tests/Log/LogServiceProviderTest.php b/tests/Log/LogServiceProviderTest.php
new file mode 100644
index 000000000..02289b852
--- /dev/null
+++ b/tests/Log/LogServiceProviderTest.php
@@ -0,0 +1,63 @@
+ [
+ 'stdout_log' => [
+ 'level' => [LogLevel::ERROR],
+ 'format' => 'line',
+ ],
+ ],
+ ]);
+ $output = new BufferedOutput;
+ $logger = new StdoutLogger($config, $output);
+ $manager = m::mock(LogManager::class);
+ $manager->shouldReceive('forgetChannels')->once()->andReturnSelf();
+ $application = m::mock(Application::class);
+ $application->shouldReceive('resolved')->once()->with('log')->andReturnTrue();
+ $application->shouldReceive('make')->once()->with('log')->andReturn($manager);
+ $application->shouldReceive('resolved')->once()->with(StdoutLoggerInterface::class)->andReturnTrue();
+ $application->shouldReceive('make')->once()->with(StdoutLoggerInterface::class)->andReturn($logger);
+
+ $config->set('app.stdout_log.level', [LogLevel::INFO]);
+ $config->set('app.stdout_log.format', 'json');
+ (new LogServiceProvider($application))->reloadConfiguration();
+ $logger->info('Refreshed.');
+
+ $this->assertSame('Refreshed.', json_decode(
+ $output->fetch(),
+ true,
+ flags: JSON_THROW_ON_ERROR,
+ )['message']);
+ }
+
+ public function testReloadConfigurationLeavesAnApplicationStdoutLoggerAlone(): void
+ {
+ $logger = m::mock(StdoutLoggerInterface::class);
+ $logger->shouldNotReceive('reloadConfiguration');
+ $application = m::mock(Application::class);
+ $application->shouldReceive('resolved')->once()->with('log')->andReturnFalse();
+ $application->shouldReceive('resolved')->once()->with(StdoutLoggerInterface::class)->andReturnTrue();
+ $application->shouldReceive('make')->once()->with(StdoutLoggerInterface::class)->andReturn($logger);
+
+ (new LogServiceProvider($application))->reloadConfiguration();
+ }
+}
diff --git a/tests/Mail/MailServiceProviderTest.php b/tests/Mail/MailServiceProviderTest.php
new file mode 100644
index 000000000..a05dccd29
--- /dev/null
+++ b/tests/Mail/MailServiceProviderTest.php
@@ -0,0 +1,64 @@
+ 'first',
+ 'mail.mailers.first' => ['transport' => 'array'],
+ 'mail.mailers.second' => ['transport' => 'array'],
+ 'mail.markdown.theme' => 'old',
+ 'mail.markdown.paths' => [],
+ 'mail.markdown.extensions' => [],
+ ]);
+ $manager = $this->app->make('mail.manager');
+ $mailer = $this->app->make('mailer');
+ $markdown = $this->app->make(Markdown::class);
+
+ config([
+ 'mail.default' => 'second',
+ 'mail.markdown.theme' => 'new',
+ ]);
+ $this->app->getProvider(MailServiceProvider::class)->reloadConfiguration();
+
+ $refreshedMailer = $this->app->make('mailer');
+ $refreshedMarkdown = $this->app->make(Markdown::class);
+ $this->assertSame($manager, $this->app->make(MailManager::class));
+ $this->assertNotSame($mailer, $refreshedMailer);
+ $this->assertSame($refreshedMailer, $manager->mailer('second'));
+ $this->assertNotSame($markdown, $refreshedMarkdown);
+ $this->assertSame('new', (new ReflectionProperty(Markdown::class, 'theme'))->getValue($refreshedMarkdown));
+ }
+
+ public function testReloadConfigurationPreservesMailFakeAndRefreshesItsWrappedManager(): void
+ {
+ config([
+ 'mail.default' => 'first',
+ 'mail.mailers.first' => ['transport' => 'array'],
+ ]);
+ $manager = $this->app->make('mail.manager');
+ $mailer = $manager->mailer('first');
+ $fake = Mail::fake();
+ $mailable = new Mailable;
+ $fake->send($mailable);
+
+ $this->app->getProvider(MailServiceProvider::class)->reloadConfiguration();
+
+ $this->assertSame($fake, Mail::getFacadeRoot());
+ $this->assertSame([$mailable], $fake->sent(Mailable::class)->all());
+ $this->assertNotSame($mailer, $manager->mailer('first'));
+ }
+}
diff --git a/tests/Notifications/NotificationServiceProviderTest.php b/tests/Notifications/NotificationServiceProviderTest.php
new file mode 100644
index 000000000..e8d245ee3
--- /dev/null
+++ b/tests/Notifications/NotificationServiceProviderTest.php
@@ -0,0 +1,47 @@
+app->make(ChannelManager::class);
+ $channel = $manager->channel('mail');
+
+ $this->app->getProvider(NotificationServiceProvider::class)->reloadConfiguration();
+
+ $refreshedChannel = $manager->channel('mail');
+ $this->assertSame($manager, $this->app->make(ChannelManager::class));
+ $this->assertNotSame($channel, $refreshedChannel);
+ $this->assertSame($refreshedChannel, $this->app->make(MailChannel::class));
+ }
+
+ public function testReloadConfigurationPreservesNotificationFakeAndItsRecordedState(): void
+ {
+ $fake = NotificationFacade::fake();
+ $notification = new class extends Notification {
+ public function via(mixed $notifiable): array
+ {
+ return ['mail'];
+ }
+ };
+ $fake->sendNow(new AnonymousNotifiable, $notification);
+ $recordedNotifications = $fake->sentNotifications();
+
+ $this->app->getProvider(NotificationServiceProvider::class)->reloadConfiguration();
+
+ $this->assertSame($fake, NotificationFacade::getFacadeRoot());
+ $this->assertSame($recordedNotifications, $fake->sentNotifications());
+ }
+}
diff --git a/tests/ObjectPool/ObjectPoolServiceProviderTest.php b/tests/ObjectPool/ObjectPoolServiceProviderTest.php
index 04eef6885..29349f00e 100644
--- a/tests/ObjectPool/ObjectPoolServiceProviderTest.php
+++ b/tests/ObjectPool/ObjectPoolServiceProviderTest.php
@@ -4,13 +4,20 @@
namespace Hypervel\Tests\ObjectPool;
+use Hypervel\Contracts\Events\Dispatcher;
+use Hypervel\Contracts\Foundation\Application;
+use Hypervel\Core\Events\AfterWorkerStart;
+use Hypervel\Core\Events\BeforeServerFork;
use Hypervel\ObjectPool\Contracts\Factory;
use Hypervel\ObjectPool\Contracts\Recycler;
+use Hypervel\ObjectPool\Listeners\StartRecycler;
use Hypervel\ObjectPool\ObjectPoolServiceProvider;
use Hypervel\ObjectPool\PoolManager;
use Hypervel\ObjectPool\PoolRecycler;
use Hypervel\Testbench\TestCase;
+use Mockery as m;
use stdClass;
+use Swoole\Server;
class ObjectPoolServiceProviderTest extends TestCase
{
@@ -36,4 +43,49 @@ public function testConcreteRecyclerAndContractShareOneTimerOwner(): void
$this->assertSame($recycler, $this->app->make(Recycler::class));
$this->assertSame(2.5, $this->app->make(Recycler::class)->getInterval());
}
+
+ public function testLifecycleFlushesResolvedMasterPoolsBeforeForkAndStartsTheWorkerRecycler(): void
+ {
+ $listeners = [];
+ $events = m::mock(Dispatcher::class);
+ $events->shouldReceive('listen')
+ ->twice()
+ ->andReturnUsing(function (string $event, callable $listener) use (&$listeners): void {
+ $listeners[$event] = $listener;
+ });
+ $manager = m::mock(PoolManager::class);
+ $manager->shouldReceive('flush')->once();
+ $recycler = m::mock(StartRecycler::class);
+ $server = m::mock(Server::class);
+ $afterWorkerStart = new AfterWorkerStart($server, 0);
+ $recycler->shouldReceive('handle')->once()->with($afterWorkerStart);
+ $application = m::mock(Application::class);
+ $application->shouldReceive('make')->once()->with('events')->andReturn($events);
+ $application->shouldReceive('resolved')->once()->with(PoolManager::class)->andReturnTrue();
+ $application->shouldReceive('make')->once()->with(PoolManager::class)->andReturn($manager);
+ $application->shouldReceive('make')->once()->with(StartRecycler::class)->andReturn($recycler);
+
+ (new ObjectPoolServiceProvider($application))->boot();
+
+ $listeners[BeforeServerFork::class](new BeforeServerFork($server));
+ $listeners[AfterWorkerStart::class]($afterWorkerStart);
+ }
+
+ public function testBeforeForkDoesNotResolveAnUnusedPoolManager(): void
+ {
+ $listeners = [];
+ $events = m::mock(Dispatcher::class);
+ $events->shouldReceive('listen')
+ ->twice()
+ ->andReturnUsing(function (string $event, callable $listener) use (&$listeners): void {
+ $listeners[$event] = $listener;
+ });
+ $application = m::mock(Application::class);
+ $application->shouldReceive('make')->once()->with('events')->andReturn($events);
+ $application->shouldReceive('resolved')->once()->with(PoolManager::class)->andReturnFalse();
+
+ (new ObjectPoolServiceProvider($application))->boot();
+
+ $listeners[BeforeServerFork::class](new BeforeServerFork(m::mock(Server::class)));
+ }
}
diff --git a/tests/Permission/PermissionServiceProviderTest.php b/tests/Permission/PermissionServiceProviderTest.php
new file mode 100644
index 000000000..74ac56111
--- /dev/null
+++ b/tests/Permission/PermissionServiceProviderTest.php
@@ -0,0 +1,45 @@
+app->make(PermissionRegistrar::class);
+
+ $this->assertSame('hypervel.permission.cache.roles', $registrar->getCacheKey());
+
+ $this->app->make('config')->set('permission.cache.keys.roles', 'permissions.refreshed');
+ $this->app->getProvider(PermissionServiceProvider::class)->reloadConfiguration();
+
+ $this->assertSame($registrar, $this->app->make(PermissionRegistrar::class));
+ $this->assertSame('permissions.refreshed', $registrar->getCacheKey());
+ }
+
+ public function testReloadConfigurationDoesNotResolveAnUnusedRegistrar(): void
+ {
+ $application = new Application;
+ $application->instance('config', new Repository);
+ $provider = new PermissionServiceProvider($application);
+ $provider->register();
+
+ $provider->reloadConfiguration();
+
+ $this->assertFalse($application->resolved(PermissionRegistrar::class));
+ }
+}
diff --git a/tests/Queue/QueueCommandIdentifierTest.php b/tests/Queue/QueueCommandIdentifierTest.php
index b8b07ebb0..ef958bd03 100644
--- a/tests/Queue/QueueCommandIdentifierTest.php
+++ b/tests/Queue/QueueCommandIdentifierTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Queue;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Queue\ClearableQueue;
use Hypervel\Contracts\Queue\Queue;
use Hypervel\Queue\Console\ClearCommand;
@@ -18,11 +19,13 @@
class QueueCommandIdentifierTest extends TestCase
{
- protected function defineEnvironment($app): void
+ protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('queue.default', 'redis');
- $app['config']->set('queue.connections.redis.queue', 'default');
- $app['config']->set('queue.connections.0.queue', 'zero-default');
+ $config = $app->make('config');
+
+ $config->set('queue.default', 'redis');
+ $config->set('queue.connections.redis.queue', 'default');
+ $config->set('queue.connections.0.queue', 'zero-default');
}
#[DataProvider('queueIdentifierProvider')]
@@ -59,7 +62,7 @@ public function testListenCommandPreservesZeroAndDefaultsEmptyIdentifiers(
string $expectedConnection,
string $expectedQueue,
): void {
- $this->app['config']->set("queue.connections.{$expectedConnection}.queue", $expectedQueue);
+ $this->app->make('config')->set("queue.connections.{$expectedConnection}.queue", $expectedQueue);
$listener = m::mock(Listener::class);
$listener->shouldReceive('setOutputHandler')->once();
diff --git a/tests/Queue/QueueManagerTest.php b/tests/Queue/QueueManagerTest.php
index d8a3374fd..2469c4f2a 100644
--- a/tests/Queue/QueueManagerTest.php
+++ b/tests/Queue/QueueManagerTest.php
@@ -332,6 +332,25 @@ public function testSetApplicationUpdatesCachedDirectQueueInPlace(): void
$this->assertSame($queue, $manager->connection('sync'));
}
+ public function testForgetConnectionsClearsEveryConnectionAndPreservesConnectors(): void
+ {
+ $container = $this->getContainer();
+ $config = $container->make('config');
+ $config->set('queue.connections.first', ['driver' => 'custom']);
+ $config->set('queue.connections.second', ['driver' => 'custom']);
+ $manager = new QueueManager($container);
+ $connector = m::mock(ConnectorInterface::class);
+ $connector->shouldReceive('connect')->times(4)->andReturnUsing(fn () => new NullQueue);
+ $manager->addConnector('custom', fn () => $connector);
+ $first = $manager->connection('first');
+ $second = $manager->connection('second');
+
+ $this->assertSame($manager, $manager->forgetConnections());
+
+ $this->assertNotSame($first, $manager->connection('first'));
+ $this->assertNotSame($second, $manager->connection('second'));
+ }
+
protected function getContainer(): Container
{
$container = new Container;
diff --git a/tests/Queue/QueueServiceProviderTest.php b/tests/Queue/QueueServiceProviderTest.php
index cae69fa48..0fc9619a6 100644
--- a/tests/Queue/QueueServiceProviderTest.php
+++ b/tests/Queue/QueueServiceProviderTest.php
@@ -4,17 +4,85 @@
namespace Hypervel\Tests\Queue;
+use Hypervel\Contracts\Debug\ExceptionHandler;
+use Hypervel\Queue\BackgroundQueue;
+use Hypervel\Queue\DeferredQueue;
use Hypervel\Queue\Failed\DatabaseFailedJobProvider;
use Hypervel\Queue\Failed\DatabaseUuidFailedJobProvider;
use Hypervel\Queue\Failed\FileFailedJobProvider;
use Hypervel\Queue\Failed\NullFailedJobProvider;
+use Hypervel\Queue\NullQueue;
+use Hypervel\Queue\QueueManager;
+use Hypervel\Queue\QueueServiceProvider;
+use Hypervel\Queue\SyncQueue;
+use Hypervel\Support\Facades\Queue;
use Hypervel\Testbench\TestCase;
use InvalidArgumentException;
+use Mockery as m;
use PHPUnit\Framework\Attributes\DataProvider;
use ReflectionProperty;
+use RuntimeException;
class QueueServiceProviderTest extends TestCase
{
+ public function testReloadConfigurationRebuildsConnectionsWithExceptionReporting(): void
+ {
+ $handler = m::mock(ExceptionHandler::class);
+ $handler->shouldReceive('report')->twice()->with(m::type(RuntimeException::class));
+ $this->app->instance(ExceptionHandler::class, $handler);
+ config([
+ 'queue.default' => 'sync',
+ 'queue.failed.driver' => 'null',
+ ]);
+ $manager = $this->app->make('queue');
+ $this->assertFalse($manager->connected('background'));
+ $this->assertFalse($manager->connected('deferred'));
+ $connection = $this->app->make('queue.connection');
+ $failedJobs = $this->app->make('queue.failer');
+ $background = $manager->connection('background');
+ $deferred = $manager->connection('deferred');
+ $this->assertInstanceOf(SyncQueue::class, $connection);
+ $this->assertInstanceOf(NullFailedJobProvider::class, $failedJobs);
+
+ config([
+ 'queue.default' => 'null',
+ 'queue.failed.driver' => 'file',
+ ]);
+ $this->app->getProvider(QueueServiceProvider::class)->reloadConfiguration();
+
+ $refreshedBackground = $manager->connection('background');
+ $refreshedDeferred = $manager->connection('deferred');
+ $this->assertSame($manager, $this->app->make(QueueManager::class));
+ $this->assertNotSame($background, $refreshedBackground);
+ $this->assertNotSame($deferred, $refreshedDeferred);
+ $this->assertInstanceOf(NullQueue::class, $this->app->make('queue.connection'));
+ $this->assertInstanceOf(FileFailedJobProvider::class, $this->app->make('queue.failer'));
+
+ $backgroundCallback = (new ReflectionProperty(BackgroundQueue::class, 'exceptionCallback'))
+ ->getValue($refreshedBackground);
+ $deferredCallback = (new ReflectionProperty(DeferredQueue::class, 'exceptionCallback'))
+ ->getValue($refreshedDeferred);
+
+ $this->assertIsCallable($backgroundCallback);
+ $this->assertIsCallable($deferredCallback);
+ $backgroundCallback(new RuntimeException('Background failed.'));
+ $deferredCallback(new RuntimeException('Deferred failed.'));
+ }
+
+ public function testReloadConfigurationPreservesQueueFakeAndRefreshesItsWrappedManager(): void
+ {
+ $manager = $this->app->make('queue');
+ $background = $manager->connection('background');
+ $fake = Queue::fake(['queued-job']);
+ $fake->push('queued-job');
+
+ $this->app->getProvider(QueueServiceProvider::class)->reloadConfiguration();
+
+ $this->assertSame($fake, Queue::getFacadeRoot());
+ $this->assertSame(['queued-job'], $fake->pushed('queued-job')->all());
+ $this->assertNotSame($background, $manager->connection('background'));
+ }
+
#[DataProvider('failedJobProviders')]
public function testFailedJobProviderIsSelectedExplicitly(mixed $driver, string $provider): void
{
diff --git a/tests/RateLimiter/RateLimiterServiceProviderTest.php b/tests/RateLimiter/RateLimiterServiceProviderTest.php
index eeb9c7640..a187f4ae0 100644
--- a/tests/RateLimiter/RateLimiterServiceProviderTest.php
+++ b/tests/RateLimiter/RateLimiterServiceProviderTest.php
@@ -10,6 +10,7 @@
use Hypervel\Core\Events\BeforeServerStart;
use Hypervel\RateLimiter\Listeners\InitializeSwooleTables;
use Hypervel\RateLimiter\Listeners\RegisterPruneTimer;
+use Hypervel\RateLimiter\RateLimiter;
use Hypervel\RateLimiter\RateLimiterServiceProvider;
use Hypervel\Support\DefaultProviders;
use Hypervel\Tests\TestCase;
@@ -18,6 +19,26 @@
class RateLimiterServiceProviderTest extends TestCase
{
+ public function testReloadConfigurationForgetsResolvedStores(): void
+ {
+ $manager = m::mock(RateLimiter::class);
+ $manager->shouldReceive('forgetInstances')->once()->andReturnSelf();
+ $application = m::mock(Application::class);
+ $application->shouldReceive('resolved')->once()->with(RateLimiter::class)->andReturnTrue();
+ $application->shouldReceive('make')->once()->with(RateLimiter::class)->andReturn($manager);
+
+ (new RateLimiterServiceProvider($application))->reloadConfiguration();
+ }
+
+ public function testReloadConfigurationDoesNotResolveAnUnusedManager(): void
+ {
+ $application = m::mock(Application::class);
+ $application->shouldReceive('resolved')->once()->with(RateLimiter::class)->andReturnFalse();
+ $application->shouldNotReceive('make');
+
+ (new RateLimiterServiceProvider($application))->reloadConfiguration();
+ }
+
public function testRateLimiterIsARequiredFrameworkProvider(): void
{
$this->assertContains(
diff --git a/tests/RateLimiter/RateLimiterTest.php b/tests/RateLimiter/RateLimiterTest.php
index 84f0b5be2..1cf8d04be 100644
--- a/tests/RateLimiter/RateLimiterTest.php
+++ b/tests/RateLimiter/RateLimiterTest.php
@@ -60,6 +60,42 @@ public function testManagerForgetsResolvedStores(): void
$this->assertNotSame($resolved, $manager->store('worker-array'));
}
+ public function testForgettingResolvedStoresPreservesRegisteredConfiguration(): void
+ {
+ config([
+ 'rate-limiter.stores.custom' => [
+ 'driver' => 'custom',
+ ],
+ ]);
+
+ $manager = $this->app->make(RateLimiter::class);
+ $callback = static fn (): Limit => Limit::perMinute(1)->by('user');
+ $scopeCalls = [];
+ $created = 0;
+ $manager->extend('custom', static function () use (&$created): WorkerArrayStore {
+ ++$created;
+
+ return new WorkerArrayStore;
+ });
+ $manager->for('api', $callback, 'custom');
+ $manager->resolveKeyScopeUsing(static function (string $name) use (&$scopeCalls): string {
+ $scopeCalls[] = $name;
+
+ return 'tenant:7';
+ });
+ $store = $manager->store('custom');
+
+ $manager->forgetInstances();
+
+ $refreshedStore = $manager->store('custom');
+ $this->assertNotSame($store, $refreshedStore);
+ $this->assertSame(2, $created);
+ $this->assertSame($callback, $manager->limiter('api'));
+ $this->assertSame('custom', $manager->limiterStore('api'));
+ $this->assertTrue($refreshedStore->consume($callback(), 'api')->allowed());
+ $this->assertSame(['api'], $scopeCalls);
+ }
+
public function testNamedLimiterStoresAreRegisteredAndNormalized(): void
{
$manager = $this->app->make(RateLimiter::class);
diff --git a/tests/Reverb/EventDispatcherTest.php b/tests/Reverb/EventDispatcherTest.php
index cdda7a699..94a2c74c1 100644
--- a/tests/Reverb/EventDispatcherTest.php
+++ b/tests/Reverb/EventDispatcherTest.php
@@ -206,7 +206,7 @@ public function testCacheMissLockClearsOnVacateAndFiresOnRecreation(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['cache_miss'],
'disconnect_smoothing_ms' => 0,
diff --git a/tests/Reverb/Protocols/Pusher/Channels/ChannelTest.php b/tests/Reverb/Protocols/Pusher/Channels/ChannelTest.php
index 5eec71778..e7b81cc55 100644
--- a/tests/Reverb/Protocols/Pusher/Channels/ChannelTest.php
+++ b/tests/Reverb/Protocols/Pusher/Channels/ChannelTest.php
@@ -282,7 +282,7 @@ public function testSubscribeFiresSubscriptionCountWebhook(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
@@ -303,7 +303,7 @@ public function testUnsubscribeFiresSubscriptionCountWebhook(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
@@ -330,7 +330,7 @@ public function testSubscriptionCountNotFiredWhenOptInIsFalse(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
// subscription_count not set — defaults to false
@@ -347,7 +347,7 @@ public function testSubscriptionCountNotFiredForPresenceChannels(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
@@ -364,7 +364,7 @@ public function testSubscriptionCountNotFiredForPresenceCacheChannels(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
@@ -381,7 +381,7 @@ public function testSubscriptionCountFiredForPrivateChannels(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
@@ -398,7 +398,7 @@ public function testSubscriptionCountFiredForCacheChannels(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
@@ -417,7 +417,7 @@ public function testDisconnectDefersChannelVacatedWebhook(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_vacated'],
'disconnect_smoothing_ms' => 3000,
@@ -430,7 +430,7 @@ public function testDisconnectDefersChannelVacatedWebhook(): void
$server = $this->app->make(Server::class);
$server->close($connection);
- // Webhook should NOT fire immediately — it's deferred
+ // Webhook should not fire immediately — it's deferred
Queue::assertNotPushed(WebhookDeliveryJob::class, function (WebhookDeliveryJob $job) {
return $job->payload->events[0]['name'] === 'channel_vacated';
});
@@ -440,7 +440,7 @@ public function testExplicitUnsubscribeFiresChannelVacatedImmediately(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_vacated'],
'disconnect_smoothing_ms' => 3000,
@@ -465,7 +465,7 @@ public function testReconnectWithinSmoothingWindowSuppressesChannelOccupied(): v
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_occupied', 'channel_vacated'],
'disconnect_smoothing_ms' => 3000,
@@ -491,7 +491,7 @@ public function testNormalSubscribeFiresChannelOccupied(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_occupied'],
'disconnect_smoothing_ms' => 3000,
@@ -509,7 +509,7 @@ public function testCrossWorkerSmoothingMarkerSuppressesChannelOccupied(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_occupied'],
'disconnect_smoothing_ms' => 3000,
@@ -534,7 +534,7 @@ public function testConsumedMarkerDoesNotSuppressSubsequentLegitimateOccupied():
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_occupied', 'channel_vacated'],
'disconnect_smoothing_ms' => 3000,
@@ -568,7 +568,7 @@ public function testSubscriptionCountThrottledAbove100Subscribers(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
@@ -603,7 +603,7 @@ public function testSubscriptionCountFiresAbove100WhenLockAcquired(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => [],
'subscription_count' => true,
diff --git a/tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php b/tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php
index 01d432d31..a7b9b2b4a 100644
--- a/tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php
+++ b/tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php
@@ -180,7 +180,7 @@ public function testSubscriptionAndUnsubscriptionPreserveZeroUserId(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['member_added', 'member_removed'],
'disconnect_smoothing_ms' => 0,
@@ -232,7 +232,7 @@ public function testEnsuresTheMemberAddedEventIsOnlyFiredOnce(): void
$channel->subscribe($connectionOne->connection(), static::validAuth($connectionOne->id(), 'presence-test-channel', $data = json_encode($connectionOne->data())), $data);
$channel->subscribe($connectionTwo->connection(), static::validAuth($connectionTwo->id(), 'presence-test-channel', $data = json_encode($connectionTwo->data())), $data);
- // Second subscribe for same user_id should NOT trigger member_added broadcast
+ // Second subscribe for same user_id should not trigger member_added broadcast
$connectionOne->connection()->assertNothingReceived();
}
@@ -265,7 +265,7 @@ public function testDisconnectDefersMemberRemovedWebhook(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['member_removed'],
'disconnect_smoothing_ms' => 3000,
@@ -289,7 +289,7 @@ public function testDisconnectDefersMemberRemovedWebhook(): void
$this->connection->markDisconnecting();
$channel->unsubscribe($this->connection);
- // Webhook should NOT fire immediately — it's deferred
+ // Webhook should not fire immediately — it's deferred
Queue::assertNotPushed(WebhookDeliveryJob::class, function (WebhookDeliveryJob $job) {
return $job->payload->events[0]['name'] === 'member_removed';
});
@@ -299,7 +299,7 @@ public function testExplicitUnsubscribeFiresMemberRemovedImmediately(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['member_removed'],
'disconnect_smoothing_ms' => 3000,
@@ -334,7 +334,7 @@ public function testReconnectWithinSmoothingWindowSuppressesMemberAddedWebhook()
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['member_added', 'member_removed'],
'disconnect_smoothing_ms' => 3000,
@@ -349,7 +349,6 @@ public function testReconnectWithinSmoothingWindowSuppressesMemberAddedWebhook()
$data
);
- // Set up mock for unsubscribe's find() call
// Simulate disconnect
$this->connection->markDisconnecting();
$channel->unsubscribe($this->connection);
@@ -377,7 +376,7 @@ public function testReconnectStillSendsInternalMemberAddedEvent(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['member_added', 'member_removed'],
'disconnect_smoothing_ms' => 3000,
@@ -426,7 +425,7 @@ public function testCrossWorkerSmoothingMarkerSuppressesMemberAdded(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['member_added'],
'disconnect_smoothing_ms' => 3000,
@@ -455,7 +454,7 @@ public function testConsumedMemberMarkerDoesNotSuppressSubsequentLegitimateAdd()
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['member_added', 'member_removed'],
'disconnect_smoothing_ms' => 3000,
diff --git a/tests/Reverb/Protocols/Pusher/ClientEventTest.php b/tests/Reverb/Protocols/Pusher/ClientEventTest.php
index 20d5a27ef..8032b9574 100644
--- a/tests/Reverb/Protocols/Pusher/ClientEventTest.php
+++ b/tests/Reverb/Protocols/Pusher/ClientEventTest.php
@@ -61,7 +61,7 @@ public function testClientMessagePreservesZeroUserIdInBroadcastAndWebhook(): voi
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['client_event'],
]);
@@ -133,7 +133,7 @@ public function testRejectClientEventOnPublicChannelInMembersMode(): void
public function testAllowsClientEventOnPublicChannelInAllMode(): void
{
- $this->app['config']->set('reverb.apps.apps.0.accept_client_events_from', 'all');
+ config()->set('reverb.apps.apps.0.accept_client_events_from', 'all');
$this->channels()->findOrCreate('test-channel');
$this->channelConnectionManager->shouldReceive('all')
@@ -161,7 +161,7 @@ public function testRejectClientEventOnPublicChannelDoesNotProduceWebhook(): voi
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['client_event'],
]);
@@ -215,7 +215,7 @@ public function testDoesNotForwardUnauthenticatedClientMessageWhenInMembersMode(
public function testDoesNotForwardClientMessageWhenSetToNone(): void
{
- $this->app['config']->set('reverb.apps.apps.0.accept_client_events_from', 'none');
+ config()->set('reverb.apps.apps.0.accept_client_events_from', 'none');
$this->channels()->findOrCreate('private-test-channel');
$connectionOne = collect(static::factory(data: ['user_info' => ['name' => 'Joe'], 'user_id' => '1']))->first();
@@ -247,7 +247,7 @@ public function testDoesNotForwardClientMessageWhenSetToNone(): void
public function testForwardsAClientMessageForUnauthenticatedClientWhenSetToAll(): void
{
- $this->app['config']->set('reverb.apps.apps.0.accept_client_events_from', 'all');
+ config()->set('reverb.apps.apps.0.accept_client_events_from', 'all');
$connection = new FakeConnection;
$this->channels()->findOrCreate('test-channel');
@@ -298,8 +298,8 @@ public function testWebhookIncludesUserIdForPresenceChannelInAllMode(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.accept_client_events_from', 'all');
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.accept_client_events_from', 'all');
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['client_event'],
]);
diff --git a/tests/Reverb/Protocols/Pusher/EventHandlerTest.php b/tests/Reverb/Protocols/Pusher/EventHandlerTest.php
index bfd81ba75..c1e4ef0e8 100644
--- a/tests/Reverb/Protocols/Pusher/EventHandlerTest.php
+++ b/tests/Reverb/Protocols/Pusher/EventHandlerTest.php
@@ -319,7 +319,7 @@ public function testCacheMissFiresWebhook(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['cache_miss'],
]);
@@ -338,7 +338,7 @@ public function testCacheHitDoesNotFireWebhook(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['cache_miss'],
]);
@@ -366,7 +366,7 @@ public function testCacheMissWebhookIsDeduplicated(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['cache_miss'],
]);
@@ -392,9 +392,9 @@ public function testCacheMissWebhookRespectsEventFilter(): void
{
Queue::fake();
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
- 'events' => ['channel_occupied'], // cache_miss NOT in the list
+ 'events' => ['channel_occupied'], // cache_miss is not in the list
]);
$this->pusher->subscribe($this->connection, 'cache-test-channel');
@@ -407,11 +407,11 @@ public function testCacheMissWebhookRespectsEventFilter(): void
public function testCacheMissWithNoWebhooksDoesNotTouchLock(): void
{
// Default config — no webhook URL configured
- $sharedState = $this->app->make(\Hypervel\Reverb\Servers\Hypervel\Contracts\SharedState::class);
+ $sharedState = $this->app->make(SharedState::class);
$this->pusher->subscribe($this->connection, 'cache-test-channel');
- // The lock should NOT have been acquired since hasWebhooks() is false.
+ // The lock should not have been acquired since hasWebhooks() is false.
// Verify by acquiring it now — if it was already held, this would fail.
$this->assertTrue(
$sharedState->tryCacheMissLock($this->connection->app()->id(), 'cache-test-channel')
diff --git a/tests/Reverb/Protocols/Pusher/Http/Controllers/EventsControllerTest.php b/tests/Reverb/Protocols/Pusher/Http/Controllers/EventsControllerTest.php
index 9a3ec764f..5cbf022c2 100644
--- a/tests/Reverb/Protocols/Pusher/Http/Controllers/EventsControllerTest.php
+++ b/tests/Reverb/Protocols/Pusher/Http/Controllers/EventsControllerTest.php
@@ -88,7 +88,7 @@ public function testCanIgnoreASubscriber(): void
$connection->assertReceivedCount(1);
- // Second request — exclude this socket, connection should NOT receive
+ // Second request — exclude this socket, connection should not receive
$connection->resetReceived();
$this->signedPostRequest('events', [
'name' => 'NewEvent',
@@ -254,7 +254,7 @@ public function testCanVerifySignatureWhenUsingACustomServerPath(): void
$body = json_encode($data);
$timestamp = time();
- // Build the signature WITHOUT the path prefix (as the Pusher PHP client does)
+ // Build the signature without the path prefix (as the Pusher PHP client does)
$query = "auth_key={$key}&auth_timestamp={$timestamp}&auth_version=1.0";
$params = explode('&', $query);
sort($params);
@@ -264,7 +264,7 @@ public function testCanVerifySignatureWhenUsingACustomServerPath(): void
$signatureString = "POST\n/apps/{$appId}/events\n{$query}";
$signature = hash_hmac('sha256', $signatureString, $secret);
- // Send the request TO the prefixed URL
+ // Send the request to the prefixed URL
$response = $this->reverbCall('POST', "/ws/apps/{$appId}/events?{$query}&auth_signature={$signature}", [
'CONTENT_TYPE' => 'application/json',
'CONTENT_LENGTH' => (string) strlen($body),
@@ -278,7 +278,7 @@ public function testCanVerifySignatureWhenUsingACustomServerPath(): void
*/
protected function withPathPrefix(ApplicationContract $app): void
{
- $app['config']->set('reverb.servers.reverb.path', '/ws');
+ $app->make('config')->set('reverb.servers.reverb.path', '/ws');
}
public function testReturnsEmptyObjectWhenNoInfoRequested(): void
diff --git a/tests/Reverb/Protocols/Pusher/ServerTest.php b/tests/Reverb/Protocols/Pusher/ServerTest.php
index 1dbfe8854..2505eb34a 100644
--- a/tests/Reverb/Protocols/Pusher/ServerTest.php
+++ b/tests/Reverb/Protocols/Pusher/ServerTest.php
@@ -7,6 +7,7 @@
use Hypervel\Contracts\Debug\ExceptionHandler;
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Reverb\Connection;
+use Hypervel\Reverb\Contracts\ApplicationProvider;
use Hypervel\Reverb\Contracts\WebSocketConnection;
use Hypervel\Reverb\Events\ConnectionClosed;
use Hypervel\Reverb\Events\ConnectionEstablished;
@@ -386,7 +387,7 @@ public function testUnsubscribesAUserFromAPresenceChannelOnDisconnection(): void
#[DataProvider('invalidOriginProvider')]
public function testRejectsAConnectionFromAnInvalidOrigin(string $origin, array $allowedOrigins): void
{
- $this->app['config']->set('reverb.apps.apps.0.allowed_origins', $allowedOrigins);
+ config()->set('reverb.apps.apps.0.allowed_origins', $allowedOrigins);
$this->server->open($connection = new FakeConnection(origin: $origin));
$this->assertFalse($connection->isEstablished());
@@ -410,11 +411,11 @@ public static function invalidOriginProvider(): array
public function testRejectsAConnectionWithoutAnOrigin(): void
{
- $this->app['config']->set('reverb.apps.apps.0.allowed_origins', ['localhost']);
+ config()->set('reverb.apps.apps.0.allowed_origins', ['localhost']);
$webSocket = m::mock(WebSocketConnection::class);
$webSocket->shouldReceive('send')->once();
- $application = $this->app->make(\Hypervel\Reverb\Contracts\ApplicationProvider::class)
+ $application = $this->app->make(ApplicationProvider::class)
->findByKey('reverb-key');
$connection = new Connection($webSocket, $application, null);
@@ -426,7 +427,7 @@ public function testRejectsAConnectionWithoutAnOrigin(): void
#[DataProvider('validOriginProvider')]
public function testAcceptsAConnectionFromAValidOrigin(string $origin, array $allowedOrigins): void
{
- $this->app['config']->set('reverb.apps.apps.0.allowed_origins', $allowedOrigins);
+ config()->set('reverb.apps.apps.0.allowed_origins', $allowedOrigins);
$this->server->open($connection = new FakeConnection(origin: $origin));
$this->assertTrue($connection->isEstablished());
@@ -449,7 +450,7 @@ public static function validOriginProvider(): array
public function testRejectsAConnectionWhenTheAppIsOverTheConnectionLimit(): void
{
- $this->app['config']->set('reverb.apps.apps.0.max_connections', 1);
+ config()->set('reverb.apps.apps.0.max_connections', 1);
$this->server->open($connection = new FakeConnection);
$this->server->message(
$connection,
@@ -631,7 +632,7 @@ public function testSendsAnErrorIfSomethingFailsForChannelType(): void
public function testRejectsAMessageWhenTheRateLimitIsExceeded(): void
{
- $this->app['config']->set('reverb.apps.apps.0.rate_limiting', [
+ config()->set('reverb.apps.apps.0.rate_limiting', [
'enabled' => true,
'max_attempts' => 3,
'decay_seconds' => 1,
@@ -671,7 +672,7 @@ public function testRejectsAMessageWhenTheRateLimitIsExceeded(): void
public function testEnforcesRateLimitConfiguredWithNumericStrings(): void
{
- $this->app['config']->set('reverb.apps.apps.0.rate_limiting', [
+ config()->set('reverb.apps.apps.0.rate_limiting', [
'enabled' => true,
'max_attempts' => '1',
'decay_seconds' => '60',
@@ -708,7 +709,7 @@ public function testEnforcesRateLimitConfiguredWithNumericStrings(): void
#[DefineEnvironment('withInvalidRateLimiterConfiguration')]
public function testMessageRateLimiterIsIndependentOfRateLimiterConfiguration(): void
{
- $this->app['config']->set('reverb.apps.apps.0.rate_limiting', [
+ config()->set('reverb.apps.apps.0.rate_limiting', [
'enabled' => true,
'max_attempts' => 1,
'decay_seconds' => 60,
@@ -762,7 +763,7 @@ protected function withInvalidRateLimiterConfiguration(ApplicationContract $app)
public function testCloseClearsInitializedMessageRateLimiterState(): void
{
- $this->app['config']->set('reverb.apps.apps.0.rate_limiting', [
+ config()->set('reverb.apps.apps.0.rate_limiting', [
'enabled' => true,
'max_attempts' => 1,
'decay_seconds' => 60,
@@ -804,7 +805,7 @@ public function testCloseClearsInitializedMessageRateLimiterState(): void
public function testTerminatesTheConnectionWhenRateLimitIsExceededAndConfiguredToTerminate(): void
{
- $this->app['config']->set('reverb.apps.apps.0.rate_limiting', [
+ config()->set('reverb.apps.apps.0.rate_limiting', [
'enabled' => true,
'max_attempts' => 1,
'decay_seconds' => 1,
@@ -842,7 +843,7 @@ public function testTerminatesTheConnectionWhenRateLimitIsExceededAndConfiguredT
public function testTerminatesTheConnectionWhenSendingTheRateLimitErrorFails(): void
{
- $this->app['config']->set('reverb.apps.apps.0.rate_limiting', [
+ config()->set('reverb.apps.apps.0.rate_limiting', [
'enabled' => true,
'max_attempts' => 1,
'decay_seconds' => 1,
@@ -876,7 +877,7 @@ public function testTerminatesTheConnectionWhenSendingTheRateLimitErrorFails():
public function testEnabledRateLimitingRequiresDecaySeconds(): void
{
- $this->app['config']->set('reverb.apps.apps.0.rate_limiting', [
+ config()->set('reverb.apps.apps.0.rate_limiting', [
'enabled' => true,
'max_attempts' => 1,
'terminate_on_limit' => false,
@@ -964,7 +965,7 @@ public function testCloseDoesNotTerminateTheConnection(): void
$server->close($connection);
// close() is the "client already disconnected" cleanup path.
- // It should NOT try to terminate/disconnect the connection again —
+ // It should not try to terminate/disconnect the connection again —
// the fd is already gone.
$this->assertFalse($connection->wasTerminated);
}
@@ -1004,7 +1005,7 @@ public function testConnectionEstablishedEventNotDispatchedOnFailure(): void
{
Event::fake();
- $this->app['config']->set('reverb.apps.apps.0.allowed_origins', ['laravel.com']);
+ config()->set('reverb.apps.apps.0.allowed_origins', ['laravel.com']);
$this->server->open(new FakeConnection(origin: 'http://localhost'));
Event::assertNotDispatched(ConnectionEstablished::class);
diff --git a/tests/Reverb/ReverbServiceProviderTest.php b/tests/Reverb/ReverbServiceProviderTest.php
index 4f116c0c4..de231eb2f 100644
--- a/tests/Reverb/ReverbServiceProviderTest.php
+++ b/tests/Reverb/ReverbServiceProviderTest.php
@@ -5,6 +5,7 @@
namespace Hypervel\Tests\Reverb;
use Hypervel\Redis\RedisProxy;
+use Hypervel\Reverb\ApplicationManager;
use Hypervel\Reverb\Contracts\ApplicationProvider;
use Hypervel\Reverb\Contracts\Logger;
use Hypervel\Reverb\Loggers\NullLogger;
@@ -12,6 +13,7 @@
use Hypervel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
use Hypervel\Reverb\Protocols\Pusher\Managers\ArrayChannelManager;
use Hypervel\Reverb\ReverbServiceProvider;
+use Hypervel\Reverb\ServerProviderManager;
use Hypervel\Reverb\Webhooks\WebhookBatchBuffer;
use Hypervel\Support\Facades\Log;
use Mockery as m;
@@ -105,6 +107,27 @@ public function testWebhookBatchBufferUsesConfiguredScalingRedisConnection(): vo
$this->assertSame('queue', $this->bufferRedisConnection($buffer)->getName());
}
+ public function testReloadConfigurationRefreshesResolvedApplicationsAndWebhookBuffer(): void
+ {
+ $manager = $this->app->make(ApplicationManager::class);
+ $applicationProvider = $manager->driver();
+ $serverProviderManager = $this->app->make(ServerProviderManager::class);
+ $buffer = $this->app->make(WebhookBatchBuffer::class);
+
+ $this->app->make('config')->set([
+ 'reverb.apps.apps.0.app_id' => 'refreshed-app',
+ 'reverb.servers.reverb.scaling.connection' => 'queue',
+ ]);
+ $this->app->getProvider(ReverbServiceProvider::class)->reloadConfiguration();
+
+ $this->assertSame($manager, $this->app->make(ApplicationManager::class));
+ $this->assertNotSame($applicationProvider, $manager->driver());
+ $this->assertSame('refreshed-app', $manager->driver()->findById('refreshed-app')->id());
+ $this->assertSame($serverProviderManager, $this->app->make(ServerProviderManager::class));
+ $this->assertNotSame($buffer, $refreshedBuffer = $this->app->make(WebhookBatchBuffer::class));
+ $this->assertSame('queue', $this->bufferRedisConnection($refreshedBuffer)->getName());
+ }
+
public function testPreservesCustomChannelManagerBindings(): void
{
$channelManager = m::mock(ChannelManager::class);
diff --git a/tests/Reverb/ReverbTestCase.php b/tests/Reverb/ReverbTestCase.php
index 40137ae67..9322bc3e8 100644
--- a/tests/Reverb/ReverbTestCase.php
+++ b/tests/Reverb/ReverbTestCase.php
@@ -43,7 +43,9 @@ protected function getPackageProviders(ApplicationContract $app): array
*/
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('reverb.apps.apps', [
+ $config = $app->make('config');
+
+ $config->set('reverb.apps.apps', [
[
'key' => 'reverb-key',
'secret' => 'reverb-secret',
@@ -76,10 +78,10 @@ protected function defineEnvironment(ApplicationContract $app): void
],
];
- $app['config']->set('database.redis.options', []);
- $app['config']->set('database.redis.default', $redisConnection);
- $app['config']->set('database.redis.queue', $redisConnection);
- $app['config']->set('database.redis.reverb', $redisConnection);
+ $config->set('database.redis.options', []);
+ $config->set('database.redis.default', $redisConnection);
+ $config->set('database.redis.queue', $redisConnection);
+ $config->set('database.redis.reverb', $redisConnection);
$server = m::mock(Server::class);
$server->shouldReceive('sendMessage')->zeroOrMoreTimes();
diff --git a/tests/Reverb/Servers/Hypervel/GracefulShutdownTest.php b/tests/Reverb/Servers/Hypervel/GracefulShutdownTest.php
index 5bad10aec..c0c89635a 100644
--- a/tests/Reverb/Servers/Hypervel/GracefulShutdownTest.php
+++ b/tests/Reverb/Servers/Hypervel/GracefulShutdownTest.php
@@ -9,12 +9,16 @@
use Hypervel\Core\Events\OnWorkerExit;
use Hypervel\Filesystem\Filesystem;
use Hypervel\Reverb\Application;
+use Hypervel\Reverb\Connection as ReverbConnection;
+use Hypervel\Reverb\Contracts\ApplicationProvider;
use Hypervel\Reverb\Protocols\Pusher\Server as PusherServer;
use Hypervel\Reverb\ReverbServiceProvider;
use Hypervel\Reverb\ServerProviderManager;
+use Hypervel\Reverb\Servers\Hypervel\Connection as WebSocketConnection;
use Hypervel\Reverb\Servers\Hypervel\ConnectionLifecycle;
use Hypervel\Reverb\Servers\Hypervel\Contracts\PubSubProvider;
use Hypervel\Reverb\Servers\Hypervel\Contracts\SharedState;
+use Hypervel\Reverb\Servers\Hypervel\HypervelServerProvider;
use Hypervel\Reverb\Servers\Hypervel\WebSocketHandler;
use Hypervel\Reverb\Webhooks\DeferredWebhookManager;
use Hypervel\Reverb\Webhooks\Jobs\FlushWebhookBatchJob;
@@ -202,7 +206,7 @@ public function testFlushWebhookBuffersSchedulesFlushJob(): void
{
Queue::fake([FlushWebhookBatchJob::class]);
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_occupied'],
'batching' => ['enabled' => true],
@@ -224,7 +228,7 @@ public function testFlushWebhookBuffersSkipsWhenNoBatching(): void
{
Queue::fake([FlushWebhookBatchJob::class]);
- // Default config has no batching
+ // Default config disables batching.
$provider = $this->app->getProvider(ReverbServiceProvider::class);
$method = new ReflectionMethod($provider, 'flushWebhookBuffers');
$method->invoke($provider);
@@ -236,7 +240,7 @@ public function testFlushWebhookBuffersSkipsWhenBufferEmpty(): void
{
Queue::fake([FlushWebhookBatchJob::class]);
- $this->app['config']->set('reverb.apps.apps.0.webhooks', [
+ config()->set('reverb.apps.apps.0.webhooks', [
'url' => 'https://example.com/webhook',
'events' => ['channel_occupied'],
'batching' => ['enabled' => true],
@@ -323,11 +327,11 @@ public function testShutdownDoesNotLosePreExistingDeferredWebhooks(): void
public function testDisconnectScalingSubscriberCallsDisconnect(): void
{
- $this->app['config']->set('reverb.servers.reverb.scaling.enabled', true);
+ config()->set('reverb.servers.reverb.scaling.enabled', true);
- $provider = new \Hypervel\Reverb\Servers\Hypervel\HypervelServerProvider(
+ $provider = new HypervelServerProvider(
$this->app,
- $this->app['config']->get('reverb.servers.reverb', [])
+ config()->array('reverb.servers.reverb')
);
$provider->register();
$this->app->make(ServerProviderManager::class)->withPublishing();
@@ -359,7 +363,7 @@ public function testDisconnectWithNoCodeUsesPlainPath(): void
$sender = m::mock(Sender::class);
$sender->shouldReceive('disconnect')->once()->with(99)->andReturn(true);
- $wsConnection = new \Hypervel\Reverb\Servers\Hypervel\Connection($sender, 99);
+ $wsConnection = new WebSocketConnection($sender, 99);
$wsConnection->close();
}
@@ -368,13 +372,13 @@ public function testDisconnectWithCodeForwardsCodeAndReason(): void
$sender = m::mock(Sender::class);
$sender->shouldReceive('disconnect')->once()->with(99, 1001, 'Server restarting')->andReturn(true);
- $wsConnection = new \Hypervel\Reverb\Servers\Hypervel\Connection($sender, 99);
+ $wsConnection = new WebSocketConnection($sender, 99);
$wsConnection->close(code: 1001, reason: 'Server restarting');
}
// ── Helpers ───────────────────────────────────────────────────────
- protected function createReverbConnection(?Sender $sender = null, ?int $fd = null): \Hypervel\Reverb\Connection
+ protected function createReverbConnection(?Sender $sender = null, ?int $fd = null): ReverbConnection
{
if ($sender === null) {
$sender = m::mock(Sender::class);
@@ -382,13 +386,13 @@ protected function createReverbConnection(?Sender $sender = null, ?int $fd = nul
$sender->shouldReceive('disconnect')->zeroOrMoreTimes()->andReturn(true);
}
- $wsConnection = new \Hypervel\Reverb\Servers\Hypervel\Connection($sender, $fd ?? rand(1, 99999));
- $app = $this->app->make(\Hypervel\Reverb\Contracts\ApplicationProvider::class)->all()->first();
+ $wsConnection = new WebSocketConnection($sender, $fd ?? rand(1, 99999));
+ $app = $this->app->make(ApplicationProvider::class)->all()->first();
- return new \Hypervel\Reverb\Connection($wsConnection, $app, null);
+ return new ReverbConnection($wsConnection, $app, null);
}
- protected function addToWebSocketHandler(int $fd, \Hypervel\Reverb\Connection $connection): void
+ protected function addToWebSocketHandler(int $fd, ReverbConnection $connection): void
{
$connection->markEstablished();
$lifecycle = new ConnectionLifecycle($fd);
diff --git a/tests/Reverb/Servers/Hypervel/HypervelServerProviderTest.php b/tests/Reverb/Servers/Hypervel/HypervelServerProviderTest.php
index 4b80e4d86..50dfef9c3 100644
--- a/tests/Reverb/Servers/Hypervel/HypervelServerProviderTest.php
+++ b/tests/Reverb/Servers/Hypervel/HypervelServerProviderTest.php
@@ -28,12 +28,12 @@ public function testBindsSwooleTableSharedStateByDefault(): void
public function testBindsRedisSharedStateWhenScalingEnabled(): void
{
- $this->app['config']->set('reverb.servers.reverb.scaling.enabled', true);
+ config()->set('reverb.servers.reverb.scaling.enabled', true);
// Re-register the provider with new config
$provider = new HypervelServerProvider(
$this->app,
- $this->app['config']->get('reverb.servers.reverb', [])
+ config()->array('reverb.servers.reverb')
);
$provider->register();
@@ -52,11 +52,11 @@ public function testCreatesSwooleTableWithConfiguredRows(): void
public function testScalingSharedStateDefaultsToReverbRedisConnection(): void
{
- $this->app['config']->set('reverb.servers.reverb.scaling.enabled', true);
+ config()->set('reverb.servers.reverb.scaling.enabled', true);
$provider = new HypervelServerProvider(
$this->app,
- $this->app['config']->get('reverb.servers.reverb', [])
+ config()->array('reverb.servers.reverb')
);
$provider->register();
@@ -68,12 +68,12 @@ public function testScalingSharedStateDefaultsToReverbRedisConnection(): void
public function testScalingSharedStateUsesConfiguredRedisConnection(): void
{
- $this->app['config']->set('reverb.servers.reverb.scaling.enabled', true);
- $this->app['config']->set('reverb.servers.reverb.scaling.connection', 'queue');
+ config()->set('reverb.servers.reverb.scaling.enabled', true);
+ config()->set('reverb.servers.reverb.scaling.connection', 'queue');
$provider = new HypervelServerProvider(
$this->app,
- $this->app['config']->get('reverb.servers.reverb', [])
+ config()->array('reverb.servers.reverb')
);
$provider->register();
diff --git a/tests/Routing/RoutingServiceProviderTest.php b/tests/Routing/RoutingServiceProviderTest.php
index 3dc31eec3..76f57570c 100644
--- a/tests/Routing/RoutingServiceProviderTest.php
+++ b/tests/Routing/RoutingServiceProviderTest.php
@@ -5,9 +5,11 @@
namespace Hypervel\Tests\Routing;
use Hypervel\Http\Request;
+use Hypervel\Routing\RoutingServiceProvider;
use Hypervel\Testbench\Attributes\WithConfig;
use Hypervel\Tests\Testbench\TestCase;
use InvalidArgumentException;
+use ReflectionClass;
class RoutingServiceProviderTest extends TestCase
{
@@ -53,4 +55,40 @@ public function testRebindingTheContainerRequestDoesNotMutateTheUrlGeneratorFall
$this->assertSame($original, $url->getRequest());
}
+
+ public function testReloadConfigurationUpdatesRetainedUrlGeneratorInBothHttpsDirections(): void
+ {
+ $url = $this->app->make('url');
+ $provider = $this->app->getProvider(RoutingServiceProvider::class);
+ $reflection = new ReflectionClass($url);
+ $routes = $reflection->getProperty('routes')->getValue($url);
+
+ config([
+ 'app.url' => 'http://refreshed.example',
+ 'app.asset_url' => 'https://assets.example',
+ 'app.force_https' => true,
+ ]);
+
+ $provider->reloadConfiguration();
+
+ $this->assertSame($url, $this->app->make('url'));
+ $this->assertSame($routes, $reflection->getProperty('routes')->getValue($url));
+ $this->assertSame(
+ 'http://refreshed.example',
+ $reflection->getProperty('request')->getValue($url)->root(),
+ );
+ $this->assertSame('https://assets.example/image.png', $url->asset('image.png'));
+ $this->assertSame('https://refreshed.example/path', $url->to('path'));
+
+ config([
+ 'app.url' => 'http://second.example',
+ 'app.asset_url' => null,
+ 'app.force_https' => false,
+ ]);
+
+ $provider->reloadConfiguration();
+
+ $this->assertSame('http://second.example/image.png', $url->asset('image.png'));
+ $this->assertSame('http://second.example/path', $url->to('path'));
+ }
}
diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php
index 8005c75dd..80339fc56 100755
--- a/tests/Routing/RoutingUrlGeneratorTest.php
+++ b/tests/Routing/RoutingUrlGeneratorTest.php
@@ -879,6 +879,23 @@ public function testAssetOriginsTakePrecedenceOverExplicitApplicationOrigin(): v
$this->assertSame('https://local-assets.example.com/app.js', $url->asset('app.js'));
}
+ public function testAssetRootCanBeReconfigured(): void
+ {
+ $url = new UrlGenerator(
+ new RouteCollection,
+ Request::create('https://request.example.com/'),
+ 'https://old-assets.example.com',
+ );
+
+ $url->setAssetRoot('https://new-assets.example.com');
+
+ $this->assertSame('https://new-assets.example.com/app.js', $url->asset('app.js'));
+
+ $url->setAssetRoot(null);
+
+ $this->assertSame('https://request.example.com/app.js', $url->asset('app.js'));
+ }
+
public function testOriginResolverCanBeCleared(): void
{
$url = new UrlGenerator(
diff --git a/tests/Saloon/SaloonServiceProviderTest.php b/tests/Saloon/SaloonServiceProviderTest.php
index c616f5cef..f5a237fe8 100644
--- a/tests/Saloon/SaloonServiceProviderTest.php
+++ b/tests/Saloon/SaloonServiceProviderTest.php
@@ -7,6 +7,7 @@
use GuzzleHttp\TransportSharing;
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Http\Client\Factory;
+use Hypervel\Saloon\Exceptions\PendingRequestException;
use Hypervel\Saloon\Facades\Saloon;
use Hypervel\Saloon\SaloonManager;
use Hypervel\Saloon\SaloonServiceProvider;
@@ -40,6 +41,64 @@ public function testProviderMergesConfigurationAndRegistersTheNamedConnection():
], $this->app->make(Factory::class)->getConnectionConfig('saloon'));
}
+ public function testProviderReloadsConnectionOptionsAndHandler(): void
+ {
+ $factory = $this->app->make(Factory::class);
+ $oldHandler = $factory->getConnectionHandler('saloon');
+ $options = [
+ 'connect_timeout' => 5,
+ 'timeout' => 15,
+ 'transport_sharing' => TransportSharing::HANDLER_PREFER,
+ ];
+ config()->set('saloon.connection.options', $options);
+
+ $provider = $this->app->getProvider(SaloonServiceProvider::class);
+ $this->assertInstanceOf(SaloonServiceProvider::class, $provider);
+ $provider->reloadConfiguration();
+
+ $this->assertSame($options, $factory->getConnectionConfig('saloon'));
+ $this->assertNotSame($oldHandler, $factory->getConnectionHandler('saloon'));
+ }
+
+ public function testProviderRegistersChangedConnectionNameWithoutRemovingApplicationPreset(): void
+ {
+ $factory = $this->app->make(Factory::class);
+ $applicationOptions = ['timeout' => 60];
+ $saloonOptions = ['timeout' => 15];
+ $factory->registerConnection('saloon', $applicationOptions);
+ config()->set('saloon.connection.name', 'saloon-refreshed');
+ config()->set('saloon.connection.options', $saloonOptions);
+
+ $provider = $this->app->getProvider(SaloonServiceProvider::class);
+ $this->assertInstanceOf(SaloonServiceProvider::class, $provider);
+ $provider->reloadConfiguration();
+
+ $this->assertSame($applicationOptions, $factory->getConnectionConfig('saloon'));
+ $this->assertSame($saloonOptions, $factory->getConnectionConfig('saloon-refreshed'));
+ }
+
+ public function testProviderRejectsInvalidReloadedOptionsBeforeChangingConnection(): void
+ {
+ $factory = $this->app->make(Factory::class);
+ $originalOptions = $factory->getConnectionConfig('saloon');
+ config()->set('saloon.connection.options', ['headers' => ['X-Test' => 'value']]);
+
+ $provider = $this->app->getProvider(SaloonServiceProvider::class);
+ $this->assertInstanceOf(SaloonServiceProvider::class, $provider);
+
+ try {
+ $provider->reloadConfiguration();
+ $this->fail('Expected invalid reloaded Saloon options to be rejected.');
+ } catch (PendingRequestException $exception) {
+ $this->assertSame(
+ 'The [headers] option cannot be set in HTTP connection [saloon]; use the Saloon request API instead.',
+ $exception->getMessage(),
+ );
+ }
+
+ $this->assertSame($originalOptions, $factory->getConnectionConfig('saloon'));
+ }
+
public function testProviderPublishesConfigurationAndGeneratorStubs(): void
{
$packageSource = dirname(__DIR__, 2) . '/src/saloon/src/../';
diff --git a/tests/Scout/Unit/ScoutServiceProviderTest.php b/tests/Scout/Unit/ScoutServiceProviderTest.php
index b1a0df3bf..f8fb44887 100644
--- a/tests/Scout/Unit/ScoutServiceProviderTest.php
+++ b/tests/Scout/Unit/ScoutServiceProviderTest.php
@@ -13,11 +13,14 @@
use Http\Client\Common\HttpMethodsClient;
use Hypervel\Contracts\Foundation\Application;
use Hypervel\Contracts\Telescope\TelescopeTag;
+use Hypervel\Scout\EngineManager;
+use Hypervel\Scout\Engines\Engine;
use Hypervel\Scout\ScoutServiceProvider;
use Hypervel\Support\ClassInvoker;
use Hypervel\Testbench\TestCase;
use Meilisearch\Client as MeilisearchClient;
use Meilisearch\Http\Client as MeilisearchHttpClient;
+use Mockery as m;
use Psr\Http\Client\ClientInterface;
use ReflectionProperty;
use stdClass;
@@ -274,4 +277,45 @@ public function testTypesenseClientHasScoutTelescopeTags(): void
$client->getConfig('telescope_tags'),
);
}
+
+ public function testReloadConfigurationRefreshesResolvedEnginesAndClients(): void
+ {
+ $config = $this->app->make('config');
+ $config->set([
+ 'scout.driver' => 'reload-test',
+ 'scout.algolia.id' => 'test-app-id',
+ 'scout.algolia.secret' => 'test-secret',
+ 'scout.typesense.client-settings' => [
+ 'api_key' => 'test-key',
+ 'nodes' => [
+ ['host' => 'localhost', 'port' => '8108', 'protocol' => 'http'],
+ ],
+ ],
+ ]);
+
+ $manager = $this->app->make(EngineManager::class);
+ $manager->extend('reload-test', static fn (): Engine => m::mock(Engine::class));
+ $engine = $manager->engine();
+ $algolia = $this->app->make(AlgoliaSearchClient::class);
+ $meilisearch = $this->app->make(MeilisearchClient::class);
+ $typesense = $this->app->make(TypesenseClient::class);
+
+ $this->app->getProvider(ScoutServiceProvider::class)->reloadConfiguration();
+
+ $this->assertSame($manager, $this->app->make(EngineManager::class));
+ $this->assertNotSame($engine, $manager->engine());
+ $this->assertNotSame($algolia, $this->app->make(AlgoliaSearchClient::class));
+ $this->assertNotSame($meilisearch, $this->app->make(MeilisearchClient::class));
+ $this->assertNotSame($typesense, $this->app->make(TypesenseClient::class));
+ }
+
+ public function testReloadConfigurationDoesNotResolveUnusedScoutServices(): void
+ {
+ $this->app->getProvider(ScoutServiceProvider::class)->reloadConfiguration();
+
+ $this->assertFalse($this->app->resolved(EngineManager::class));
+ $this->assertFalse($this->app->resolved(AlgoliaSearchClient::class));
+ $this->assertFalse($this->app->resolved(MeilisearchClient::class));
+ $this->assertFalse($this->app->resolved(TypesenseClient::class));
+ }
}
diff --git a/tests/Sentry/ConfigTest.php b/tests/Sentry/ConfigTest.php
index 0fd8dd12b..bf96a42a2 100644
--- a/tests/Sentry/ConfigTest.php
+++ b/tests/Sentry/ConfigTest.php
@@ -70,12 +70,12 @@ public static function unsupportedPoolOptions(): array
public function testOldPoolsKeyIsNotUsed(): void
{
- $this->assertNull($this->app['config']->get('pools.sentry'));
+ $this->assertNull($this->app->make('config')->get('pools.sentry'));
}
public function testRedisFeatureIsInDefaultFeaturesConfig(): void
{
- $features = $this->app['config']->get('sentry.features', []);
+ $features = $this->app->make('config')->array('sentry.features');
$this->assertContains(RedisFeature::class, $features);
}
diff --git a/tests/Sentry/EventHandler/DatabaseEventsTest.php b/tests/Sentry/EventHandler/DatabaseEventsTest.php
index 80c100f0c..fa8d382a4 100644
--- a/tests/Sentry/EventHandler/DatabaseEventsTest.php
+++ b/tests/Sentry/EventHandler/DatabaseEventsTest.php
@@ -17,7 +17,7 @@ public function testSqlQueriesAreRecordedWhenEnabled(): void
'sentry.breadcrumbs.sql_queries' => true,
]);
- $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.sql_queries'));
+ $this->assertTrue($this->app->make('config')->boolean('sentry.breadcrumbs.sql_queries'));
$this->dispatchHypervelEvent(new QueryExecuted(
$query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;',
@@ -37,7 +37,7 @@ public function testSqlBindingsAreRecordedWhenEnabled(): void
'sentry.breadcrumbs.sql_bindings' => true,
]);
- $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.sql_bindings'));
+ $this->assertTrue($this->app->make('config')->boolean('sentry.breadcrumbs.sql_bindings'));
$this->dispatchHypervelEvent(new QueryExecuted(
$query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;',
@@ -58,7 +58,7 @@ public function testSqlQueriesAreRecordedWhenDisabled(): void
'sentry.breadcrumbs.sql_queries' => false,
]);
- $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.sql_queries'));
+ $this->assertFalse($this->app->make('config')->boolean('sentry.breadcrumbs.sql_queries'));
$this->dispatchHypervelEvent(new QueryExecuted(
'SELECT * FROM breadcrumbs WHERE bindings = ?;',
@@ -76,7 +76,7 @@ public function testSqlBindingsAreRecordedWhenDisabled(): void
'sentry.breadcrumbs.sql_bindings' => false,
]);
- $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.sql_bindings'));
+ $this->assertFalse($this->app->make('config')->boolean('sentry.breadcrumbs.sql_bindings'));
$this->dispatchHypervelEvent(new QueryExecuted(
$query = 'SELECT * FROM breadcrumbs WHERE bindings <> ?;',
diff --git a/tests/Sentry/EventHandler/LogEventsTest.php b/tests/Sentry/EventHandler/LogEventsTest.php
index cf24814fe..e3bd43b94 100644
--- a/tests/Sentry/EventHandler/LogEventsTest.php
+++ b/tests/Sentry/EventHandler/LogEventsTest.php
@@ -15,7 +15,7 @@ public function testHypervelLogsAreRecordedWhenEnabled(): void
'sentry.breadcrumbs.logs' => true,
]);
- $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.logs'));
+ $this->assertTrue($this->app->make('config')->boolean('sentry.breadcrumbs.logs'));
$this->dispatchHypervelEvent(new MessageLogged(
$level = 'debug',
@@ -36,7 +36,7 @@ public function testHypervelLogsAreRecordedWhenDisabled(): void
'sentry.breadcrumbs.logs' => false,
]);
- $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.logs'));
+ $this->assertFalse($this->app->make('config')->boolean('sentry.breadcrumbs.logs'));
$this->dispatchHypervelEvent(new MessageLogged('debug', 'test message'));
diff --git a/tests/Sentry/Features/CacheIntegrationTest.php b/tests/Sentry/Features/CacheIntegrationTest.php
index a8893548e..fd6c20378 100644
--- a/tests/Sentry/Features/CacheIntegrationTest.php
+++ b/tests/Sentry/Features/CacheIntegrationTest.php
@@ -66,7 +66,7 @@ public function testCacheBreadcrumbIsNotRecordedWhenDisabled(): void
'sentry.breadcrumbs.cache' => false,
]);
- $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.cache'));
+ $this->assertFalse($this->app->make('config')->boolean('sentry.breadcrumbs.cache'));
Cache::get('foo');
@@ -76,7 +76,7 @@ public function testCacheBreadcrumbIsNotRecordedWhenDisabled(): void
public function testCacheBreadcrumbReplacesSessionKeyWithPlaceholder(): void
{
$this->startSession();
- $sessionId = $this->app['session']->getId();
+ $sessionId = $this->app->make('session')->getId();
Cache::put($sessionId, 'session-data');
@@ -255,7 +255,7 @@ public function testCacheSpanReplacesSessionKeyWithPlaceholder(): void
$this->markSkippedIfTracingEventsNotAvailable();
$this->startSession();
- $sessionId = $this->app['session']->getId();
+ $sessionId = $this->app->make('session')->getId();
$span = $this->executeAndReturnMostRecentSpan(function () use ($sessionId) {
Cache::get($sessionId);
@@ -271,7 +271,7 @@ public function testCacheSpanReplacesMultipleSessionKeysWithPlaceholder(): void
$this->markSkippedIfTracingEventsNotAvailable();
$this->startSession();
- $sessionId = $this->app['session']->getId();
+ $sessionId = $this->app->make('session')->getId();
$span = $this->executeAndReturnMostRecentSpan(function () use ($sessionId) {
Cache::get([$sessionId, 'regular-key', $sessionId . '_another']);
@@ -293,7 +293,7 @@ public function testCacheOperationDoesNotStartSessionPrematurely(): void
});
// Check that session was not started
- $this->assertFalse($this->app['session']->isStarted());
+ $this->assertFalse($this->app->make('session')->isStarted());
// And the key should not be replaced
$this->assertEquals('some-key', $span->getDescription());
diff --git a/tests/Sentry/Features/ConsoleIntegrationTest.php b/tests/Sentry/Features/ConsoleIntegrationTest.php
index 984782d83..e2ab2ff90 100644
--- a/tests/Sentry/Features/ConsoleIntegrationTest.php
+++ b/tests/Sentry/Features/ConsoleIntegrationTest.php
@@ -17,7 +17,7 @@ public function testCommandBreadcrumbIsRecordedWhenEnabled(): void
'sentry.breadcrumbs.command_info' => true,
]);
- $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.command_info'));
+ $this->assertTrue($this->app->make('config')->boolean('sentry.breadcrumbs.command_info'));
$this->dispatchCommandStartEvent();
@@ -33,7 +33,7 @@ public function testCommandBreadcrumbIsNotRecordedWhenDisabled(): void
'sentry.breadcrumbs.command_info' => false,
]);
- $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.command_info'));
+ $this->assertFalse($this->app->make('config')->boolean('sentry.breadcrumbs.command_info'));
$this->dispatchCommandStartEvent();
diff --git a/tests/Sentry/Features/LogIntegrationTest.php b/tests/Sentry/Features/LogIntegrationTest.php
index 2a5c6b2e0..06ab80b6e 100644
--- a/tests/Sentry/Features/LogIntegrationTest.php
+++ b/tests/Sentry/Features/LogIntegrationTest.php
@@ -23,7 +23,7 @@ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- tap($app['config'], static function (Repository $config) {
+ tap($app->make('config'), static function (Repository $config) {
$config->set('logging.channels.sentry', [
'driver' => 'sentry',
]);
diff --git a/tests/Sentry/Features/LogLogsIntegrationTest.php b/tests/Sentry/Features/LogLogsIntegrationTest.php
index b8baf3d47..a17ba1b1b 100644
--- a/tests/Sentry/Features/LogLogsIntegrationTest.php
+++ b/tests/Sentry/Features/LogLogsIntegrationTest.php
@@ -27,7 +27,7 @@ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- tap($app['config'], static function (Repository $config) {
+ tap($app->make('config'), static function (Repository $config) {
$config->set('sentry.enable_logs', true);
$config->set('logging.channels.sentry_logs', [
diff --git a/tests/Sentry/Features/QueueIntegrationTest.php b/tests/Sentry/Features/QueueIntegrationTest.php
index 0b50bafc6..4da98f398 100644
--- a/tests/Sentry/Features/QueueIntegrationTest.php
+++ b/tests/Sentry/Features/QueueIntegrationTest.php
@@ -39,21 +39,25 @@ class QueueIntegrationTest extends SentryTestCase
protected function withTracingEnabled(ApplicationContract $app): void
{
- $app['config']->set('sentry.traces_sample_rate', 1.0);
+ $app->make('config')->set('sentry.traces_sample_rate', 1.0);
}
protected function withQueueJobTracingDisabled(ApplicationContract $app): void
{
- $app['config']->set('sentry.traces_sample_rate', 1.0);
- $app['config']->set('sentry.tracing.queue_job_transactions', false);
+ $config = $app->make('config');
+
+ $config->set('sentry.traces_sample_rate', 1.0);
+ $config->set('sentry.tracing.queue_job_transactions', false);
}
protected function withLocalQueueOutputDisabled(ApplicationContract $app): void
{
- $app['config']->set('sentry.traces_sample_rate', null);
- $app['config']->set('sentry.breadcrumbs.queue_info', false);
- $app['config']->set('sentry.tracing.queue_jobs', false);
- $app['config']->set('sentry.tracing.queue_job_transactions', false);
+ $config = $app->make('config');
+
+ $config->set('sentry.traces_sample_rate', null);
+ $config->set('sentry.breadcrumbs.queue_info', false);
+ $config->set('sentry.tracing.queue_jobs', false);
+ $config->set('sentry.tracing.queue_job_transactions', false);
}
public function testQueueJobPushesAndPopsScopeWithBreadcrumbs(): void
diff --git a/tests/Sentry/Features/RedisIntegrationTest.php b/tests/Sentry/Features/RedisIntegrationTest.php
index 455cb07c2..5c2cad14a 100644
--- a/tests/Sentry/Features/RedisIntegrationTest.php
+++ b/tests/Sentry/Features/RedisIntegrationTest.php
@@ -108,7 +108,7 @@ public function testRedisCommandWithSessionKeyReplacesWithPlaceholder(): void
{
$this->setupMocks();
$this->startSession();
- $sessionId = $this->app['session']->getId();
+ $sessionId = $this->app->make('session')->getId();
$transaction = $this->startTransaction();
$dispatcher = $this->app->make(Dispatcher::class);
@@ -130,7 +130,7 @@ public function testRedisParametersRequirePiiConsentAndRedactSessionKey(): void
$this->app->make(RedisFeature::class)->detectSessionKeyOnConsole = true;
$this->setupMocks();
$this->startSession();
- $sessionId = $this->app['session']->getId();
+ $sessionId = $this->app->make('session')->getId();
$transaction = $this->startTransaction();
$dispatcher = $this->app->make(Dispatcher::class);
diff --git a/tests/Sentry/Features/StrictTraceContinuationIntegrationTest.php b/tests/Sentry/Features/StrictTraceContinuationIntegrationTest.php
index e60f37944..7a3d57b8b 100644
--- a/tests/Sentry/Features/StrictTraceContinuationIntegrationTest.php
+++ b/tests/Sentry/Features/StrictTraceContinuationIntegrationTest.php
@@ -19,7 +19,7 @@ class StrictTraceContinuationIntegrationTest extends SentryTestCase
private function registerRoutes(): void
{
- $this->app['router']->group(['prefix' => 'sentry'], function (Router $router) {
+ $this->app->make('router')->group(['prefix' => 'sentry'], function (Router $router) {
$router->get('/strict-trace-continuation', function () {
return 'ok';
});
diff --git a/tests/Sentry/SentryTestCase.php b/tests/Sentry/SentryTestCase.php
index 1a289f7ba..59da2b4d4 100644
--- a/tests/Sentry/SentryTestCase.php
+++ b/tests/Sentry/SentryTestCase.php
@@ -36,7 +36,7 @@ protected function defineEnvironment(ApplicationContract $app): void
self::$lastSentryEvents = [];
$this->setupGlobalEventProcessor();
- tap($app['config'], function (Repository $config) {
+ tap($app->make('config'), function (Repository $config) {
$config->set('sentry.before_send', static function (Event $event, ?EventHint $hint) {
self::$lastSentryEvents[] = [$event, $hint];
@@ -57,13 +57,15 @@ protected function defineEnvironment(ApplicationContract $app): void
protected function envWithoutDsnSet(ApplicationContract $app): void
{
- $app['config']->set('sentry.dsn', null);
- $app['config']->set('sentry_test.override_dsn', true);
+ $config = $app->make('config');
+
+ $config->set('sentry.dsn', null);
+ $config->set('sentry_test.override_dsn', true);
}
protected function envSamplingAllTransactions(ApplicationContract $app): void
{
- $app['config']->set('sentry.traces_sample_rate', 1.0);
+ $app->make('config')->set('sentry.traces_sample_rate', 1.0);
}
protected function getPackageProviders(ApplicationContract $app): array
diff --git a/tests/Sentry/ServiceProviderTest.php b/tests/Sentry/ServiceProviderTest.php
index 06a8e658e..04a709eb9 100644
--- a/tests/Sentry/ServiceProviderTest.php
+++ b/tests/Sentry/ServiceProviderTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Sentry;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Http\Kernel;
use Hypervel\Di\Aop\AspectCollector;
use Hypervel\Http\Request;
@@ -12,12 +13,15 @@
use Hypervel\Sentry\Features\Feature;
use Hypervel\Sentry\Http\FlushEventsMiddleware;
use Hypervel\Sentry\Http\SetRequestIpMiddleware;
+use Hypervel\Sentry\Hub;
use Hypervel\Sentry\SentryServiceProvider;
+use Hypervel\Sentry\Tracing\BacktraceHelper;
use Hypervel\Sentry\Tracing\Middleware as TracingMiddleware;
use Hypervel\Support\Facades\Artisan;
use Mockery as m;
use Psr\Log\LoggerInterface;
use RuntimeException;
+use Sentry\SentrySdk;
use Sentry\State\HubInterface;
use Symfony\Component\HttpFoundation\Response;
@@ -39,6 +43,46 @@ public function testEnvironment(): void
$this->assertEquals('testing', app('sentry')->getClient()->getOptions()->getEnvironment());
}
+ public function testReloadConfigurationRebindsTheClientWithoutReplacingTheHub(): void
+ {
+ $provider = new SentryServiceProvider($this->app);
+ $hub = $this->app->make(HubInterface::class);
+ $client = $hub->getClient();
+ $backtraceHelper = $this->app->make(BacktraceHelper::class);
+
+ config()->set('sentry.environment', 'reloaded');
+
+ $provider->reloadConfiguration();
+
+ $this->assertInstanceOf(Hub::class, $hub);
+ $this->assertSame($hub, $this->app->make(HubInterface::class));
+ $this->assertSame($hub, SentrySdk::getCurrentHub());
+ $this->assertNotSame($client, $hub->getClient());
+ $this->assertSame('reloaded', $hub->getClient()->getOptions()->getEnvironment());
+ $this->assertNotSame($backtraceHelper, $this->app->make(BacktraceHelper::class));
+ }
+
+ public function testReloadConfigurationDoesNotResolveAnUnusedHub(): void
+ {
+ $app = m::mock(ApplicationContract::class);
+ $app->shouldReceive('resolved')->once()->with(HubInterface::class)->andReturnFalse();
+ $app->shouldNotReceive('make');
+
+ (new SentryServiceProvider($app))->reloadConfiguration();
+ }
+
+ public function testReloadConfigurationLeavesAReplacedHubUntouched(): void
+ {
+ $backtraceHelper = $this->app->make(BacktraceHelper::class);
+ $hub = m::mock(HubInterface::class);
+ $this->app->instance(HubInterface::class, $hub);
+
+ (new SentryServiceProvider($this->app))->reloadConfiguration();
+
+ $this->assertSame($hub, $this->app->make(HubInterface::class));
+ $this->assertSame($backtraceHelper, $this->app->make(BacktraceHelper::class));
+ }
+
public function testDsnWasSetFromConfig(): void
{
$options = app('sentry')->getClient()->getOptions();
diff --git a/tests/Sentry/ServiceProviderWithCustomAliasTest.php b/tests/Sentry/ServiceProviderWithCustomAliasTest.php
index c099d87e0..abd4f40db 100644
--- a/tests/Sentry/ServiceProviderWithCustomAliasTest.php
+++ b/tests/Sentry/ServiceProviderWithCustomAliasTest.php
@@ -14,8 +14,10 @@ class ServiceProviderWithCustomAliasTest extends TestCase
{
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('custom-sentry.dsn', 'http://publickey@sentry.dev/123');
- $app['config']->set('custom-sentry.error_types', E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED);
+ $config = $app->make('config');
+
+ $config->set('custom-sentry.dsn', 'http://publickey@sentry.dev/123');
+ $config->set('custom-sentry.error_types', E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED);
}
protected function getPackageProviders(ApplicationContract $app): array
diff --git a/tests/Sentry/ServiceProviderWithoutDsnTest.php b/tests/Sentry/ServiceProviderWithoutDsnTest.php
index fdc33c917..ddb29c902 100644
--- a/tests/Sentry/ServiceProviderWithoutDsnTest.php
+++ b/tests/Sentry/ServiceProviderWithoutDsnTest.php
@@ -18,7 +18,7 @@ class ServiceProviderWithoutDsnTest extends TestCase
{
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('sentry.dsn', null);
+ $app->make('config')->set('sentry.dsn', null);
}
protected function getPackageProviders(ApplicationContract $app): array
diff --git a/tests/Server/ServerReloadCommandTest.php b/tests/Server/ServerReloadCommandTest.php
index 001efb67d..1ad80b64b 100644
--- a/tests/Server/ServerReloadCommandTest.php
+++ b/tests/Server/ServerReloadCommandTest.php
@@ -4,173 +4,60 @@
namespace Hypervel\Tests\Server;
-use Hypervel\Config\Repository;
use Hypervel\Contracts\Filesystem\FileNotFoundException;
-use Hypervel\Filesystem\Filesystem;
use Hypervel\Server\Commands\ServerReloadCommand;
+use Hypervel\Server\Exceptions\InvalidArgumentException;
+use Hypervel\Server\Exceptions\ServerException;
+use Hypervel\Server\ServerReloader;
use Hypervel\Testbench\TestCase;
-use InvalidArgumentException;
use Mockery as m;
use PHPUnit\Framework\Attributes\DataProvider;
-use Swoole\Constant;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
+use Throwable;
class ServerReloadCommandTest extends TestCase
{
- public function testReloadCommandThrowsCommandExceptionWhenPidFileConfigIsMissing(): void
+ public function testReloadCommandDelegatesToTheServerReloader(): void
{
- $filesystem = m::mock(Filesystem::class);
- $filesystem->shouldNotReceive('get');
- $command = $this->reloadCommand([], $filesystem);
-
- $this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('Configuration value for key [server.settings.pid_file] must be a string, NULL given.');
-
- (new CommandTester($command))->execute([]);
- }
-
- public function testReloadCommandFailsWhenPidFileCannotBeRead(): void
- {
- $filesystem = m::mock(Filesystem::class);
- $filesystem->expects('get')->with('/tmp/hypervel.pid')->andThrow(
- new FileNotFoundException('File does not exist.')
- );
- $command = $this->reloadCommand($this->settings(), $filesystem);
- $tester = new CommandTester($command);
-
- $this->assertSame(Command::FAILURE, $tester->execute([]));
- $this->assertStringContainsString(
- 'Unable to read the server PID file [/tmp/hypervel.pid].',
- $tester->getDisplay(),
- );
- $this->assertSame([], $command->signals);
- }
-
- #[DataProvider('invalidProcessIds')]
- public function testReloadCommandRejectsInvalidProcessIds(string $contents): void
- {
- $filesystem = m::mock(Filesystem::class);
- $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn($contents);
- $command = $this->reloadCommand($this->settings(), $filesystem);
- $tester = new CommandTester($command);
-
- $this->assertSame(Command::FAILURE, $tester->execute([]));
- $this->assertStringContainsString(
- 'The server PID file [/tmp/hypervel.pid] does not contain a valid process ID.',
- $tester->getDisplay(),
- );
- $this->assertSame([], $command->signals);
- }
-
- public static function invalidProcessIds(): array
- {
- return [
- 'empty' => [''],
- 'whitespace' => [" \n"],
- 'malformed' => ['123abc'],
- 'zero' => ['0'],
- 'negative' => ['-123'],
- 'overflow' => ['999999999999999999999999999999'],
- ];
- }
-
- public function testReloadCommandSignalsEventWorkers(): void
- {
- $filesystem = m::mock(Filesystem::class);
- $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn("123\n");
- $command = $this->reloadCommand($this->settings(), $filesystem);
- $command->returnSignalResults(true);
+ $reloader = m::mock(ServerReloader::class);
+ $reloader->expects('reload');
+ $command = $this->reloadCommand($reloader);
$tester = new CommandTester($command);
$this->assertSame(Command::SUCCESS, $tester->execute([]));
- $this->assertSame([[123, SIGUSR1]], $command->signals);
$this->assertStringContainsString('Reloading workers...', $tester->getDisplay());
- $this->assertStringNotContainsString('Reloading task workers...', $tester->getDisplay());
- $this->assertStringContainsString('Done.', $tester->getDisplay());
- }
-
- public function testReloadCommandSignalsEventAndTaskWorkers(): void
- {
- $filesystem = m::mock(Filesystem::class);
- $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn('123');
- $command = $this->reloadCommand($this->settings(taskWorkers: 2), $filesystem);
- $command->returnSignalResults(true, true);
- $tester = new CommandTester($command);
-
- $this->assertSame(Command::SUCCESS, $tester->execute([]));
- $this->assertSame([[123, SIGUSR1], [123, SIGUSR2]], $command->signals);
- $this->assertStringContainsString('Reloading task workers...', $tester->getDisplay());
$this->assertStringContainsString('Done.', $tester->getDisplay());
}
- public function testReloadCommandFailsWhenEventWorkersCannotBeSignaled(): void
+ #[DataProvider('reloadExceptions')]
+ public function testReloadCommandReportsReloadFailures(Throwable $exception): void
{
- $filesystem = m::mock(Filesystem::class);
- $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn('123');
- $command = $this->reloadCommand($this->settings(taskWorkers: 2), $filesystem);
- $command->returnSignalResults(false);
+ $reloader = m::mock(ServerReloader::class);
+ $reloader->expects('reload')->andThrow($exception);
+ $command = $this->reloadCommand($reloader);
$tester = new CommandTester($command);
$this->assertSame(Command::FAILURE, $tester->execute([]));
- $this->assertSame([[123, SIGUSR1]], $command->signals);
- $this->assertStringContainsString('Unable to reload workers.', $tester->getDisplay());
- $this->assertStringNotContainsString('Reloading task workers...', $tester->getDisplay());
- $this->assertStringNotContainsString('Done.', $tester->getDisplay());
- }
-
- public function testReloadCommandFailsWhenTaskWorkersCannotBeSignaled(): void
- {
- $filesystem = m::mock(Filesystem::class);
- $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn('123');
- $command = $this->reloadCommand($this->settings(taskWorkers: 2), $filesystem);
- $command->returnSignalResults(true, false);
- $tester = new CommandTester($command);
-
- $this->assertSame(Command::FAILURE, $tester->execute([]));
- $this->assertSame([[123, SIGUSR1], [123, SIGUSR2]], $command->signals);
- $this->assertStringContainsString('Unable to reload task workers.', $tester->getDisplay());
+ $this->assertStringContainsString('Reloading workers...', $tester->getDisplay());
+ $this->assertStringContainsString($exception->getMessage(), $tester->getDisplay());
$this->assertStringNotContainsString('Done.', $tester->getDisplay());
}
- private function reloadCommand(array $config, Filesystem $filesystem): ServerReloadCommandTestCommand
- {
- $command = new ServerReloadCommandTestCommand(new Repository($config), $filesystem);
- $command->setHypervel($this->app);
-
- return $command;
- }
-
- private function settings(int $taskWorkers = 0): array
+ public static function reloadExceptions(): array
{
return [
- 'server' => [
- 'settings' => [
- Constant::OPTION_PID_FILE => '/tmp/hypervel.pid',
- Constant::OPTION_TASK_WORKER_NUM => $taskWorkers,
- ],
- ],
+ 'unreadable PID file' => [new FileNotFoundException('File does not exist.')],
+ 'invalid PID file' => [new InvalidArgumentException('Invalid process ID.')],
+ 'signal failure' => [new ServerException('Unable to signal workers.')],
];
}
-}
-
-class ServerReloadCommandTestCommand extends ServerReloadCommand
-{
- /** @var list */
- public array $signals = [];
- /** @var list */
- private array $signalResults = [];
-
- public function returnSignalResults(bool ...$results): void
- {
- $this->signalResults = $results;
- }
-
- protected function signalProcess(int $pid, int $signal): bool
+ private function reloadCommand(ServerReloader $reloader): ServerReloadCommand
{
- $this->signals[] = [$pid, $signal];
+ $command = new ServerReloadCommand($reloader);
+ $command->setHypervel($this->app);
- return array_shift($this->signalResults) ?? true;
+ return $command;
}
}
diff --git a/tests/Server/ServerReloaderTest.php b/tests/Server/ServerReloaderTest.php
new file mode 100644
index 000000000..f20d2f832
--- /dev/null
+++ b/tests/Server/ServerReloaderTest.php
@@ -0,0 +1,162 @@
+shouldNotReceive('get');
+
+ $this->expectException(BaseInvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'Configuration value for key [server.settings.pid_file] must be a string, NULL given.'
+ );
+
+ $this->reloader([], $filesystem)->reload();
+ }
+
+ public function testUnreadablePidFileExceptionIsNotWrapped(): void
+ {
+ $exception = new FileNotFoundException('File does not exist.');
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->expects('get')->with('/tmp/hypervel.pid')->andThrow($exception);
+
+ try {
+ $this->reloader($this->settings(), $filesystem)->reload();
+ $this->fail('Expected the PID file exception to be thrown.');
+ } catch (FileNotFoundException $thrown) {
+ $this->assertSame($exception, $thrown);
+ }
+ }
+
+ #[DataProvider('invalidProcessIds')]
+ public function testInvalidProcessIdsAreRejected(string $contents): void
+ {
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn($contents);
+ $reloader = $this->reloader($this->settings(), $filesystem);
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'The server PID file [/tmp/hypervel.pid] does not contain a valid process ID.'
+ );
+
+ $reloader->reload();
+ }
+
+ public static function invalidProcessIds(): array
+ {
+ return [
+ 'empty' => [''],
+ 'whitespace' => [" \n"],
+ 'malformed' => ['123abc'],
+ 'zero' => ['0'],
+ 'negative' => ['-123'],
+ 'overflow' => ['999999999999999999999999999999'],
+ ];
+ }
+
+ public function testReloadSignalsEventWorkers(): void
+ {
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn("123\n");
+ $reloader = $this->reloader($this->settings(), $filesystem);
+ $reloader->returnSignalResults(true);
+
+ $reloader->reload();
+
+ $this->assertSame([[123, SIGUSR1]], $reloader->signals);
+ }
+
+ public function testReloadSignalsEventAndTaskWorkers(): void
+ {
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn('123');
+ $reloader = $this->reloader($this->settings(taskWorkers: 2), $filesystem);
+ $reloader->returnSignalResults(true, true);
+
+ $reloader->reload();
+
+ $this->assertSame([[123, SIGUSR1], [123, SIGUSR2]], $reloader->signals);
+ }
+
+ public function testEventWorkerSignalFailureIsReported(): void
+ {
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn('123');
+ $reloader = $this->reloader($this->settings(taskWorkers: 2), $filesystem);
+ $reloader->returnSignalResults(false);
+
+ $this->expectException(ServerException::class);
+ $this->expectExceptionMessage('Unable to send [SIGUSR1] to reload event workers.');
+
+ $reloader->reload();
+ }
+
+ public function testTaskWorkerSignalFailureIsReported(): void
+ {
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->expects('get')->with('/tmp/hypervel.pid')->andReturn('123');
+ $reloader = $this->reloader($this->settings(taskWorkers: 2), $filesystem);
+ $reloader->returnSignalResults(true, false);
+
+ $this->expectException(ServerException::class);
+ $this->expectExceptionMessage('Unable to send [SIGUSR2] to reload task workers.');
+
+ $reloader->reload();
+ }
+
+ private function reloader(array $config, Filesystem $filesystem): ServerReloaderTestReloader
+ {
+ return new ServerReloaderTestReloader(new Repository($config), $filesystem);
+ }
+
+ private function settings(int $taskWorkers = 0): array
+ {
+ return [
+ 'server' => [
+ 'settings' => [
+ Constant::OPTION_PID_FILE => '/tmp/hypervel.pid',
+ Constant::OPTION_TASK_WORKER_NUM => $taskWorkers,
+ ],
+ ],
+ ];
+ }
+}
+
+class ServerReloaderTestReloader extends ServerReloader
+{
+ /** @var list */
+ public array $signals = [];
+
+ /** @var list */
+ private array $signalResults = [];
+
+ public function returnSignalResults(bool ...$results): void
+ {
+ $this->signalResults = $results;
+ }
+
+ protected function signalProcess(int $pid, int $signal): bool
+ {
+ $this->signals[] = [$pid, $signal];
+
+ return array_shift($this->signalResults) ?? true;
+ }
+}
diff --git a/tests/Session/SessionServiceProviderTest.php b/tests/Session/SessionServiceProviderTest.php
new file mode 100644
index 000000000..09048c3d1
--- /dev/null
+++ b/tests/Session/SessionServiceProviderTest.php
@@ -0,0 +1,38 @@
+app->make('redirect');
+ $responseFactory = $this->app->make(ResponseFactoryContract::class);
+ $session = $this->app->make('session');
+ $store = $this->app->make('session.store');
+
+ $this->app->getProvider(SessionServiceProvider::class)->reloadConfiguration();
+
+ $refreshedStore = $this->app->make('session.store');
+
+ $this->assertSame($session, $this->app->make('session'));
+ $this->assertNotSame($store, $refreshedStore);
+ $this->assertSame($redirector, $this->app->make('redirect'));
+ $this->assertSame($responseFactory, $this->app->make(ResponseFactoryContract::class));
+ $this->assertSame(
+ $refreshedStore,
+ (new ReflectionClass($redirector))->getProperty('session')->getValue($redirector),
+ );
+ $this->assertSame(
+ $redirector,
+ (new ReflectionClass($responseFactory))->getProperty('redirector')->getValue($responseFactory),
+ );
+ }
+}
diff --git a/tests/Socialite/SocialiteManagerTest.php b/tests/Socialite/SocialiteManagerTest.php
index 209b095b7..b032f0ba4 100644
--- a/tests/Socialite/SocialiteManagerTest.php
+++ b/tests/Socialite/SocialiteManagerTest.php
@@ -7,6 +7,7 @@
use Hypervel\Config\Repository;
use Hypervel\Context\RequestContext;
use Hypervel\Contracts\Container\Container;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Coroutine\Coroutine;
use Hypervel\Http\Request;
use Hypervel\Socialite\Contracts\Factory;
@@ -17,6 +18,7 @@
use Hypervel\Socialite\Two\GitlabProvider;
use Hypervel\Testbench\TestCase;
use Hypervel\Tests\Socialite\Fixtures\OAuthTwoTestProviderStub;
+use Mockery as m;
use ReflectionProperty;
use Swoole\Coroutine\Channel;
@@ -67,6 +69,38 @@ public function testFactoryAndConcreteManagerShareOneDriverRegistry(): void
$this->assertSame($manager->driver('custom'), $factory->driver('custom'));
}
+ public function testProviderReloadClearsDriversButPreservesTheManagerAndCustomCreators(): void
+ {
+ $manager = $this->app->make(SocialiteManager::class);
+ $manager->extend('custom', static fn (Container $container) => new OAuthTwoTestProviderStub(
+ $container->make('request'),
+ 'client_id',
+ 'client_secret',
+ 'redirect'
+ ));
+ $originalGithub = $manager->driver('github');
+ $originalCustom = $manager->driver('custom');
+
+ config()->set('services.github.client_id', 'reloaded-client-id');
+
+ (new SocialiteServiceProvider($this->app))->reloadConfiguration();
+
+ $reloadedGithub = $manager->driver('github')->stateless();
+ $this->assertSame($manager, $this->app->make(SocialiteManager::class));
+ $this->assertNotSame($originalGithub, $reloadedGithub);
+ $this->assertNotSame($originalCustom, $manager->driver('custom'));
+ $this->assertStringContainsString('client_id=reloaded-client-id', $reloadedGithub->redirect()->getTargetUrl());
+ }
+
+ public function testProviderReloadDoesNotResolveAnUnusedManager(): void
+ {
+ $app = m::mock(ApplicationContract::class);
+ $app->shouldReceive('resolved')->once()->with(SocialiteManager::class)->andReturnFalse();
+ $app->shouldNotReceive('make');
+
+ (new SocialiteServiceProvider($app))->reloadConfiguration();
+ }
+
public function testGitlabDriverUsesConfiguredHost(): void
{
$this->app->make('config')
diff --git a/tests/Support/SupportCapsuleManagerTraitTest.php b/tests/Support/SupportCapsuleManagerTraitTest.php
index 2a05bba0f..f21e135a9 100644
--- a/tests/Support/SupportCapsuleManagerTraitTest.php
+++ b/tests/Support/SupportCapsuleManagerTraitTest.php
@@ -15,23 +15,23 @@ class SupportCapsuleManagerTraitTest extends TestCase
{
use CapsuleManagerTrait;
- public function testSetupContainerForCapsule()
+ public function testSetupContainerForCapsule(): void
{
$app = new Container;
$this->setupContainer($app);
- $this->assertEquals($app, $this->getContainer());
- $this->assertInstanceOf(Fluent::class, $app['config']);
+ $this->assertSame($app, $this->getContainer());
+ $this->assertInstanceOf(Fluent::class, $app->make('config'));
}
- public function testSetupContainerForCapsuleWhenConfigIsBound()
+ public function testSetupContainerForCapsuleWhenConfigIsBound(): void
{
$app = new Container;
- $app['config'] = new Repository([]);
+ $app->instance('config', new Repository([]));
$this->setupContainer($app);
- $this->assertEquals($app, $this->getContainer());
- $this->assertInstanceOf(Repository::class, $app['config']);
+ $this->assertSame($app, $this->getContainer());
+ $this->assertInstanceOf(Repository::class, $app->make('config'));
}
public function testFlushStateClearsGlobalInstance()
diff --git a/tests/Support/SupportFacadeTest.php b/tests/Support/SupportFacadeTest.php
index 5e8f4389c..22408d6d0 100755
--- a/tests/Support/SupportFacadeTest.php
+++ b/tests/Support/SupportFacadeTest.php
@@ -4,7 +4,7 @@
namespace Hypervel\Tests\Support\SupportFacadeTest;
-use ArrayAccess;
+use Hypervel\Container\Container;
use Hypervel\Support\Facades\Facade;
use Hypervel\Support\Testing\Fakes\Fake;
use Hypervel\Tests\TestCase;
@@ -22,29 +22,29 @@ protected function setUp(): void
FacadeStub::setFacadeApplication(null);
}
- public function testFacadeCallsUnderlyingApplication()
+ public function testFacadeCallsUnderlyingApplication(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => $mock = m::mock(stdClass::class)]);
+ $app->setInstances(['foo' => $mock = m::mock(stdClass::class)]);
$mock->shouldReceive('bar')->once()->andReturn('baz');
FacadeStub::setFacadeApplication($app);
$this->assertSame('baz', FacadeStub::bar());
}
- public function testShouldReceiveReturnsAMockeryMock()
+ public function testShouldReceiveReturnsAMockeryMock(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => new stdClass]);
+ $app->setInstances(['foo' => new stdClass]);
FacadeStub::setFacadeApplication($app);
$this->assertInstanceOf(MockInterface::class, $mock = FacadeStub::shouldReceive('foo')->once()->with('bar')->andReturn('baz')->getMock());
- $this->assertSame('baz', $app['foo']->foo('bar'));
+ $this->assertSame('baz', $app->make('foo')->foo('bar'));
}
- public function testSpyReturnsAMockerySpy()
+ public function testSpyReturnsAMockerySpy(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => new stdClass]);
+ $app->setInstances(['foo' => new stdClass]);
FacadeStub::setFacadeApplication($app);
$this->assertInstanceOf(MockInterface::class, $spy = FacadeStub::spy());
@@ -53,16 +53,16 @@ public function testSpyReturnsAMockerySpy()
$spy->shouldHaveReceived('foo');
}
- public function testShouldReceiveCanBeCalledTwice()
+ public function testShouldReceiveCanBeCalledTwice(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => new stdClass]);
+ $app->setInstances(['foo' => new stdClass]);
FacadeStub::setFacadeApplication($app);
$this->assertInstanceOf(MockInterface::class, FacadeStub::shouldReceive('foo')->once()->with('bar')->andReturn('baz')->getMock());
$this->assertInstanceOf(MockInterface::class, FacadeStub::shouldReceive('foo2')->once()->with('bar2')->andReturn('baz2')->getMock());
- $this->assertSame('baz', $app['foo']->foo('bar'));
- $this->assertSame('baz2', $app['foo']->foo2('bar2'));
+ $this->assertSame('baz', $app->make('foo')->foo('bar'));
+ $this->assertSame('baz2', $app->make('foo')->foo2('bar2'));
}
public function testCanBeMockedWithoutUnderlyingInstance()
@@ -71,20 +71,20 @@ public function testCanBeMockedWithoutUnderlyingInstance()
$this->assertSame('bar', FacadeStub::foo());
}
- public function testExpectsReturnsAMockeryMockWithExpectationRequired()
+ public function testExpectsReturnsAMockeryMockWithExpectationRequired(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => new stdClass]);
+ $app->setInstances(['foo' => new stdClass]);
FacadeStub::setFacadeApplication($app);
$this->assertInstanceOf(MockInterface::class, $mock = FacadeStub::expects('foo')->with('bar')->andReturn('baz')->getMock());
- $this->assertSame('baz', $app['foo']->foo('bar'));
+ $this->assertSame('baz', $app->make('foo')->foo('bar'));
}
- public function testFacadeResolvesAgainAfterClearingSpecific()
+ public function testFacadeResolvesAgainAfterClearingSpecific(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => $mock = m::mock(stdClass::class)]);
+ $app->setInstances(['foo' => $mock = m::mock(stdClass::class)]);
$mock->shouldReceive('bar')->times(3)->andReturn('baz');
// Resolve for the first time
@@ -100,10 +100,10 @@ public function testFacadeResolvesAgainAfterClearingSpecific()
$this->assertSame('baz', FacadeStub::bar());
}
- public function testFacadeResolvesAgainAfterClearingAll()
+ public function testFacadeResolvesAgainAfterClearingAll(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => $mock = m::mock(stdClass::class)]);
+ $app->setInstances(['foo' => $mock = m::mock(stdClass::class)]);
$mock->shouldReceive('bar')->times(2)->andReturn('baz');
// Resolve for the first time
@@ -135,16 +135,16 @@ public function testSetFacadeApplicationToNullClearsApp()
$this->assertNull(FacadeStub::getFacadeApplication());
}
- public function testSwapSetsInstanceOnApp()
+ public function testSwapSetsInstanceOnApp(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => new stdClass]);
+ $app->setInstances(['foo' => new stdClass]);
FacadeStub::setFacadeApplication($app);
$replacement = new stdClass;
FacadeStub::swap($replacement);
- $this->assertSame($replacement, $app['foo']);
+ $this->assertSame($replacement, $app->make('foo'));
$this->assertSame($replacement, FacadeStub::getFacadeRoot());
}
@@ -176,26 +176,26 @@ public function testIsFakeReturnsTrueForFakeInstance()
$this->assertTrue(FacadeStub::isFake());
}
- public function testIsFakeReturnsFalseForNonFakeInstance()
+ public function testIsFakeReturnsFalseForNonFakeInstance(): void
{
$app = new ApplicationStub;
- $app->setAttributes(['foo' => new stdClass]);
+ $app->setInstances(['foo' => new stdClass]);
FacadeStub::setFacadeApplication($app);
$this->assertFalse(FacadeStub::isFake());
}
- public function testUncachedFacadeResolvesEachTime()
+ public function testUncachedFacadeResolvesEachTime(): void
{
$app = new CountingApplicationStub;
- $app->setAttributes(['uncached' => new stdClass]);
+ $app->setInstances(['uncached' => new stdClass]);
UncachedFacadeStub::setFacadeApplication($app);
UncachedFacadeStub::getFacadeRoot();
UncachedFacadeStub::getFacadeRoot();
- // offsetGet should be called twice since $cached = false
- $this->assertSame(2, $app->offsetGetCount);
+ // The container should be queried twice since $cached = false.
+ $this->assertSame(2, $app->makeCount);
}
}
@@ -207,38 +207,13 @@ protected static function getFacadeAccessor(): string
}
}
-class ApplicationStub implements ArrayAccess
+class ApplicationStub extends Container
{
- protected array $attributes = [];
-
- public function setAttributes(array $attributes): void
- {
- $this->attributes = $attributes;
- }
-
- public function instance(string $key, mixed $instance): void
- {
- $this->attributes[$key] = $instance;
- }
-
- public function offsetExists($offset): bool
- {
- return isset($this->attributes[$offset]);
- }
-
- public function offsetGet($key): mixed
- {
- return $this->attributes[$key];
- }
-
- public function offsetSet($key, $value): void
- {
- $this->attributes[$key] = $value;
- }
-
- public function offsetUnset($key): void
+ public function setInstances(array $instances): void
{
- unset($this->attributes[$key]);
+ foreach ($instances as $key => $instance) {
+ $this->instance($key, $instance);
+ }
}
}
@@ -258,12 +233,12 @@ protected static function getFacadeAccessor(): string
class CountingApplicationStub extends ApplicationStub
{
- public int $offsetGetCount = 0;
+ public int $makeCount = 0;
- public function offsetGet($key): mixed
+ public function make(string $abstract, array $parameters = []): mixed
{
- ++$this->offsetGetCount;
+ ++$this->makeCount;
- return parent::offsetGet($key);
+ return parent::make($abstract, $parameters);
}
}
diff --git a/tests/Support/SupportMaintenanceModeTest.php b/tests/Support/SupportMaintenanceModeTest.php
index fb4813f12..a19f4c3cf 100644
--- a/tests/Support/SupportMaintenanceModeTest.php
+++ b/tests/Support/SupportMaintenanceModeTest.php
@@ -11,11 +11,11 @@
class SupportMaintenanceModeTest extends TestCase
{
- public function testExtend()
+ public function testExtend(): void
{
MaintenanceMode::extend('test', fn () => new TestMaintenanceMode);
- $this->app->config->set('app.maintenance.driver', 'test');
+ $this->app->make('config')->set('app.maintenance.driver', 'test');
$driver = $this->app->make(MaintenanceModeManager::class)->driver();
@@ -24,7 +24,9 @@ public function testExtend()
public function testCacheDriverPreservesZeroStoreAndEmptyFallback(): void
{
- $this->app->config->set([
+ $config = $this->app->make('config');
+
+ $config->set([
'app.maintenance.driver' => 'cache',
'cache.default' => 'array',
'cache.stores.0' => ['driver' => 'array'],
@@ -32,7 +34,7 @@ public function testCacheDriverPreservesZeroStoreAndEmptyFallback(): void
]);
$this->app->make('cache')->store('0')->put('hypervel:foundation:down', ['store' => 'zero']);
- $this->app->config->set('app.maintenance.store', '0');
+ $config->set('app.maintenance.store', '0');
$this->assertSame(
['store' => 'zero'],
@@ -40,7 +42,7 @@ public function testCacheDriverPreservesZeroStoreAndEmptyFallback(): void
);
$this->app->make('cache')->store('array')->put('hypervel:foundation:down', ['store' => 'default']);
- $this->app->config->set('app.maintenance.store', '');
+ $config->set('app.maintenance.store', '');
$this->assertSame(
['store' => 'default'],
diff --git a/tests/Telescope/FeatureTestCase.php b/tests/Telescope/FeatureTestCase.php
index f4ab9a4c7..16d9b7610 100644
--- a/tests/Telescope/FeatureTestCase.php
+++ b/tests/Telescope/FeatureTestCase.php
@@ -64,7 +64,7 @@ protected function setUp(): void
$this->app->make(CacheFactoryContract::class)
->forever('telescope:dump-watcher', true);
- $this->app['env'] = 'production';
+ $this->app->instance('env', 'production');
// Clear any entries recorded during bootstrap (e.g. migrations).
Telescope::flushEntries();
diff --git a/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php b/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php
index 931f46b8e..407afa1db 100644
--- a/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php
+++ b/tests/Telescope/Storage/DatabaseEntriesRepositoryTest.php
@@ -21,6 +21,34 @@
class DatabaseEntriesRepositoryTest extends FeatureTestCase
{
+ public function testConfigurationCanBeReloaded(): void
+ {
+ $repository = new class('initial', 25) extends DatabaseEntriesRepository {
+ public function connection(): string
+ {
+ return $this->connection;
+ }
+
+ public function chunkSize(): int
+ {
+ return $this->chunkSize;
+ }
+ };
+
+ $this->assertSame('initial', $repository->connection());
+ $this->assertSame(25, $repository->chunkSize());
+
+ $repository->setConnection('refreshed');
+ $repository->setChunkSize(null);
+
+ $this->assertSame('refreshed', $repository->connection());
+ $this->assertSame(1000, $repository->chunkSize());
+
+ $repository->setChunkSize(0);
+
+ $this->assertSame(1000, $repository->chunkSize());
+ }
+
public function testFindEntryByUuid(): void
{
$entry = EntryModelFactory::new()->create();
diff --git a/tests/Telescope/TelescopeServiceProviderTest.php b/tests/Telescope/TelescopeServiceProviderTest.php
index 5b10a60e5..146fce063 100644
--- a/tests/Telescope/TelescopeServiceProviderTest.php
+++ b/tests/Telescope/TelescopeServiceProviderTest.php
@@ -4,10 +4,19 @@
namespace Hypervel\Tests\Telescope;
+use Hypervel\Container\Container;
use Hypervel\Context\CoroutineContext;
+use Hypervel\Contracts\Config\Repository as ConfigRepository;
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Coroutine\Coroutine;
+use Hypervel\Telescope\Contracts\ClearableRepository;
+use Hypervel\Telescope\Contracts\EntriesRepository;
+use Hypervel\Telescope\Contracts\PrunableRepository;
+use Hypervel\Telescope\Storage\DatabaseEntriesRepository;
use Hypervel\Telescope\Telescope;
+use Hypervel\Telescope\TelescopeServiceProvider;
+use Mockery as m;
+use ReflectionProperty;
class TelescopeServiceProviderTest extends FeatureTestCase
{
@@ -83,4 +92,50 @@ public function testForkInheritsOmittedTelescopeContextFromParent(): void
$this->assertSame([true, 'selected'], $observed);
}
+
+ public function testReloadConfigurationUpdatesEveryResolvedDatabaseRepositoryInPlace(): void
+ {
+ $entries = $this->app->make(EntriesRepository::class);
+ $clearable = $this->app->make(ClearableRepository::class);
+ $prunable = $this->app->make(PrunableRepository::class);
+ $store = new ReflectionProperty(Telescope::class, 'store');
+ $connection = new ReflectionProperty(DatabaseEntriesRepository::class, 'connection');
+ $chunkSize = new ReflectionProperty(DatabaseEntriesRepository::class, 'chunkSize');
+ $instances = new ReflectionProperty(Container::class, 'instances');
+ $autoSingletons = new ReflectionProperty(Container::class, 'autoSingletons');
+
+ config()->set('telescope.storage.database.connection', 'reloaded');
+ config()->set('telescope.storage.database.chunk', 250);
+
+ (new TelescopeServiceProvider($this->app))->reloadConfiguration();
+
+ $this->assertSame($entries, $this->app->make(EntriesRepository::class));
+ $this->assertSame($clearable, $this->app->make(ClearableRepository::class));
+ $this->assertSame($prunable, $this->app->make(PrunableRepository::class));
+ $this->assertSame($entries, $store->getValue());
+
+ foreach ([$entries, $clearable, $prunable] as $repository) {
+ $this->assertInstanceOf(DatabaseEntriesRepository::class, $repository);
+ $this->assertSame('reloaded', $connection->getValue($repository));
+ $this->assertSame(250, $chunkSize->getValue($repository));
+ }
+
+ $this->assertArrayNotHasKey(DatabaseEntriesRepository::class, $instances->getValue($this->app));
+ $this->assertArrayNotHasKey(DatabaseEntriesRepository::class, $autoSingletons->getValue($this->app));
+ }
+
+ public function testReloadConfigurationDoesNotResolveUnusedOrReplacedRepositories(): void
+ {
+ $app = m::mock(ApplicationContract::class);
+ $config = m::mock(ConfigRepository::class);
+ $repository = m::mock(EntriesRepository::class);
+ $app->shouldReceive('resolved')->once()->with(EntriesRepository::class)->andReturnTrue();
+ $app->shouldReceive('resolved')->once()->with(ClearableRepository::class)->andReturnFalse();
+ $app->shouldReceive('resolved')->once()->with(PrunableRepository::class)->andReturnFalse();
+ $app->shouldReceive('make')->once()->with(ConfigRepository::class)->andReturn($config);
+ $app->shouldReceive('make')->once()->with(EntriesRepository::class)->andReturn($repository);
+ $app->shouldNotReceive('make')->with(DatabaseEntriesRepository::class);
+
+ (new TelescopeServiceProvider($app))->reloadConfiguration();
+ }
}
diff --git a/tests/Telescope/Watchers/DumpWatcherTest.php b/tests/Telescope/Watchers/DumpWatcherTest.php
index 8c49b8f8d..a9136cf6e 100644
--- a/tests/Telescope/Watchers/DumpWatcherTest.php
+++ b/tests/Telescope/Watchers/DumpWatcherTest.php
@@ -6,6 +6,7 @@
use Hypervel\Contracts\Cache\Factory as CacheFactory;
use Hypervel\Contracts\Cache\Repository as CacheRepository;
+use Hypervel\Foundation\Providers\FoundationServiceProvider;
use Hypervel\Telescope\EntryType;
use Hypervel\Telescope\Watchers\DumpWatcher;
use Hypervel\Testbench\Attributes\WithConfig;
@@ -153,6 +154,27 @@ public function testWatcherDoesNotStackHandlers(): void
$this->assertCount(1, $this->loadTelescopeEntries());
}
+ public function testFoundationConfigurationReloadPreservesInstalledWatcher(): void
+ {
+ cache()->forever('telescope:dump-watcher', true);
+ $handler = $this->varDumperHandler();
+
+ $this->assertTrue($this->watcherInstalled());
+ $this->assertNotNull($handler);
+
+ config(['view.compiled' => '/tmp/reloaded-compiled-views']);
+ $this->app->getProvider(FoundationServiceProvider::class)->reloadConfiguration();
+
+ $this->assertSame($handler, $this->varDumperHandler());
+
+ VarDumper::dump('recorded-after-reload');
+
+ $entry = $this->loadTelescopeEntries()->first();
+
+ $this->assertSame(EntryType::DUMP, $entry->type);
+ $this->assertStringContainsString('recorded-after-reload', $entry->content['dump']);
+ }
+
public function testFlushStateDropsThePriorApplicationHandlerAndAllowsReregistration(): void
{
$this->installWatcher(
diff --git a/tests/Telescope/Watchers/ReverbWatcherTest.php b/tests/Telescope/Watchers/ReverbWatcherTest.php
index bfcdae4f1..62f700ac4 100644
--- a/tests/Telescope/Watchers/ReverbWatcherTest.php
+++ b/tests/Telescope/Watchers/ReverbWatcherTest.php
@@ -52,7 +52,9 @@ protected function defineEnvironment(ApplicationContract $app): void
{
parent::defineEnvironment($app);
- $app['config']->set('reverb.apps.apps', [
+ $config = $app->make('config');
+
+ $config->set('reverb.apps.apps', [
[
'key' => 'reverb-key',
'secret' => 'reverb-secret',
@@ -85,10 +87,10 @@ protected function defineEnvironment(ApplicationContract $app): void
],
];
- $app['config']->set('database.redis.options', []);
- $app['config']->set('database.redis.default', $redisConnection);
- $app['config']->set('database.redis.queue', $redisConnection);
- $app['config']->set('database.redis.reverb', $redisConnection);
+ $config->set('database.redis.options', []);
+ $config->set('database.redis.default', $redisConnection);
+ $config->set('database.redis.queue', $redisConnection);
+ $config->set('database.redis.reverb', $redisConnection);
$server = m::mock(Server::class);
$server->shouldReceive('sendMessage')->zeroOrMoreTimes();
diff --git a/tests/Testbench/AttributeEnvironmentSetupTest.php b/tests/Testbench/AttributeEnvironmentSetupTest.php
index ca99499c7..f9e9cd48b 100644
--- a/tests/Testbench/AttributeEnvironmentSetupTest.php
+++ b/tests/Testbench/AttributeEnvironmentSetupTest.php
@@ -74,7 +74,7 @@ public function itDoesntLoadInvalidEnvironmentConfig(): void
*/
protected function classConfig(ApplicationContract $app): void
{
- $app['config']->set('testbench.class', 'testbench');
+ $app->make('config')->set('testbench.class', 'testbench');
}
/**
@@ -82,7 +82,7 @@ protected function classConfig(ApplicationContract $app): void
*/
protected function globalConfig(ApplicationContract $app): void
{
- $app['config']->set('testbench.global', 'testbench');
+ $app->make('config')->set('testbench.global', 'testbench');
}
/**
@@ -90,7 +90,7 @@ protected function globalConfig(ApplicationContract $app): void
*/
protected function firstConfig(ApplicationContract $app): void
{
- $app['config']->set('testbench.one', 'testbench');
+ $app->make('config')->set('testbench.one', 'testbench');
}
/**
@@ -98,7 +98,7 @@ protected function firstConfig(ApplicationContract $app): void
*/
protected function secondConfig(ApplicationContract $app): void
{
- $app['config']->set('testbench.two', 'testbench');
+ $app->make('config')->set('testbench.two', 'testbench');
}
/**
@@ -106,8 +106,10 @@ protected function secondConfig(ApplicationContract $app): void
*/
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('database.default', 'testbench');
- $app['config']->set('database.connections.testbench', [
+ $config = $app->make('config');
+
+ $config->set('database.default', 'testbench');
+ $config->set('database.connections.testbench', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
diff --git a/tests/Testbench/Attributes/ResolvesHypervelTest.php b/tests/Testbench/Attributes/ResolvesHypervelTest.php
index 976cc6952..2a01f875e 100644
--- a/tests/Testbench/Attributes/ResolvesHypervelTest.php
+++ b/tests/Testbench/Attributes/ResolvesHypervelTest.php
@@ -16,7 +16,7 @@ class ResolvesHypervelTest extends TestCase
#[ResolvesHypervel('hypervelDefaultConfiguration')]
public function itCanResolveDefinedConfiguration(): void
{
- $this->assertSame(LoadConfiguration::class, $this->app[LoadConfiguration::class]::class);
+ $this->assertSame(LoadConfiguration::class, $this->app->make(LoadConfiguration::class)::class);
}
/**
diff --git a/tests/Testbench/Attributes/UsesFrameworkConfigurationTest.php b/tests/Testbench/Attributes/UsesFrameworkConfigurationTest.php
index 28792ef8e..9a573bdb7 100644
--- a/tests/Testbench/Attributes/UsesFrameworkConfigurationTest.php
+++ b/tests/Testbench/Attributes/UsesFrameworkConfigurationTest.php
@@ -4,8 +4,11 @@
namespace Hypervel\Tests\Testbench\Attributes;
+use App\Models\User as ApplicationUser;
+use Hypervel\Foundation\Auth\User as FoundationUser;
use Hypervel\Foundation\Bootstrap\LoadConfiguration;
use Hypervel\Testbench\Attributes\UsesFrameworkConfiguration;
+use Hypervel\Testbench\Bootstrap\LoadConfiguration as TestbenchLoadConfiguration;
use Hypervel\Testbench\Foundation\Env;
use Hypervel\Testbench\TestCase;
use PHPUnit\Framework\Attributes\Test;
@@ -17,23 +20,23 @@ class UsesFrameworkConfigurationTest extends TestCase
#[Test]
public function itCanLoadUsingTestbenchConfigurations(): void
{
- $this->assertSame(\Hypervel\Testbench\Bootstrap\LoadConfiguration::class, $this->app[LoadConfiguration::class]::class);
+ $this->assertSame(TestbenchLoadConfiguration::class, $this->app->make(LoadConfiguration::class)::class);
$environment = Env::has('TESTBENCH_PACKAGE_TESTER') ? 'testing' : 'workbench';
$this->assertSame($environment, config('app.env'));
- $this->assertSame(\Hypervel\Foundation\Auth\User::class, config('auth.providers.users.model'));
+ $this->assertSame(FoundationUser::class, config('auth.providers.users.model'));
}
#[Test]
#[UsesFrameworkConfiguration]
public function itCanLoadUsingFrameworkConfigurations(): void
{
- $this->assertSame(LoadConfiguration::class, $this->app[LoadConfiguration::class]::class);
+ $this->assertSame(LoadConfiguration::class, $this->app->make(LoadConfiguration::class)::class);
$environment = Env::has('TESTBENCH_PACKAGE_TESTER') ? 'testing' : 'production';
$this->assertSame($environment, config('app.env'));
- $this->assertSame(\App\Models\User::class, config('auth.providers.users.model'));
+ $this->assertSame(ApplicationUser::class, config('auth.providers.users.model'));
}
}
diff --git a/tests/Testbench/Concerns/CreatesApplicationTest.php b/tests/Testbench/Concerns/CreatesApplicationTest.php
index 43ad6821e..eb1be922b 100644
--- a/tests/Testbench/Concerns/CreatesApplicationTest.php
+++ b/tests/Testbench/Concerns/CreatesApplicationTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Testbench\Concerns;
+use Hypervel\Contracts\Events\Dispatcher;
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Foundation\Bootstrap\LoadEnvironmentVariables;
use Hypervel\Testbench\TestCase;
@@ -43,8 +44,7 @@ public function testGetPackageAliasesReturnsAliases(): void
public function testRegisterPackageProvidersRegistersProviders(): void
{
- // The provider should be registered via defineEnvironment
- // which calls registerPackageProviders
+ // The package provider is registered during application configuration.
$this->assertTrue(
$this->app->providerIsLoaded(TestServiceProvider::class),
'TestServiceProvider should be registered'
@@ -59,21 +59,20 @@ public function testRegisterPackageAliasesAddsToConfig(): void
$this->assertSame(TestFacade::class, $aliases['TestAlias']);
}
- public function testAfterLoadingEnvironmentFiresThroughTestbenchPath(): void
+ public function testAfterLoadingEnvironmentRegistersThroughTestbenchPath(): void
{
// The bootstrapped event should have been dispatched by bootstrapWith()
// in CreatesApplication::resolveApplicationConfiguration().
- $listeners = $this->app['events']->getListeners(
+ $events = $this->app->make(Dispatcher::class);
+ $listeners = $events->getListeners(
'bootstrapped: ' . LoadEnvironmentVariables::class
);
// Register a callback now and verify it gets added to the listener list.
- $called = false;
- $this->app->afterLoadingEnvironment(function () use (&$called) {
- $called = true;
+ $this->app->afterLoadingEnvironment(static function (): void {
});
- $updatedListeners = $this->app['events']->getListeners(
+ $updatedListeners = $events->getListeners(
'bootstrapped: ' . LoadEnvironmentVariables::class
);
diff --git a/tests/Testbench/Concerns/DefineCacheRoutesTest.php b/tests/Testbench/Concerns/DefineCacheRoutesTest.php
index e9dfe4625..3ff9458d8 100644
--- a/tests/Testbench/Concerns/DefineCacheRoutesTest.php
+++ b/tests/Testbench/Concerns/DefineCacheRoutesTest.php
@@ -40,7 +40,7 @@ public function testCompiledRouteCollectionIsInstalledAfterDefineCacheRoutes():
);
$this->assertInstanceOf(
RouteCollection::class,
- $this->app['router']->getRoutes()
+ $this->app->make(Router::class)->getRoutes()
);
$this->defineCacheRoutes(<<<'PHP'
@@ -51,7 +51,7 @@ public function testCompiledRouteCollectionIsInstalledAfterDefineCacheRoutes():
$this->assertInstanceOf(
CompiledRouteCollection::class,
- $this->app['router']->getRoutes()
+ $this->app->make(Router::class)->getRoutes()
);
}
@@ -91,8 +91,7 @@ public function testNamedRoutesSurviveCaching(): void
Route::get('/named', fn () => 'named_response')->name('test.named');
PHP);
- /** @var Router $router */
- $router = $this->app['router'];
+ $router = $this->app->make(Router::class);
$routes = $router->getRoutes();
$this->assertNotNull($routes->getByName('test.named'));
@@ -307,7 +306,7 @@ public function testSetUpApplicationRoutesSkipsWhenRoutesCached(): void
// routesAreCached() should return true
$this->assertTrue($this->app->routesAreCached());
- // Routes from defineRoutes() should NOT be registered since
+ // Routes from defineRoutes() should not be registered since
// setUpApplicationRoutes returns early when routes are cached.
// Only the cached /cached-only route should exist.
$this->get('/cached-only')->assertOk();
diff --git a/tests/Testbench/DefaultConfigurationTest.php b/tests/Testbench/DefaultConfigurationTest.php
index c7b73aea2..6e58ed62e 100644
--- a/tests/Testbench/DefaultConfigurationTest.php
+++ b/tests/Testbench/DefaultConfigurationTest.php
@@ -21,19 +21,19 @@ class DefaultConfigurationTest extends TestCase
#[Test]
public function itCanLoadUsingTestbenchConfigurations(): void
{
- $this->assertSame(\Hypervel\Testbench\Bootstrap\LoadConfiguration::class, \get_class($this->app[LoadConfiguration::class]));
+ $this->assertSame(TestbenchLoadConfiguration::class, $this->app->make(LoadConfiguration::class)::class);
}
#[Test]
public function itPopulatesExpectedDebugConfig(): void
{
- $this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER'), $this->app['config']['app.debug']);
+ $this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER'), $this->app->make('config')->boolean('app.debug'));
}
#[Test]
public function itPopulatesExpectedAppKeyConfig(): void
{
- $this->assertSame('AckfSECXIvnK5r28GVIWUAxmbBSjTsmF', $this->app['config']['app.key']);
+ $this->assertSame('AckfSECXIvnK5r28GVIWUAxmbBSjTsmF', $this->app->make('config')->string('app.key'));
}
#[Test]
@@ -43,7 +43,7 @@ public function itPopulatesExpectedTestingConfig(): void
'driver' => 'sqlite',
'database' => ':memory:',
'foreign_key_constraints' => false,
- ], $this->app['config']['database.connections.testing']);
+ ], $this->app->make('config')->array('database.connections.testing'));
$this->assertTrue($this->usesSqliteInMemoryDatabaseConnection('testing'));
$this->assertFalse($this->usesSqliteInMemoryDatabaseConnection('sqlite'));
@@ -69,9 +69,10 @@ public function itUsesTheCanonicalSqliteMemoryClassification(): void
#[Test]
public function itFallsBackToTheTestingConnectionWhenRuntimeSqliteIsMissing(): void
{
- $sqliteDatabase = $this->app['config']['database.connections.sqlite.database'];
+ $config = $this->app->make('config');
+ $sqliteDatabase = $config->string('database.connections.sqlite.database');
- $this->assertSame('testing', $this->app['config']['database.default']);
+ $this->assertSame('testing', $config->string('database.default'));
$this->assertSame(BASE_PATH . '/database/database.sqlite', $sqliteDatabase);
$this->assertFileDoesNotExist($sqliteDatabase);
}
@@ -116,30 +117,34 @@ public static function sqliteNonFileIdentifiers(): array
#[Test]
public function itPopulatesExpectedCacheDefaults(): void
{
- $this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER') ? 'database' : 'array', $this->app['config']['cache.default']);
- $this->assertFalse($this->app['config']['cache.serializable_classes']);
+ $config = $this->app->make('config');
+
+ $this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER') ? 'database' : 'array', $config->string('cache.default'));
+ $this->assertFalse($config->boolean('cache.serializable_classes'));
}
#[Test]
public function itPopulatesExpectedRateLimiterDefaults(): void
{
- $this->assertSame('worker-array', $this->app['config']['rate-limiter.default']);
+ $config = $this->app->make('config');
+
+ $this->assertSame('worker-array', $config->string('rate-limiter.default'));
$this->assertSame(
['database', 'redis', 'swoole', 'worker-array'],
- array_keys($this->app['config']['rate-limiter.stores']),
+ array_keys($config->array('rate-limiter.stores')),
);
}
#[Test]
public function itPopulatesExpectedSessionDefaults(): void
{
- $this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER') ? 'cookie' : 'array', $this->app['config']['session.driver']);
+ $this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER') ? 'cookie' : 'array', $this->app->make('config')->string('session.driver'));
}
#[Test]
public function itPopulatesExpectedRedisConnections(): void
{
- $connections = $this->app['config']['database.redis'];
+ $connections = $this->app->make('config')->array('database.redis');
$this->assertArrayHasKey('default', $connections);
$this->assertArrayHasKey('cache', $connections);
@@ -159,6 +164,6 @@ public function itUsesImmutableDatesByDefault(): void
#[Test]
public function itResolvesTheDefaultUserModel(): void
{
- $this->assertSame(User::class, $this->app['config']['auth.providers.users.model']);
+ $this->assertSame(User::class, $this->app->make('config')->string('auth.providers.users.model'));
}
}
diff --git a/tests/Testbench/Fixtures/Providers/ChildServiceProvider.php b/tests/Testbench/Fixtures/Providers/ChildServiceProvider.php
index 28518ac90..7deb52617 100644
--- a/tests/Testbench/Fixtures/Providers/ChildServiceProvider.php
+++ b/tests/Testbench/Fixtures/Providers/ChildServiceProvider.php
@@ -10,6 +10,6 @@ class ChildServiceProvider extends ServiceProvider
{
public function register(): void
{
- $this->app['child.loaded'] = true;
+ $this->app->instance('child.loaded', true);
}
}
diff --git a/tests/Testbench/Fixtures/Providers/ParentServiceProvider.php b/tests/Testbench/Fixtures/Providers/ParentServiceProvider.php
index 0f43ca002..5c88df00d 100644
--- a/tests/Testbench/Fixtures/Providers/ParentServiceProvider.php
+++ b/tests/Testbench/Fixtures/Providers/ParentServiceProvider.php
@@ -16,6 +16,6 @@ public function register(): void
{
parent::register();
- $this->app['parent.loaded'] = true;
+ $this->app->instance('parent.loaded', true);
}
}
diff --git a/tests/Testbench/Foundation/ApplicationTest.php b/tests/Testbench/Foundation/ApplicationTest.php
index e481295ed..aad91fd0c 100644
--- a/tests/Testbench/Foundation/ApplicationTest.php
+++ b/tests/Testbench/Foundation/ApplicationTest.php
@@ -59,11 +59,12 @@ public function itCanCreateAnApplication(): void
$app = $testbench->createApplication();
$environment = Env::has('TESTBENCH_PACKAGE_TESTER') ? 'testing' : 'workbench';
+ $applicationEnvironment = $app->make('env');
$this->assertInstanceOf(Application::class, $app);
$this->assertSame('App\\', $app->getNamespace());
- $this->assertEquals($environment, $app['env']);
- $this->assertSame($app['env'], $app['config']['app.env']);
+ $this->assertSame($environment, $applicationEnvironment);
+ $this->assertSame($applicationEnvironment, $app->make('config')->string('app.env'));
$this->assertSame($environment, $app->environment());
$this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER'), $app->runningUnitTests());
$this->assertFalse($testbench->isRunningTestCase());
@@ -75,11 +76,12 @@ public function itCanCreateAnApplicationUsingCreateHelper(): void
$app = TestbenchApplication::create((string) default_skeleton_path());
$environment = Env::has('TESTBENCH_PACKAGE_TESTER') ? 'testing' : 'workbench';
+ $applicationEnvironment = $app->make('env');
$this->assertInstanceOf(Application::class, $app);
$this->assertSame('App\\', $app->getNamespace());
- $this->assertEquals($environment, $app['env']);
- $this->assertSame($app['env'], $app['config']['app.env']);
+ $this->assertSame($environment, $applicationEnvironment);
+ $this->assertSame($applicationEnvironment, $app->make('config')->string('app.env'));
$this->assertSame($environment, $app->environment());
$this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER'), $app->runningUnitTests());
}
@@ -94,11 +96,12 @@ public function itCanCreateAnApplicationUsingCreateFromConfigHelper(): void
$app = TestbenchApplication::createFromConfig($config);
$environment = Env::has('TESTBENCH_PACKAGE_TESTER') ? 'testing' : 'workbench';
+ $applicationEnvironment = $app->make('env');
$this->assertInstanceOf(Application::class, $app);
$this->assertSame('App\\', $app->getNamespace());
- $this->assertEquals($environment, $app['env']);
- $this->assertSame($app['env'], $app['config']['app.env']);
+ $this->assertSame($environment, $applicationEnvironment);
+ $this->assertSame($applicationEnvironment, $app->make('config')->string('app.env'));
$this->assertSame($environment, $app->environment());
$this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER'), $app->runningUnitTests());
}
diff --git a/tests/Testbench/Foundation/Bootstrap/CreateVendorSymlinkTest.php b/tests/Testbench/Foundation/Bootstrap/CreateVendorSymlinkTest.php
index c59fcaab2..68c1bff55 100644
--- a/tests/Testbench/Foundation/Bootstrap/CreateVendorSymlinkTest.php
+++ b/tests/Testbench/Foundation/Bootstrap/CreateVendorSymlinkTest.php
@@ -69,7 +69,7 @@ public function itCanCreateVendorSymlink(): void
(new CreateVendorSymlink($workingPath))->bootstrap($application);
- $this->assertTrue($application['TESTBENCH_VENDOR_SYMLINK']);
+ $this->assertTrue($application->make('TESTBENCH_VENDOR_SYMLINK'));
$this->assertSame($config, $application->make('config'));
$application->terminate();
@@ -89,7 +89,7 @@ public function itCanSkipExistingVendorSymlink(): void
(new CreateVendorSymlink($workingPath))->bootstrap($application);
- $this->assertFalse($application['TESTBENCH_VENDOR_SYMLINK']);
+ $this->assertFalse($application->make('TESTBENCH_VENDOR_SYMLINK'));
}
#[Test]
diff --git a/tests/Testbench/Integrations/ConfigTest.php b/tests/Testbench/Integrations/ConfigTest.php
index a0ea08baf..324e97496 100644
--- a/tests/Testbench/Integrations/ConfigTest.php
+++ b/tests/Testbench/Integrations/ConfigTest.php
@@ -15,8 +15,10 @@ class ConfigTest extends TestCase
#[Override]
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('database.default', 'testbench');
- $app['config']->set('database.connections.testbench', [
+ $config = $app->make('config');
+
+ $config->set('database.default', 'testbench');
+ $config->set('database.connections.testbench', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
diff --git a/tests/Testbench/Integrations/EnvironmentVariablesTest.php b/tests/Testbench/Integrations/EnvironmentVariablesTest.php
index eea35cb29..8a4bfbd65 100644
--- a/tests/Testbench/Integrations/EnvironmentVariablesTest.php
+++ b/tests/Testbench/Integrations/EnvironmentVariablesTest.php
@@ -19,7 +19,7 @@ class EnvironmentVariablesTest extends TestCase
#[Override]
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('database.default', 'testing');
+ $app->make('config')->set('database.default', 'testing');
}
#[Override]
diff --git a/tests/Testbench/Integrations/LoadUsingFrameworkConfigurationTest.php b/tests/Testbench/Integrations/LoadUsingFrameworkConfigurationTest.php
index da68edfb4..ea033e034 100644
--- a/tests/Testbench/Integrations/LoadUsingFrameworkConfigurationTest.php
+++ b/tests/Testbench/Integrations/LoadUsingFrameworkConfigurationTest.php
@@ -19,7 +19,7 @@ class LoadUsingFrameworkConfigurationTest extends TestCase
#[ResolvesHypervel('overrideHypervelConfiguration')]
public function itCanLoadUsingFrameworkConfigurations(): void
{
- $this->assertSame(LoadConfiguration::class, $this->app[LoadConfiguration::class]::class);
+ $this->assertSame(LoadConfiguration::class, $this->app->make(LoadConfiguration::class)::class);
$environment = Env::has('TESTBENCH_PACKAGE_TESTER') ? 'testing' : 'production';
diff --git a/tests/Testbench/Integrations/RouteTest.php b/tests/Testbench/Integrations/RouteTest.php
index 29c9acb74..ec15b98be 100644
--- a/tests/Testbench/Integrations/RouteTest.php
+++ b/tests/Testbench/Integrations/RouteTest.php
@@ -100,7 +100,7 @@ public function itCanResolveDomainRoute(): void
#[Test]
public function itCanResolveNameRoutes(): void
{
- $this->app['router']->get('passthrough', fn () => route('bye'))->name('pass');
+ $this->app->make(Router::class)->get('passthrough', fn () => route('bye'))->name('pass');
$response = $this->call('GET', route('pass'));
@@ -111,7 +111,7 @@ public function itCanResolveNameRoutes(): void
#[Test]
public function itCanHandleRouteThrowingException(): void
{
- $this->app['router']->get('bad-route', fn () => throw new Exception('Route error!'))->name('bad');
+ $this->app->make(Router::class)->get('bad-route', fn () => throw new Exception('Route error!'))->name('bad');
$response = $this->call('GET', route('bad'));
diff --git a/tests/Testbench/TestCaseTest.php b/tests/Testbench/TestCaseTest.php
index 852146550..e11bfb192 100644
--- a/tests/Testbench/TestCaseTest.php
+++ b/tests/Testbench/TestCaseTest.php
@@ -36,10 +36,10 @@ public function testDummy(): void
$this->assertInstanceOf(Application::class, $app);
$this->assertEquals('UTC', date_default_timezone_get());
- $this->assertEquals('testing', $app['env']);
+ $this->assertSame('testing', $app->make('env'));
$this->assertSame('testing', $app->environment());
$this->assertTrue($app->runningUnitTests());
- $this->assertInstanceOf(ConfigRepository::class, $app['config']);
+ $this->assertInstanceOf(ConfigRepository::class, $app->make('config'));
$this->assertInstanceOf(TestCaseContract::class, $testbench);
$this->assertTrue($testbench->isRunningTestCase());
@@ -59,10 +59,10 @@ public function itCanCreateAContainer(): void
$this->assertInstanceOf(Application::class, $app);
$this->assertEquals('UTC', date_default_timezone_get());
- $this->assertEquals($environment, $app['env']);
+ $this->assertSame($environment, $app->make('env'));
$this->assertSame($environment, $app->environment());
$this->assertSame(Env::has('TESTBENCH_PACKAGE_TESTER'), $app->runningUnitTests());
- $this->assertInstanceOf(ConfigRepository::class, $app['config']);
+ $this->assertInstanceOf(ConfigRepository::class, $app->make('config'));
$this->assertFalse($container->isRunningTestCase());
$this->assertFalse($container->isRunningTestCaseUsingPest());
diff --git a/tests/Testbench/TestCaseTraitsTest.php b/tests/Testbench/TestCaseTraitsTest.php
index e0377cef7..9981451fb 100644
--- a/tests/Testbench/TestCaseTraitsTest.php
+++ b/tests/Testbench/TestCaseTraitsTest.php
@@ -103,7 +103,7 @@ public function testAppIsAvailable(): void
public function testPackageTestCaseRunsInTestingEnvironment(): void
{
- $this->assertSame('testing', $this->app['env']);
+ $this->assertSame('testing', $this->app->make('env'));
$this->assertSame('testing', $this->app->environment());
$this->assertTrue($this->app->runningUnitTests());
}
diff --git a/tests/Testbench/TestbenchTest.php b/tests/Testbench/TestbenchTest.php
index 21d2daf75..f72a97f49 100644
--- a/tests/Testbench/TestbenchTest.php
+++ b/tests/Testbench/TestbenchTest.php
@@ -5,6 +5,7 @@
namespace Hypervel\Tests\Testbench;
use Hypervel\Contracts\Bus\QueueingDispatcher;
+use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Queue\Queue;
use Hypervel\Testbench\Attributes\DefineEnvironment;
use Hypervel\Testbench\Concerns\Testing;
@@ -32,14 +33,14 @@ public function itCanHandleCustomQueuePayload(): void
$this->addToAssertionCount(1);
}
- protected function registerCustomQueuePayload(\Hypervel\Contracts\Foundation\Application $app): void
+ protected function registerCustomQueuePayload(ApplicationContract $app): void
{
- $app->bind('one.time.password', fn (): int => random_int(1, 10));
+ $app->instance('one.time.password', random_int(1, 10));
Queue::createPayloadUsing(function () use ($app): array {
$password = $app->make('one.time.password');
- $app->offsetUnset('one.time.password');
+ $app->forgetInstance('one.time.password');
return ['password' => $password];
});
diff --git a/tests/Testing/Concerns/TestCachesTest.php b/tests/Testing/Concerns/TestCachesTest.php
index b46cf33ea..8a1bb4f41 100644
--- a/tests/Testing/Concerns/TestCachesTest.php
+++ b/tests/Testing/Concerns/TestCachesTest.php
@@ -60,7 +60,7 @@ protected function tearDown(): void
#[DataProvider('cachePrefixes')]
public function testCachePrefixAppendsToken(string $prefix, string $token, string $expected): void
{
- Container::getInstance()['config']->set('cache.prefix', $prefix);
+ Container::getInstance()->make('config')->set('cache.prefix', $prefix);
Container::getInstance()->make(ParallelTesting::class)->resolveTokenUsing(fn () => $token);
$this->assertSame($expected, $this->getParallelSafeCachePrefix());
@@ -87,11 +87,11 @@ public function testCachePrefixDoesNotReuseCustomPrefixFromPreviousCall(): void
{
Container::getInstance()->make(ParallelTesting::class)->resolveTokenUsing(fn () => '1');
- Container::getInstance()['config']->set('cache.prefix', 'custom_cache_');
+ Container::getInstance()->make('config')->set('cache.prefix', 'custom_cache_');
$this->assertSame('custom_cache_test_1_', $this->getParallelSafeCachePrefix());
- Container::getInstance()['config']->set('cache.prefix', 'myapp_cache_');
+ Container::getInstance()->make('config')->set('cache.prefix', 'myapp_cache_');
$this->assertSame('myapp_cache_test_1_', $this->getParallelSafeCachePrefix());
}
@@ -99,7 +99,7 @@ public function testCachePrefixDoesNotReuseCustomPrefixFromPreviousCall(): void
public function testCachePrefixDoesNotDoubleAppendToken(): void
{
Container::getInstance()->make(ParallelTesting::class)->resolveTokenUsing(fn () => '1');
- Container::getInstance()['config']->set('cache.prefix', 'myapp_cache_test_1_');
+ Container::getInstance()->make('config')->set('cache.prefix', 'myapp_cache_test_1_');
$this->assertSame('myapp_cache_test_1_', $this->getParallelSafeCachePrefix());
}
@@ -108,7 +108,7 @@ public function testSwitchToCachePrefixUpdatesConfig(): void
{
$this->switchToCachePrefix('new_prefix_');
- $this->assertSame('new_prefix_', Container::getInstance()['config']->get('cache.prefix'));
+ $this->assertSame('new_prefix_', Container::getInstance()->make('config')->get('cache.prefix'));
}
public function testBootTestCacheRegistersSetUpTestCaseCallback(): void
@@ -142,7 +142,7 @@ public function testBootTestCacheSkipsIsolationIfOptedOut(): void
Container::getInstance()->make(ParallelTesting::class)->callSetUpTestCaseCallbacks(new class {});
- $this->assertSame('myapp_cache_', Container::getInstance()['config']->get('cache.prefix'));
+ $this->assertSame('myapp_cache_', Container::getInstance()->make('config')->get('cache.prefix'));
} finally {
if ($hadValue) {
$_SERVER['HYPERVEL_PARALLEL_TESTING_WITHOUT_CACHE'] = $original;
@@ -157,15 +157,17 @@ public function testSwitchToCachePrefixDoesNotRemoveResolvedDrivers(): void
$container = Container::getInstance();
$container->singleton('cache', fn ($app) => new CacheManager($app));
+ $config = $container->make('config');
- $container['config']->set('cache.default', 'array');
- $container['config']->set('cache.stores.array', ['driver' => 'array']);
+ $config->set('cache.default', 'array');
+ $config->set('cache.stores.array', ['driver' => 'array']);
- $driver = $container['cache']->driver();
+ $cache = $container->make('cache');
+ $driver = $cache->driver();
$this->switchToCachePrefix('new_prefix_');
- $this->assertSame($driver, $container['cache']->driver());
+ $this->assertSame($driver, $cache->driver());
}
protected function getParallelSafeCachePrefix(): string
@@ -190,7 +192,7 @@ protected function makeTestCachesInstance(): object
return new class {
use TestCaches;
- public $app;
+ public Container $app;
public function __construct()
{
diff --git a/tests/Testing/Concerns/TestViewsTest.php b/tests/Testing/Concerns/TestViewsTest.php
index 30ba1219c..28b7e72c7 100644
--- a/tests/Testing/Concerns/TestViewsTest.php
+++ b/tests/Testing/Concerns/TestViewsTest.php
@@ -68,7 +68,7 @@ public function testCompiledViewPathTrimsTrailingSlash(): void
{
Container::getInstance()->make(ParallelTesting::class)->resolveTokenUsing(fn () => '3');
- Container::getInstance()['config']->set('view.compiled', '/path/to/compiled/views/');
+ Container::getInstance()->make('config')->set('view.compiled', '/path/to/compiled/views/');
$this->assertSame('/path/to/compiled/views/test_3', $this->getCompiledViewPath());
}
@@ -77,7 +77,7 @@ public function testCompiledViewPathWithDifferentToken(): void
{
Container::getInstance()->make(ParallelTesting::class)->resolveTokenUsing(fn () => '42');
- Container::getInstance()['config']->set('view.compiled', '/var/www/storage/views');
+ Container::getInstance()->make('config')->set('view.compiled', '/var/www/storage/views');
$this->assertSame('/var/www/storage/views/test_42', $this->getCompiledViewPath());
}
@@ -86,11 +86,11 @@ public function testCompiledViewPathDoesNotReuseCustomPathFromPreviousCall(): vo
{
Container::getInstance()->make(ParallelTesting::class)->resolveTokenUsing(fn () => '1');
- Container::getInstance()['config']->set('view.compiled', '/custom/views');
+ Container::getInstance()->make('config')->set('view.compiled', '/custom/views');
$this->assertSame('/custom/views/test_1', $this->getCompiledViewPath());
- Container::getInstance()['config']->set('view.compiled', '/path/to/compiled/views');
+ Container::getInstance()->make('config')->set('view.compiled', '/path/to/compiled/views');
$this->assertSame('/path/to/compiled/views/test_1', $this->getCompiledViewPath());
}
@@ -98,14 +98,14 @@ public function testCompiledViewPathDoesNotReuseCustomPathFromPreviousCall(): vo
public function testCompiledViewPathDoesNotDoubleAppendToken(): void
{
Container::getInstance()->make(ParallelTesting::class)->resolveTokenUsing(fn () => '1');
- Container::getInstance()['config']->set('view.compiled', '/path/to/compiled/views/test_1');
+ Container::getInstance()->make('config')->set('view.compiled', '/path/to/compiled/views/test_1');
$this->assertSame('/path/to/compiled/views/test_1', $this->getCompiledViewPath());
}
public function testCompiledViewPathReturnsNullWhenEmpty(): void
{
- Container::getInstance()['config']->set('view.compiled', '');
+ Container::getInstance()->make('config')->set('view.compiled', '');
$this->assertNull($this->getCompiledViewPath());
}
@@ -114,7 +114,7 @@ public function testSwitchToCompiledViewPathUpdatesConfig(): void
{
$this->switchToCompiledViewPath('/new/compiled/path');
- $this->assertSame('/new/compiled/path', Container::getInstance()['config']->get('view.compiled'));
+ $this->assertSame('/new/compiled/path', Container::getInstance()->make('config')->get('view.compiled'));
}
public function testSwitchToCompiledViewPathUpdatesCompilerCachePath(): void
@@ -126,7 +126,7 @@ public function testSwitchToCompiledViewPathUpdatesCompilerCachePath(): void
$this->switchToCompiledViewPath('/new/compiled/path');
- $this->assertSame('/new/compiled/path', $container['config']->get('view.compiled'));
+ $this->assertSame('/new/compiled/path', $container->make('config')->get('view.compiled'));
$this->assertSame('/new/compiled/path', (new ReflectionProperty($compiler, 'cachePath'))->getValue($compiler));
}
@@ -167,7 +167,7 @@ protected function makeTestViewsInstance(): object
return new class {
use TestViews;
- public $app;
+ public Container $app;
public function __construct()
{
diff --git a/tests/Testing/TestWithoutDatabaseParallelTest.php b/tests/Testing/TestWithoutDatabaseParallelTest.php
index 6aa67e546..4dc201e55 100644
--- a/tests/Testing/TestWithoutDatabaseParallelTest.php
+++ b/tests/Testing/TestWithoutDatabaseParallelTest.php
@@ -18,7 +18,7 @@ protected function getPackageProviders(ApplicationContract $app): array
protected function defineEnvironment(ApplicationContract $app): void
{
- $app['config']->set('database.default', null);
+ $app->make('config')->set('database.default', null);
$serverKeys = [
'HYPERVEL_PARALLEL_TESTING',
@@ -47,6 +47,7 @@ protected function defineEnvironment(ApplicationContract $app): void
public function testRunningParallelTestWithoutDatabaseShouldNotCrashOnDefaultConnection(): void
{
ParallelTesting::callSetUpProcessCallbacks();
- $this->assertTrue(true);
+
+ $this->assertNull(config('database.default'));
}
}
diff --git a/tests/Translation/CoroutineIsolationTest.php b/tests/Translation/CoroutineIsolationTest.php
index 50a596382..7f347da3e 100644
--- a/tests/Translation/CoroutineIsolationTest.php
+++ b/tests/Translation/CoroutineIsolationTest.php
@@ -40,6 +40,7 @@ function () use ($translator): string {
public function testLocaleMutationIsIsolatedBetweenConcurrentCoroutines(): void
{
$translator = new Translator(new ArrayLoader, 'en');
+ $translator->setBaseLocale('es');
[$firstLocale, $secondLocale] = parallel([
function () use ($translator): string {
@@ -58,7 +59,7 @@ function () use ($translator): string {
$this->assertSame('fr', $firstLocale);
$this->assertSame('de', $secondLocale);
- $this->assertSame('en', $translator->getLocale());
+ $this->assertSame('es', $translator->getLocale());
}
}
diff --git a/tests/Translation/TranslationServiceProviderTest.php b/tests/Translation/TranslationServiceProviderTest.php
new file mode 100644
index 000000000..9bf102051
--- /dev/null
+++ b/tests/Translation/TranslationServiceProviderTest.php
@@ -0,0 +1,120 @@
+ [
+ 'locale' => 'en',
+ 'fallback_locale' => 'fr',
+ ],
+ ]);
+ $loader = (new ArrayLoader)->addMessages('en', 'messages', [
+ 'file' => 'before refresh',
+ ]);
+ $application->instance('config', $config);
+ $provider = new TranslationServiceProvider($application);
+ $provider->register();
+ $application->instance('translation.loader', $loader);
+
+ $translator = $application->make('translator');
+ $selector = new MessageSelector;
+ $translator->setSelector($selector);
+ $translator->handleMissingKeysUsing(static fn (string $key): string => "missing:{$key}");
+ $translator->stringable(
+ CarbonImmutable::class,
+ static fn (CarbonImmutable $value): string => $value->format('Y')
+ );
+ $translator->addLines([
+ 'messages.registered' => 'registered',
+ 'messages.year' => 'Year :year',
+ ], 'en');
+
+ $this->assertSame('before refresh', $translator->get('messages.file'));
+
+ $loader->addMessages('en', 'messages', [
+ 'file' => 'after refresh',
+ ]);
+ $config->set([
+ 'app.locale' => 'es',
+ 'app.fallback_locale' => 'it',
+ ]);
+ $translator->setLocale('en');
+
+ $provider->reloadConfiguration();
+
+ $this->assertSame($translator, $application->make(Translator::class));
+ $this->assertSame($loader, $translator->getLoader());
+ $this->assertSame($selector, $translator->getSelector());
+ $this->assertSame('en', $translator->getLocale());
+ $this->assertSame('it', $translator->getFallback());
+ $this->assertSame('after refresh', $translator->get('messages.file'));
+ $this->assertSame('registered', $translator->get('messages.registered'));
+ $this->assertSame(
+ 'Year 2026',
+ $translator->get('messages.year', ['year' => CarbonImmutable::create(2026)])
+ );
+ $this->assertSame('missing:messages.missing', $translator->get('messages.missing', [], 'en', false));
+
+ [$baseLocale] = parallel([
+ static fn (): string => $translator->getLocale(),
+ ]);
+
+ $this->assertSame('es', $baseLocale);
+ }
+
+ public function testReloadConfigurationDoesNotResolveAnUnusedTranslator(): void
+ {
+ $application = new Application;
+ $application->instance('config', new Repository([
+ 'app' => [
+ 'locale' => 'en',
+ 'fallback_locale' => 'fr',
+ ],
+ ]));
+ $provider = new TranslationServiceProvider($application);
+ $provider->register();
+
+ $provider->reloadConfiguration();
+
+ $this->assertFalse($application->resolved('translator'));
+ }
+
+ public function testReloadConfigurationLeavesApplicationTranslatorReplacementAlone(): void
+ {
+ $application = new Application;
+ $application->instance('config', new Repository([
+ 'app' => [
+ 'locale' => 'en',
+ 'fallback_locale' => 'fr',
+ ],
+ ]));
+ $provider = new TranslationServiceProvider($application);
+ $provider->register();
+ $replacement = m::mock(TranslatorContract::class);
+ $application->instance('translator', $replacement);
+
+ $provider->reloadConfiguration();
+
+ $this->assertSame($replacement, $application->make('translator'));
+ }
+}
diff --git a/tests/Translation/TranslationTranslatorTest.php b/tests/Translation/TranslationTranslatorTest.php
index e4ef96381..08e7f941c 100644
--- a/tests/Translation/TranslationTranslatorTest.php
+++ b/tests/Translation/TranslationTranslatorTest.php
@@ -186,6 +186,20 @@ public function testFallbackLocaleCanBeReadAndChanged(): void
$this->assertSame('lv', $translator->getFallback());
}
+ public function testBaseLocaleCanBeChangedWithoutReplacingCurrentRequestOverride(): void
+ {
+ $translator = new Translator($this->getLoader(), 'en');
+
+ $translator->setBaseLocale('fr');
+
+ $this->assertSame('fr', $translator->getLocale());
+
+ $translator->setLocale('de');
+ $translator->setBaseLocale('es');
+
+ $this->assertSame('de', $translator->getLocale());
+ }
+
public function testGetDoesNotCallGetLineTwiceForMissingKeyWhenLocaleMatchesFallback(): void
{
$translator = $this->getMockBuilder(Translator::class)->onlyMethods(['getLine'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock();
@@ -205,6 +219,88 @@ public function testGetMethodProperlyLoadsAndRetrievesItemForGlobalNamespace():
$this->assertSame('breeze bar', $translator->get('foo.bar', ['foo' => 'bar']));
}
+ public function testLinesAddedBeforeLoadingPreserveAndOverrideLoaderLines(): void
+ {
+ $loader = (new ArrayLoader)->addMessages('en', 'messages', [
+ 'file' => 'from file',
+ 'override' => 'from file',
+ ]);
+ $translator = new Translator($loader, 'en');
+
+ $translator->addLines([
+ 'messages.added' => 'registered',
+ 'messages.override' => 'registered',
+ ], 'en');
+
+ $this->assertSame('from file', $translator->get('messages.file'));
+ $this->assertSame('registered', $translator->get('messages.added'));
+ $this->assertSame('registered', $translator->get('messages.override'));
+ }
+
+ public function testRegisteredLinesReplayInCallOrder(): void
+ {
+ $translator = new Translator(new ArrayLoader, 'en');
+
+ $translator->addLines(['messages.parent.child' => 'first'], 'en');
+ $translator->addLines(['messages.parent' => 'replacement'], 'en');
+ $translator->addLines(['messages.parent.child' => 'last'], 'en');
+ $translator->forgetLoadedGroups();
+
+ $this->assertSame(['child' => 'last'], $translator->array('messages.parent'));
+ }
+
+ public function testNamespacedRegisteredLinesSurviveLoadedGroupRefresh(): void
+ {
+ $loader = (new ArrayLoader)->addMessages('en', 'messages', [
+ 'file' => 'before refresh',
+ 'override' => 'before refresh',
+ ], 'package');
+ $translator = new Translator($loader, 'en');
+
+ $translator->addLines([
+ 'messages.registered' => 'registered',
+ 'messages.override' => 'registered',
+ ], 'en', 'package');
+
+ $this->assertSame('before refresh', $translator->get('package::messages.file'));
+
+ $loader->addMessages('en', 'messages', [
+ 'file' => 'after refresh',
+ 'override' => 'after refresh',
+ ], 'package');
+ $translator->forgetLoadedGroups();
+
+ $this->assertSame('after refresh', $translator->get('package::messages.file'));
+ $this->assertSame('registered', $translator->get('package::messages.registered'));
+ $this->assertSame('registered', $translator->get('package::messages.override'));
+ }
+
+ public function testJsonRegisteredLinesSurviveLoadedGroupRefresh(): void
+ {
+ $loader = (new ArrayLoader)->addMessages('en', '*', [
+ 'Message' => 'before refresh',
+ 'Override' => 'before refresh',
+ ]);
+ $translator = new Translator($loader, 'en');
+
+ $translator->addLines([
+ '*.Registered' => 'registered',
+ '*.Override' => 'registered',
+ ], 'en');
+
+ $this->assertSame('before refresh', $translator->get('Message'));
+
+ $loader->addMessages('en', '*', [
+ 'Message' => 'after refresh',
+ 'Override' => 'after refresh',
+ ]);
+ $translator->forgetLoadedGroups();
+
+ $this->assertSame('after refresh', $translator->get('Message'));
+ $this->assertSame('registered', $translator->get('Registered'));
+ $this->assertSame('registered', $translator->get('Override'));
+ }
+
public function testChoiceMethodProperlyLoadsAndRetrievesItemForAnInt(): void
{
$translator = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock();
@@ -620,15 +716,13 @@ public function testExplicitInvalidLocaleIsRejectedBeforeFilesystemAccess(): voi
public function testInvalidFallbackLocaleIsRejectedBeforeItsFilesystemAccess(): void
{
$files = m::mock(Filesystem::class);
- $files->shouldReceive('exists')->once()->with(__DIR__ . '/en.json')->andReturn(false);
- $files->shouldReceive('exists')->once()->with(__DIR__ . '/en/messages.php')->andReturn(false);
+ $files->shouldReceive('exists')->never();
$translator = new Translator(new FileLoader($files, __DIR__), 'en');
- $translator->setFallback('../fr');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid characters present in locale.');
- $translator->get('messages.welcome');
+ $translator->setFallback('../fr');
}
public function testInvalidLocaleFromResolverIsRejectedBeforeItsFilesystemAccess(): void
@@ -644,16 +738,20 @@ public function testInvalidLocaleFromResolverIsRejectedBeforeItsFilesystemAccess
$translator->get('messages.welcome');
}
- public function testSetLocaleRejectsInvalidLocaleImmediately(): void
+ public function testLocaleSettersRejectInvalidLocaleImmediately(): void
{
$loader = m::mock(Loader::class);
$loader->shouldReceive('load')->never();
$translator = new Translator($loader, 'en');
- $this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('Invalid characters present in locale.');
-
- $translator->setLocale('..');
+ foreach (['setLocale', 'setBaseLocale'] as $method) {
+ try {
+ $translator->{$method}('..');
+ $this->fail("Expected {$method} to reject the invalid locale.");
+ } catch (InvalidArgumentException $exception) {
+ $this->assertSame('Invalid characters present in locale.', $exception->getMessage());
+ }
+ }
}
public function testMissingKeyCallbackDoesNotRecurse(): void
diff --git a/tests/View/ViewBladeCompilerTest.php b/tests/View/ViewBladeCompilerTest.php
index 130d022e8..1ee01272d 100644
--- a/tests/View/ViewBladeCompilerTest.php
+++ b/tests/View/ViewBladeCompilerTest.php
@@ -35,6 +35,41 @@ public function testCannotConstructWithBadCachePath(): void
new BladeCompiler($this->getFiles(), '');
}
+ public function testCompilerConfigurationCanBeReloaded(): void
+ {
+ $files = $this->getFiles();
+ $compiler = new BladeCompiler($files, '/old-cache');
+
+ $compiler->reloadConfiguration(
+ '/new-cache',
+ '/application',
+ false,
+ 'compiled',
+ false,
+ );
+
+ $this->assertSame(
+ '/new-cache/' . hash('xxh128', 'v3/views/home.blade.php') . '.compiled',
+ $compiler->getCompiledPath('/application/views/home.blade.php'),
+ );
+ $this->assertTrue($compiler->isExpired('/application/views/home.blade.php'));
+ }
+
+ public function testInvalidReloadKeepsThePreviousCompilerConfiguration(): void
+ {
+ $compiler = new BladeCompiler($this->getFiles(), '/old-cache');
+ $compiledPath = $compiler->getCompiledPath('home.blade.php');
+
+ try {
+ $compiler->reloadConfiguration('', '/application', false, 'compiled', false);
+ $this->fail('Expected the invalid cache path to be rejected.');
+ } catch (InvalidArgumentException $exception) {
+ $this->assertSame('Please provide a valid cache path.', $exception->getMessage());
+ }
+
+ $this->assertSame($compiledPath, $compiler->getCompiledPath('home.blade.php'));
+ }
+
public function testIsExpiredReturnsTrueWhenModificationTimesWarrant(): void
{
$compiler = new BladeCompiler($files = $this->getFiles(), __DIR__);
diff --git a/tests/View/ViewServiceProviderTest.php b/tests/View/ViewServiceProviderTest.php
index 9961240b9..2aa587c9b 100644
--- a/tests/View/ViewServiceProviderTest.php
+++ b/tests/View/ViewServiceProviderTest.php
@@ -4,12 +4,44 @@
namespace Hypervel\Tests\View;
+use Hypervel\Config\Repository;
+use Hypervel\Events\Dispatcher;
+use Hypervel\Filesystem\Filesystem;
+use Hypervel\Foundation\Application;
+use Hypervel\Testing\ParallelTesting;
use Hypervel\Tests\TestCase;
+use Hypervel\View\Compilers\BladeCompiler;
+use Hypervel\View\DynamicComponent;
+use Hypervel\View\Engines\CompilerEngine;
+use Hypervel\View\Engines\EngineResolver;
+use Hypervel\View\Factory;
+use Hypervel\View\FileViewFinder;
use Hypervel\View\ViewServiceProvider;
use ReflectionMethod;
class ViewServiceProviderTest extends TestCase
{
+ protected string $tempDirectory;
+
+ protected Filesystem $filesystem;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->filesystem = new Filesystem;
+ $this->tempDirectory = ParallelTesting::tempDir('ViewServiceProviderTest');
+ $this->filesystem->deleteDirectory($this->tempDirectory);
+ $this->filesystem->ensureDirectoryExists($this->tempDirectory);
+ }
+
+ protected function tearDown(): void
+ {
+ $this->filesystem->deleteDirectory($this->tempDirectory);
+
+ parent::tearDown();
+ }
+
public function testEngineAndViewRegistrationMethodsArePublic(): void
{
foreach ([
@@ -26,4 +58,105 @@ public function testEngineAndViewRegistrationMethodsArePublic(): void
$this->assertTrue((new ReflectionMethod(ViewServiceProvider::class, 'createFactory'))->isProtected());
}
+
+ public function testReloadConfigurationUpdatesRetainedViewServices(): void
+ {
+ $oldViewPath = $this->tempDirectory . '/old-views';
+ $newViewPath = $this->tempDirectory . '/new-views';
+ $compiledPath = $this->tempDirectory . '/compiled';
+ $this->filesystem->ensureDirectoryExists($oldViewPath);
+ $this->filesystem->ensureDirectoryExists($newViewPath);
+ $this->filesystem->ensureDirectoryExists($compiledPath);
+ $this->filesystem->put($oldViewPath . '/page.blade.php', 'old page');
+ $this->filesystem->put($newViewPath . '/page.blade.php', 'new page');
+ $renderPath = $oldViewPath . '/render.blade.php';
+ $this->filesystem->put($renderPath, 'before refresh');
+
+ $application = new Application($this->tempDirectory);
+ $config = new Repository([
+ 'view' => [
+ 'paths' => [$oldViewPath],
+ 'compiled' => $compiledPath,
+ 'relative_hash' => false,
+ 'cache' => true,
+ 'compiled_extension' => 'php',
+ 'check_cache_timestamps' => false,
+ ],
+ ]);
+ $application->instance('config', $config);
+ $application->instance('events', new Dispatcher($application));
+ $application->instance('files', $this->filesystem);
+ $provider = new ViewServiceProvider($application);
+ $provider->register();
+
+ $factory = $application->make('view');
+ $finder = $factory->getFinder();
+ $resolver = $factory->getEngineResolver();
+ $compiler = $application->make('blade.compiler');
+ $engine = $resolver->resolve('blade');
+ $this->assertInstanceOf(FileViewFinder::class, $finder);
+ $this->assertInstanceOf(EngineResolver::class, $resolver);
+ $this->assertInstanceOf(BladeCompiler::class, $compiler);
+ $this->assertInstanceOf(CompilerEngine::class, $engine);
+ $directive = static fn (): string => 'persisted directive';
+ $finder->addNamespace('package', $oldViewPath);
+ $finder->addExtension('md');
+ $compiler->directive('persisted_directive', $directive);
+ $compiler->component('persisted-component', DynamicComponent::class);
+ $compiler->if('persisted_condition', static fn (): bool => true);
+ $compiler->precompiler(static fn (string $value): string => str_replace('PRECOMPILE', 'precompiled', $value));
+
+ $this->assertSame($oldViewPath . '/page.blade.php', $finder->find('page'));
+ $compiler->compile($renderPath);
+ $this->assertSame('before refresh', $engine->get($renderPath));
+
+ $this->filesystem->put($renderPath, 'after refresh');
+ $config->set([
+ 'view.paths' => [$newViewPath],
+ 'view.cache' => false,
+ ]);
+
+ $provider->reloadConfiguration();
+
+ $this->assertSame($factory, $application->make(Factory::class));
+ $this->assertSame($finder, $factory->getFinder());
+ $this->assertSame($resolver, $application->make('view.engine.resolver'));
+ $this->assertSame($compiler, $application->make(BladeCompiler::class));
+ $this->assertSame($engine, $resolver->resolve('blade'));
+ $this->assertSame([$newViewPath], $finder->getPaths());
+ $this->assertSame($newViewPath . '/page.blade.php', $finder->find('page'));
+ $this->assertSame([$oldViewPath], $finder->getHints()['package']);
+ $this->assertContains('md', $finder->getExtensions());
+ $this->assertSame($directive, $compiler->getCustomDirectives()['persisted_directive']);
+ $this->assertSame(DynamicComponent::class, $compiler->getClassComponentAliases()['persisted-component']);
+ $this->assertTrue($compiler->check('persisted_condition'));
+ $this->assertSame('precompiled', $compiler->compileString('PRECOMPILE'));
+ $this->assertSame('after refresh', $engine->get($renderPath));
+ }
+
+ public function testReloadConfigurationDoesNotResolveUnusedViewServices(): void
+ {
+ $application = new Application($this->tempDirectory);
+ $application->instance('config', new Repository([
+ 'view' => [
+ 'paths' => [$this->tempDirectory],
+ 'compiled' => $this->tempDirectory,
+ 'relative_hash' => false,
+ 'cache' => true,
+ 'compiled_extension' => 'php',
+ 'check_cache_timestamps' => true,
+ ],
+ ]));
+ $application->instance('events', new Dispatcher($application));
+ $application->instance('files', $this->filesystem);
+ $provider = new ViewServiceProvider($application);
+ $provider->register();
+
+ $provider->reloadConfiguration();
+
+ $this->assertFalse($application->resolved('view'));
+ $this->assertFalse($application->resolved('view.finder'));
+ $this->assertFalse($application->resolved('view.engine.resolver'));
+ $this->assertFalse($application->resolved('blade.compiler'));
+ }
}