Skip to content

Add Laravel 13 support - #39

Closed
tlsqr wants to merge 7 commits into
Weebly:masterfrom
tlsqr:vehikl-barramundi/laravel-13-support
Closed

tlsqr wants to merge 7 commits into
Weebly:masterfrom
tlsqr:vehikl-barramundi/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.
  • 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. Every reload path ends in setRawAttributes() on Laravel 10-13, so the refresh() override is removed.
  • BelongsToMany::newPivotQuery() applies wherePivotNull() constraints, and wherePivotBetween() on Laravel 13, in Laravel's order.
  • Integration tests use defineEnvironment() instead of the deprecated getEnvironmentSetUp().

Behaviour changes on Laravel 10-12

These are bug fixes, but consumers may notice them:

  • 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.
  • wherePivotNull() now narrows detach(), sync(), toggle() and updateExistingPivot() on relations without a custom pivot class. Before, those touched every pivot row of the parent.

Testing

17 new tests: characterization tests committed before the upgrade, and acceptance tests committed with each fix (red before, green after). Run locally on PHP 8.3 against SQLite and MySQL 5.6:

Laravel Testbench Tests Skipped PHPStan
10.50.3 8.39.0 115 pass 4 0 errors
11.57.0 9.18.0 115 pass 4 0 errors
12.70.0 10.12.0 115 pass 4 0 errors
13.36.0 11.3.0 115 pass 0 0 errors

The skips are tests for Laravel 13-only APIs. Laravel 13 also passes on PHP 8.5.7.

Not checked locally: whether setup-php installs imagick on PHP 8.5. The package doesn't need it, so it can be dropped from that leg if the job fails.

Known issues not addressed here

Found during review, not caused by Laravel 13:

  • sync() compares unserialized current ids with serialized new ids, so it detaches and re-attaches already-attached rows.
  • where([...]) ignores $boolean, so orWhere([...]) behaves as and.
  • A where(Closure) that adds no clauses removes the previous clause.
  • On using() relations, detach() ignores pivot constraints.

🤖 Generated with Claude Code

tlsqr and others added 7 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>
@tlsqr tlsqr closed this Oct 14, 2026
@tlsqr
tlsqr deleted the vehikl-barramundi/laravel-13-support branch October 14, 2026 18:25
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