Replaces \$utopia->match(\$request) calls in the project, team, and auth
resolvers with the per-request RouteMatch injected directly via DI:
- app/init/resources/request.php — project / team resource closures now
declare 'match' as a dependency and read \$match?->route.
- app/controllers/shared/api/auth.php — auth init hook injects 'match'
instead of resolving via \$utopia.
- app/controllers/shared/api.php — drop the redundant re-match in the
storage cache init hook; \$route from the action's \$match inject is
already in scope.
Only Resolvers::resolve still calls \$utopia->match(...) — that path
genuinely matches a synthesized sub-request URL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps utopia-php/http to the latest fix/concurrency-shared-state, which
collapses the separate route / matchedPath / arguments context keys into
a single immutable RouteMatch under the 'match' key.
- Replace ->inject('route') / ->inject('matchedPath') with
->inject('match'), reading \$match->route / \$match->path.
- Replace the manual array_merge(\$route->getPathValues(), \$request->
getParams()) workaround in api.php's shutdown hook with the framework-
provided \$match->arguments — same data the action saw, no path-value
reconstruction needed.
- Update GraphQL resolver to snapshot/restore the 'match' value instead
of 'route'.
- Update top-level call sites in app/http.php that read from getResource
('route') to read getResource('match')->route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The api shutdown hook substitutes route labels like
\`cache.resource = 'file/{request.fileId}'\` against \$requestParams. We
were reading those from \$request->getParams(), which only returns body
or query params — path params (\`fileId\`, \`bucketId\`) were missing,
so cache documents got written with literal \`file/{request.fileId}\`
strings and subsequent cache hits 404'd because the placeholder was
treated as a real fileId.
Merge \$route->getPathValues(\$request) ahead of \$request->getParams() so
path params are available for substitution. Replicates the previous
behaviour of \$route->getParamsValues(), which was populated by the
upstream Hook param writeback that has since been removed.
Also rename the per-request container variable to \$context and the
loader callable to \$registerContext in app/http.php to match the new
upstream terminology.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Use ->inject('route') and ->inject('matchedPath') in actions instead of
reading via \$utopia->getResource() — this was the cause of the e2e 500s,
the bus resolver was hitting the global container after the upstream
Adapter::getContainer() semantics changed.
- Switch the bus resolver in app/http.php to use \$swooleAdapter->getContext()
so per-request resources (locale, platform, dbForProject) resolve from the
per-coroutine context container.
- Drop the dead ?->label('router', true) calls in general.php — the label was
never read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopts the breaking changes from utopia-php/http#251 (concurrency races on
shared Http/Route singletons):
- Replace `Http::getRoute()` / `setRoute()` with `getResource('route')` and
context container writes.
- Replace `Route::getMatchedPath()` with `getResource('matchedPath')`.
- Use `Adapter::getContext()` for the per-request container in `app/http.php`
(`getContainer()` now always returns the global singleton).
- Read request params from `$request->getParams()` in the api shutdown hook
instead of `Route::getParamsValues()`, which is no longer populated.
- Update Swoole promise context key to `__utopia_http_context`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per review feedback on the PHPStan cleanup, the two `if
($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE)`
blocks in `app/controllers/general.php` and
`src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php`
were load-bearing feature flags, not dead code. Removing them silently
dropped the ability to turn the cleanup on later.
Changes:
- Convert `ENABLE_EXECUTIONS_LIMIT_ON_ROUTE` from
`const ... = false;` to a `define()` backed by the new
`_APP_EXECUTIONS_LIMIT_ON_ROUTE` env var (defaults to `disabled`).
PHPStan can no longer fold the `&&` away since the value is now
runtime-resolved, so the guarded blocks are live again.
- Restore the `/* cleanup */` block in the `router()` helper in
`app/controllers/general.php`.
- Restore the two cleanup blocks in `Functions/Http/Executions/Create.php`
(one on the async-scheduled return path, one on the sync-response
path), and re-add the `DeleteEvent $queueForDeletes` /
`int $executionsRetentionCount` injections plus the
`Appwrite\Event\Delete` import.
Runtime behavior is identical to main (flag off by default); operators
can now flip it via env without a code change.
Three follow-ups from CI that the level-4 pass got wrong:
1. `account.php` / `users.php`: `Document::find()` returns `mixed`
(specifically `Document|false` in practice), not `Document`. The
earlier `@var Document $oldTarget` docblocks were lies, and the
runtime `instanceof Document` guards were load-bearing — removing
them caused `Call to a member function isEmpty() on false` 500s
on the `PATCH /v1/users/:id/email` and `/phone` endpoints (and the
analogous `/v1/account/email`, `/v1/account/phone` flows). Dropped
the misleading `@var` docblocks and restored
`$oldTarget instanceof Document && !$oldTarget->isEmpty()`.
2. `Installer/Runtime/Config::setEnabledDatabases()` is a boundary
that actually takes arbitrary user/compose input — not a trusted
`string[]`. The `is_string($v)` filter was covering for that, and
`ConfigTest::testSetEnabledDatabasesFiltersInvalid` explicitly
asserts it. Widened the PHPDoc to `array<mixed>` and restored
`is_string($v) && $v !== ''` in the filter.
3. `OAuth2/Apple::getAppSecret()` wrapped `json_decode` in a
`try/catch (\Throwable)` — but `json_decode` without
`JSON_THROW_ON_ERROR` returns `null` on failure, it doesn't throw.
PHP 8.3's PHPStan flagged the catch as dead (PHP 8.5 didn't, which
is why it slipped through locally). Replaced with
`if (!\is_array($secret)) throw`, which preserves the original
"invalid secret" guard.
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:
- `app/controllers/general.php`: path-traversal guard was negating
`\substr(...)` before the strict comparison (`!\substr(...) === $base`
was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
`DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
result as an array with `deviceName/deviceBrand/deviceModel` keys.
Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
key was checking `$resourceKey === 'functions'` when `$resourceKey`
is `'functionId'|'siteId'` — always false. Switched to the intended
`$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
to `string|false` to match `openssl_encrypt`; this lets callers'
`=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
`array_key_exists('from', [])` branch in the Msg91 provider (empty
array literal; branch was unreachable).
Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
`markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
Installer State, `$source` on MigrationsWorker, `$account2` on two
GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
`assertNotNull` assertions with `addToAssertionCount(1)` or
`assertNotEmpty` where the runtime type was already known.
Cache write hook now checks HTTP status code before writing to prevent
failed AVIF (or any other) conversions from poisoning the cache.
Bumps utopia-php/image to 0.8.5 which fixes AVIF/HEIC output by using
native Imagick instead of the deprecated magick convert shell command.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>