Skip to content

Support Laravel 12 - #207

Open
LukeTowers wants to merge 237 commits into
developfrom
wip/1.3
Open

LukeTowers wants to merge 237 commits into
developfrom
wip/1.3

Conversation

@LukeTowers

@LukeTowers LukeTowers commented Feb 25, 2025 •

Copy link
Copy Markdown
Member

Replaces #173, continuing the work done by @mjauvin @bennothommo & @wverhoogt

Summary by CodeRabbit

  • New Features

    • Added PHP 8.2+ support and modern framework capabilities.
    • Added MariaDB connectivity and improved database support across major platforms.
    • Added configurable authentication password attributes and fluent authentication setup.
    • Added optional pagination totals and improved expression-based searching.
    • Added broader translation path and locale fallback handling.
    • Added customizable application path generation.
  • Bug Fixes

    • Improved schema column-change handling across database platforms.
    • Prevented invalid file records from producing malformed results.
    • Improved form value handling and month formatting.

@JonasPardon

Copy link
Copy Markdown

"I don't understand the problem, striping the extra quotes was the whole reason to override getDefaultValue() in the first place."

— @mjauvin on Discord, about item 1 of my notes on wintercms/winter#1366

Agreed, stripping the extra quotes is needed: some databases report a column's default as a quoted SQL literal, so a default copied back from getColumns() would otherwise be quoted twice. My point was only about the old implementation, which removed every ', including apostrophes inside the value. So ->default("O'Brien") on a new column became 'OBrien', and on MySQL, which reports defaults unquoted, ->change() rewrote an existing O'Brien default to OBrien.

Your 0f69f95 fixes both. One case is still left: databases that report the default as an SQL literal also escape the quotes inside it, for example 'O''Brien'. Removing only the outer pair leaves O''Brien, which parent::getDefaultValue() escapes again.

Here's your current override run on Laravel 12.69.3, next to the same override with the inner quotes unescaped as well:

Input to getDefaultValue() Current (0f69f95) With str_replace("''", "'", …)
O'Brien (from a migration, or MySQL getColumns()) 'O''Brien' 'O''Brien'
'foo' (MariaDB/SQLite getColumns()) 'foo' 'foo'
'O''Brien' (MariaDB/SQLite getColumns()) 'O''''Brien', so the stored value becomes O''Brien 'O''Brien'
'O'Brian' (your test input) 'O''Brian' 'O''Brian'

Suggested change, which keeps your tests passing:

$value = str_replace("''", "'", substr($value, 1, -1));

and a test case with default("'O''Brian'") next to the existing "'O'Brian'" one.

I checked that SQLite's getColumns() returns 'O''Brien', but I haven't run MariaDB. Its information_schema documents the same escaped format. The SQLite ->change() path already keeps such a default intact, so this mainly matters for MariaDB through MySqlBasedGrammar::compileChange().

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@mjauvin

mjauvin commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

@LukeTowers @JonasPardon I don't think we need to override Mail::fake(), it doesn't differ from the base Laravel version.

Or is there something special about our MailFake maybe that we need ?

We should at least drop the argument, it's not needed and Laravel doesn't have it either.

@JonasPardon

Copy link
Copy Markdown

I don't think we need to override Mail::fake(), it doesn't differ from the base Laravel version.

@mjauvin It does differ, even though the body mirrors Laravel's:

  1. Storm's Mail facade extends the base Facade, not Illuminate\Support\Facades\Mail, so without the override there is no fake(): Mail::fake() is forwarded to the mailer and throws Method Illuminate\Mail\Mailer::fake does not exist.
  2. Its accessor is mailer, while Laravel's is mail.manager. Laravel's fake() passes the facade root to MailFake, which requires a MailManager, so extending Laravel's facade instead fails with MailFake::__construct(): Argument #1 ($manager) must be of type Illuminate\Mail\MailManager, Illuminate\Mail\Mailer given. The override resolves mail.manager itself.
  3. It creates Winter's MailFake, which turns Mail::send('acme.blog::mail.welcome', $data, $callback) into a mailable. Laravel's MailFake ignores anything that isn't a Mailable, so Mail::assertSent() wouldn't see Winter-style sends.

I checked 1 and 2 by removing the override on wip/1.3 and running tests/Support/MailFacadeTest.php.

@mjauvin

mjauvin commented Oct 8, 2026

Copy link
Copy Markdown
Member

@JonasPardon I agree with your answer above. But I think we can drop the mailmanager argument.

austinderrick and others added 13 commits October 10, 2026 12:15
Add Laravel's InvokeDeferredCallbacks middleware to the HTTP kernel so
callbacks registered with defer() run after the response is sent.
# Conflicts:
#	composer.json
)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Luke Towers <github@luketowers.ca>
Co-authored-by: Luke Towers <git@luketowers.ca>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

With a non-UTC app.timezone, a UNIX timestamp assigned to a date attribute is stored shifted by the UTC offset on wip/1.3:

// app.timezone = 'Europe/Brussels'
$model->published_at = 1700000000; // 2023-11-14 22:13:20 UTC

// 1.2: stored as '2023-11-14 23:13:20'
// wip/1.3: stored as '2023-11-14 22:13:20', which reads back an hour earlier
Model::asDateTime() calls Argon::createFromTimestamp($value) without a timezone. Carbon 2 used PHP's default timezone for that, which Storm sets from app.timezone. Carbon 3 uses UTC instead. Laravel hit the same regression and fixed it the same way in laravel/framework@8c61f50 (#50751), which is still how HasAttributes::asDateTime() reads on 12.x. Projects on the default UTC timezone are not affected.

Pass date_default_timezone_get(), like Laravel.
Add a test that assigns a timestamp with Europe/Brussels as the default timezone and checks that the stored value and the timestamp read back are correct. It fails without the change.
asDateTime() is the only numeric-timestamp cast in Storm. Laravel's commit also covered its other createFromTimestamp() call sites; the equivalent ones in Winter core are fixed in wintercms/winter#1579.
LukeTowers and others added 9 commits October 22, 2026 22:55
# Conflicts:
#	phpstan.neon
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Luke Towers <git@luketowers.ca>
Co-authored-by: Luke Towers <github@luketowers.ca>

With app.loadDiscoveredPackages enabled, no discovered package provider gets registered on wip/1.3.

0a6b24e ("Laravel 10 changes") dropped the brackets around the manifest's providers in Application::registerConfiguredProviders():

$providers->splice(1, 0, $this->make(PackageManifest::class)->providers());
That splices each provider class in as a separate string, between the Illuminate\ group and the other providers. collapse() only flattens arrays and skips anything else, so those strings never reach ProviderRepository::load(). Laravel 12 still passes them as one group ([…->providers()]), which is what this restores. The default (loadDiscoveredPackages => false) is unaffected.

Restore the brackets.
Add a test that boots an Application with a discovered provider and checks that it gets registered. It fails without the change.
e() has been overridden in Storm since 2020 for a single reason: Laravel 5.6 changed the helper's $doubleEncode default from false to true, aligning it with htmlspecialchars()'s own default, and rather than pass false at the handful of call sites that were double-encoding as a result, the default was reverted globally. That shim has outlived its purpose, and the call sites it was hiding are better fixed directly.

Keeping it has three costs. The body is still Laravel 5.x's, so e() here does not resolve a DeferringDisplayableValue and does not unwrap a BackedEnum — e($someEnum) raises Object of class X could not be converted to string where Laravel returns the backing value, which makes ordinary Laravel-style plugin code fail on Winter for a non-obvious reason. The escaping default matches neither Laravel nor PHP. And it makes the test suite disagree with the application: the composer binary requires vendor/autoload.php before the configured bootstrap, so PHPUnit resolves Laravel's e() while the served application resolves this one, and no test can observe the behaviour being overridden.

Deleting it restores Laravel's implementation in both contexts. bootstrap/autoload.php's priority require stays — it is still load-bearing for trans() and collect().

Behaviour changes
e() now double-encodes by default, matching Laravel and htmlspecialchars(). e('Tom &amp; Jerry') returns Tom &amp;amp; Jerry rather than Tom &amp; Jerry.
e() on a backed enum returns its value instead of raising.
e() resolves a DeferringDisplayableValue.
e(null) is unchanged (empty string).

(cherry picked from commit 7b506f5)
…-removal

Sync the e() helper override removal into wip/1.3
…l 13

Laravel 13 inlined and deleted the internal onceWithColumns() helper from
Illuminate\Database\Query\Builder. Our get() override still relies on it to
set the columns before the memory cache hashes the query, so calling it threw
BadMethodCallException on Laravel 13 (228 test errors + a PHPStan failure).
Redefine the helper locally with the same save/restore semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	composer.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement PRs that implement a new feature or substantial change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants