Skip to content

v2.0.0-alpha.19

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 19 Apr 01:22
· 29 commits to main since this release

Version v2.0.0-alpha.19 (2026-04-18)

Hardening release that resolves a 15-Blocker audit covering JWT validation, CORS, rate limiting, ORM portability, RFC 7807 error semantics, CLI safety, and documentation/skill drift. 253 tests / 548 assertions pass on PHP 8.4 across SQLite, MySQL, and Postgres targets.

πŸ›‘οΈ Security

  • JWT claim validation is now unconditional. Auth::decode() rejects tokens missing any of iss, aud, sub, jti, or exp. Previously, missing env vars silently disabled the corresponding claim check β€” tokens without identity claims could be accepted.
  • CORS wildcard + credentials is a fatal misconfiguration. CorsMiddleware throws InvalidArgumentException at construction when CORS_ALLOWED_ORIGINS=* is combined with CORS_ALLOW_CREDENTIALS=true (W3C spec forbids this combination). New env var CORS_ALLOW_CREDENTIALS (default false).
  • Rate-limit IP spoofing closed. X-Forwarded-For is honored only when REMOTE_ADDR is in RATE_LIMIT_TRUSTED_PROXIES (comma-separated, default empty). Without a trusted-proxy list, the raw REMOTE_ADDR is used, so clients can no longer set their own rate-limit key.
  • SQL-injection via identifier parameters is eliminated. ?sort= and ?fields= are filtered through QueryBuilder::getColumnAllowlist() before reaching SQL. ?sort=id;DROP TABLE users-- is normalized to ORDER BY id ASC.
  • php api serve is shell-free. ServeCommand now spawns PHP via proc_open with an argv array; host/port/router arguments are never shell-expanded.
  • Swagger UI / ReDoc HTML is XSS-safe. DocsController escapes spec metadata before interpolating into the rendered HTML shell.

πŸŽ‰ New Features

  • ValidationException (Coagus\PhpApiBuilder\Exceptions\ValidationException) β€” final exception thrown by Entity::save(), exposes public readonly array $errors keyed by field name. ErrorHandler maps it to a 422 RFC 7807 response with errors embedded.

    try {
        $product->save();
    } catch (ValidationException $e) {
        return $this->error($e->getMessage(), 422, ['errors' => $e->errors]);
    }
  • EntityNotFoundException β€” canonical way to produce a 404. ErrorHandler dispatches on class, not on message substring.

    $user = User::find($id);
    if ($user === null) {
        throw new EntityNotFoundException("User {$id} not found");
    }
  • Driver session hooks β€” DriverInterface gains getCurrentTimestampExpression(), applySessionSettings(\PDO), and getRefreshTokenTableDdl(). Applied automatically on connect:

    • SQLite: PRAGMA foreign_keys=ON, PRAGMA journal_mode=WAL, PRAGMA busy_timeout=5000.
    • MySQL: SET NAMES utf8mb4, SET time_zone='+00:00'.
    • Postgres: SET TIME ZONE 'UTC', SET client_encoding='UTF8'.
  • QueryBuilder::getColumnAllowlist(): list<string> β€” public API consumed by APIDB to validate identifier inputs. Overridable per entity.

  • docs:generate --namespace=App β€” override the root scanned namespace when emitting the OpenAPI spec.

πŸ”§ Improvements

  • RFC 7807 everywhere for errors. All error responses emit Content-Type: application/problem+json; charset=utf-8 with {type, title, status, detail, requestId}. Success responses keep application/json.
  • docs:generate registers entities AND services. Services were previously skipped silently.
  • env:check reports the correct minimum PHP 8.4, matching composer.json.
  • Portable soft-delete. Entity::delete() soft-delete uses the driver's getCurrentTimestampExpression() instead of hardcoded NOW(), working across MySQL, Postgres, and SQLite.
  • orWhere() with soft-delete is parenthesised. The deleted_at IS NULL predicate can no longer be bypassed by adding an orWhere clause.
  • Arch test added (tests/Arch/NoSqliteOnlySqlTest.php) guarding against SQLite-only SQL creeping back into driver-agnostic code.

πŸ› Bug Fixes

  • Entity::delete() soft-delete no longer fails on SQLite/Postgres (NOW() was MySQL-only).
  • QueryBuilder::orWhere() emits well-formed SQL when used as the first clause in a chain.
  • ErrorHandler no longer leaks raw exception messages in production for unmapped \Throwables β€” returns a generic "Internal Server Error" while logging the original.
  • Stale tests/Integration/APIDB/DeleteTest.php updated to account for PRAGMA foreign_keys=ON on SQLite.

πŸ’₯ Breaking Changes

  • Entity::save() throws ValidationException (not \RuntimeException). Catchers of \RuntimeException for validation must migrate.
  • ErrorHandler dispatches on exception class, not message content. Throw EntityNotFoundException for 404; a \RuntimeException('... not found') now yields 500.
  • CORS_ALLOWED_ORIGINS=* + CORS_ALLOW_CREDENTIALS=true now throws InvalidArgumentException at boot.
  • Custom DriverInterface implementations must add the three new methods.
  • Error Content-Type is now application/problem+json; charset=utf-8. Clients that switch on Content-Type must accept both JSON variants.

πŸ“ Technical Details

  • 15 atomic commits (Conventional Commits), one concern per commit, identity Christian Agustin <christian@agustin.gt>.
  • New unit tests under tests/Unit/{CLI,Exceptions,Http,OpenAPI}; new arch test under tests/Arch/; updated integration tests for APIDB, middleware, ORM, and auth.
  • Library-shipped skill bumped to v2.0.0 with full Migration Guide (resources/skill/php-api-builder/CHANGELOG.md).
  • Agent specs and specialization skills added under .claude/agents/ and .claude/skills/ driving the develop β†’ validate β†’ document β†’ release pipeline (Dockerized PHP runtime via pab-dev container).

πŸ”„ Migration Notes

  1. Replace catch (\RuntimeException) around Entity::save() with catch (ValidationException) and read $e->errors.
  2. Throw EntityNotFoundException to produce 404s; remove reliance on "not found" message matching.
  3. If you run CORS_ALLOWED_ORIGINS=* with credentials enabled, pick one of:
    • Explicit origins + CORS_ALLOW_CREDENTIALS=true, or
    • Wildcard origin + CORS_ALLOW_CREDENTIALS=false.
  4. Custom drivers: implement getCurrentTimestampExpression(), applySessionSettings(\PDO), getRefreshTokenTableDdl().
  5. Client code switching on Content-Type must accept application/problem+json.
  6. Behind a load balancer: set RATE_LIMIT_TRUSTED_PROXIES=10.0.0.1,10.0.0.2 (comma-separated).
  7. Scripts/CI referencing phantom commands (php api test, php api skill:install) β€” remove them; use vendor/bin/pest directly.

πŸ“¦ Installation

composer require coagus/php-api-builder:v2.0.0-alpha.19
docker pull coagus/php-api-builder:v2.0.0-alpha.19

Full Changelog: v2.0.0-alpha.18...v2.0.0-alpha.19