Skip to content

Release 3.0.0 - #38

Merged
AureDulvresse merged 3 commits into
mainfrom
develop
Oct 7, 2026
Merged

AureDulvresse merged 3 commits into
mainfrom
develop

Conversation

@AureDulvresse

Copy link
Copy Markdown
Collaborator

[3.0.0] - 2026-10-06

Changed (breaking)

  • Http\Response now composes a Symfony\Component\HttpFoundation\Response instead of extending it —
    specifically so Http\RedirectResponse and Http\JsonResponse can be genuine subclasses of Response
    instead of independent siblings of it (each previously extended its own Symfony ancestor directly:
    RedirectResponse extends Symfony\...\RedirectResponse, Response extends Symfony\...\Response — two
    unrelated Symfony leaf classes, so RedirectResponse was never an instanceof Response even though both are
    "a Marrow response" in every way that matters to application code). A controller method that can return
    either a redirect or a rendered view now simply declares : Response — no more typing against
    Symfony\Component\HttpFoundation\Response as a workaround (see marrow/warden's controllers, simplified
    back in the same release this motivated).
    • Every Symfony Response method not explicitly redeclared (setContent(), getStatusCode(), isRedirect(),
      cache/cookie/vary helpers, ...) still works, forwarded via __call() — @mixin on the class gives
      IDEs/PHPStan full autocomplete/type-checking for those. $response->headers is a real public property (the
      same ResponseHeaderBag the wrapped instance reads when sending), not magic. toSymfonyResponse() is the
      escape hatch for anything that specifically needs the real Symfony instance.
    • Breaking for any app/package with its own middleware or controller typed against
      Symfony\Component\HttpFoundation\Response
      (the previous, now-unnecessary workaround) — a Marrow\Http\Response
      instance is no longer an instanceof Symfony's Response, so PHP's own return-type enforcement will throw a
      TypeError the first time such code returns one. Fix: change the type to Marrow\Http\Response. Every
      framework-internal consumer (Router, Pipeline, Kernel, SessionManager, all bundled middleware) already made
      this exact change — see docs/http.md.
    • Also fixed one latent instance of the same root cause along the way: ApiController's JSON helpers
      (ok(), created(), notFound(), ...) constructed Symfony\Component\HttpFoundation\JsonResponse
      directly instead of Marrow\Http\JsonResponse — harmless on its own, but would have broken the moment one
      of those methods' return value reached Router::toResponse()'s now-Marrow\Http\Response-typed
      instanceof check.

[2.5.0] - 2026-10-06

Added

  • framework_version() (Support\helpers.php) — the installed marrow/framework version (e.g. 2.4.1),
    resolved from Composer's own runtime metadata (Composer\InstalledVersions), never hand-maintained. Distinct
    from app()->version() (config('app.version')), which is the application's own version.

Fixed

  • The php forge CLI banner always printed "Marrow 0.1.0", regardless of which marrow/framework version
    was actually installed — Console\Kernel was constructed with config('app.version', '0.1.0'), the
    application's version config (itself defaulting to 0.1.0 in a fresh skeleton), not the framework's. A
    fresh composer create-project marrow/skeleton followed immediately by php forge list looked exactly like
    a framework frozen since an early prototype. Now built from framework_version() instead — php forge list
    on a real install correctly shows e.g. Marrow 2.4.1.
  • php forge about mislabeled the application's own version as "Framework", and never showed the actual
    installed marrow/framework version anywhere. Now shows both, correctly separated: Framework (the real,
    Composer-resolved version) and App (config('app.name') + config('app.version'), the app's own).
    Also switched its four other reads from raw $_ENV to config(), matching the rest of the command.
  • FrameworkExtension::getGlobals()'s Twig app.* global read $_ENV directly instead of config('app.*')
    — a value set only in config/app.php (not mirrored to an env var) was invisible to {{ app.name }}/
    {{ app.version }} in templates even though config('app.name') elsewhere in the same app saw it correctly.
    Now reads through config() like everywhere else, and app.version's stale 0.1.0 fallback is gone (empty
    string when unset, same as AboutCommand).

AureDulvresse and others added 3 commits October 6, 2026 12:30
The `php forge` banner and `php forge about` always showed "0.1.0"/a
mislabeled app version regardless of the actually-installed
marrow/framework version, because Console\Kernel and AboutCommand both
read config('app.version') (the application's own version, itself
defaulting to 0.1.0 in a fresh skeleton) rather than anything
reflecting the framework package itself.

Added framework_version(), resolved from Composer\InstalledVersions,
and wired it into the Kernel's console banner and a new, correctly
labeled "Framework" row in `about` (separate from "App"). Also fixed
FrameworkExtension::getGlobals() reading $_ENV directly instead of
config('app.*'), which made a config-only (non-env) app.name/version
invisible to Twig's `app.*` global.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Thin wrapper around SymfonyStyle::choice()'s multiSelect mode,
following the same pattern as the existing choice()/confirm()/ask()
helpers. Used by marrow/anvil's now-interactive anvil:install.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RedirectResponse and JsonResponse were each a sibling of Response (both
extended their own Symfony ancestor directly), not a subtype of it —
so a controller method returning either a redirect or a rendered view
had no single Marrow type to declare, only their shared Symfony
ancestor worked. marrow/warden's controllers are the clearest example
of the workaround this forced.

Response now holds a Symfony\...\Response internally instead of
extending it; RedirectResponse/JsonResponse extend Response directly,
each composing their own more specific Symfony instance. Every Symfony
method not explicitly redeclared still works via __call() (documented
with @mixin for IDE/PHPStan support); headers is a real public
property pointing at the same ResponseHeaderBag the wrapped instance
sends from, not magic.

Propagated the type change through every internal consumer: Router,
Pipeline, Kernel, SessionManager, all bundled middleware, and fixed
ApiController's JSON helpers, which were still constructing a raw
Symfony JsonResponse directly (latent bug: would have broken the
moment Router::toResponse()'s now-Marrow-typed instanceof check saw
one).

Verified: isolated unit tests for the new Response/RedirectResponse/
JsonResponse wrapper (fluent chaining preserved on the Marrow instance,
not leaked to the inner Symfony one; clone deep-copies correctly);
full pest suite (261 passing) and phpstan (0 errors); and a true
end-to-end pass — overlaid this code into a real skeleton app's
vendor/marrow/framework, ran its Pest suite against the real
Http\Kernel, and hit a real `php forge serve` instance with curl to
confirm headers/cookies/status/body all still send correctly.

Breaking for any app/package with its own middleware or controller
typed against Symfony\Component\HttpFoundation\Response as the same
workaround — see CHANGELOG.md for the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@AureDulvresse
AureDulvresse merged commit 530579d into main Oct 7, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant