Skip to content

Add Laravel 13 support - #40

Merged
jholloman merged 12 commits into
Weebly:masterfrom
tlsqr:laravel-13-support
Oct 15, 2026
Merged

jholloman merged 12 commits into
Weebly:masterfrom
tlsqr:laravel-13-support

Conversation

@tlsqr

@tlsqr tlsqr commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Adds Laravel 13 support while keeping Laravel 10, 11 and 12.

Changes

  • composer.json allows laravel/framework ^13.0 and orchestra/testbench ^11.0. The package's PHP constraint stays at >=8.1, since Composer already enforces Laravel 13's PHP 8.3 minimum per install.
  • CI runs Laravel 13 / Testbench 11 on PHP 8.3, 8.4 and 8.5.
  • CI sets COMPOSER_NO_BLOCKING=1 on the Laravel 10 and 11 legs only. Composer refuses to install versions with security advisories, and every Laravel 10 and 11 release has one, so those legs failed before any test ran. This also affects master. The Laravel 12 and 13 legs keep the check.
  • EloquentBuilder::whereNot(): Laravel 13 sends an array argument straight to the base query builder, so mutated columns were compared against unmutated values (whereNot(['id' => $uuid]) excluded nothing). The override routes arrays through a negated nested where, following the same rules as Query\Builder::addArrayOfWheres().
  • Model::setRawAttributes() now clears the mutator cache. Laravel 13's refreshForUpdate() and $refreshes (attributes reloaded after save) bypass refresh(), so mutated attributes kept their stale cached values. refresh(), refreshForUpdate() and $refreshes all end in setRawAttributes(), so the refresh() override is removed.
  • BelongsToMany::newPivotQuery() applies wherePivotBetween() constraints. Laravel 13 records them and applies them in its own newPivotQuery(). The override skipped them, so wherePivotBetween(...)->detach(), sync() and updateExistingPivot() touched every pivot row of the parent. Laravel 10-12 don't record these constraints, so nothing changes there.
  • Integration tests use defineEnvironment() instead of the deprecated getEnvironmentSetUp().

Behaviour change on Laravel 10-12

This is a bug fix, but consumers may notice it:

  • whereNot([...]) now negates. Before, the array went through where() and lost both the not and the boolean, so whereNot(['id' => $id]) returned the row it should exclude, and orWhereNot([...]) behaved as and.

tlsqr and others added 8 commits October 14, 2026 12:05
Cover whereNot with scalar, closure and empty-array input, refresh()
of mutated values, global scopes around nested where closures (the
removed-scope case is version-aware for Laravel 13), related model
connection inheritance, and wherePivot()->detach() on a relation
without a custom pivot class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Laravel 13 needs PHP 8.3+, which Composer enforces per install, so the
package's own PHP constraint stays at >=8.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testbench deprecated getEnvironmentSetUp() in favour of
defineEnvironment(), which exists in Testbench 8 through 11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Laravel 13 sends an array given to whereNot() straight to the base
query builder, so mutated columns were compared against unmutated
values. Route the array through a negated nested where instead, using
the same rules as Query\Builder::addArrayOfWheres().

This also changes behaviour on Laravel 10-12, where the array went
through where() and lost both the negation and the boolean:
whereNot(['id' => $id]) used to return the row it should exclude.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Laravel 13 reloads attributes in refreshForUpdate() and, for models
that list $refreshes, after every save. Neither goes through refresh(),
so mutated attributes kept returning their stale cached values.

All of these paths, and refresh() on Laravel 10-13, end in
setRawAttributes(), so clear the cache there and drop the refresh()
override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
newPivotQuery() only applied wherePivot() and wherePivotIn(), so
detach(), sync(), toggle() and updateExistingPivot() ignored
wherePivotNull() constraints and touched every pivot row of the
parent. Laravel 13 also applies wherePivotBetween() there.

Apply both, in Laravel's order. On Laravel 10-12 this narrows those
operations when wherePivotNull() is used, matching Laravel itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit b37baae. The change is not needed for Laravel 13
and changes behaviour on Laravel 10-12, so it moves to its own PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tlsqr and others added 2 commits October 15, 2026 10:45
Composer 2.9+ refuses to install versions that have security
advisories. Every Laravel 10 and 11 release has one, so those CI legs
failed at composer update before any test ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Laravel 12 and 13 legs keep the check, since they have releases
without advisories. Use COMPOSER_NO_BLOCKING, which replaces the
deprecated COMPOSER_NO_SECURITY_BLOCKING in Composer 2.10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tlsqr
tlsqr marked this pull request as ready for review October 15, 2026 15:38
Comment thread src/Database/EloquentBuilder.php Outdated
{
// Laravel 13 sends arrays straight to the base query builder, which would skip
// mutating the values. An empty array is left to the parent, because an empty
// nested where would make where() pop the previous clause.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these comments really necessary?

Comment thread src/Database/EloquentBuilder.php Outdated
// mutating the values. An empty array is left to the parent, because an empty
// nested where would make where() pop the previous clause.
if (is_array($column) && $column !== []) {
// Same handling of an array of wheres as Query\Builder::addArrayOfWheres()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto re: comments

Comment thread src/Database/Model.php Outdated
/**
* Every reload of attributes from the database ends here: refresh(), and on
* Laravel 13 also refreshForUpdate() and the attributes refreshed after save.
*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto re: comments

Comment thread tests/Integration/MutatorTest.php Outdated
* Columns Laravel 13+ reloads from the database after every save.
*
* @var array<int, string>
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto re: comments

Comment thread tests/Integration/PivotTest.php Outdated
* Attach two second models through the relation without a custom pivot class:
* "Second null" with a null extra, "Second m" with extra "m".
*/
private function attachTwoPlain(): FirstModel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto re: comments. Maybe the function name could be a tad clearer, if verbose. attachTwoModelsWithoutPivot?

Review feedback: the comments restate what the tests and commit history
already cover. Rename attachTwoPlain() and second_models_plain() to say
what they lack: a custom pivot class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tlsqr added a commit to tlsqr/laravel-mutate that referenced this pull request Oct 15, 2026
Matches the review feedback on Weebly#40: the comments restate what the tests
already cover. Rename attachTwoPlain() and second_models_plain() to say
what they lack: a custom pivot class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Laravel 13 records wherePivotBetween() constraints and applies them in
newPivotQuery(). The override skipped them, so detach(), sync() and
updateExistingPivot() touched every pivot row of the parent. Apply them
after the whereIn constraints, as Laravel does. Laravel 10-12 don't
record these constraints, so nothing changes there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tlsqr added a commit to tlsqr/laravel-mutate that referenced this pull request Oct 15, 2026
Applying wherePivotBetween() in pivot queries only matters on Laravel
13, so it moves to Weebly#40 with its tests. This PR keeps the
wherePivotNull() fix for Laravel 10-12. The constraint order test needs
both loops, so it returns when this branch is rebased onto Weebly#40.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jholloman
jholloman merged commit 5628fa9 into Weebly:master Oct 15, 2026
12 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.

3 participants