Laravel 13.24 extends the framework's image API with dominant color detection and HEIC input and output, adds a modelKeys() method to the Eloquent query builder, introduces an array_keys validation rule, and fixes wildcard validation that could stall a request for over a minute on large arrays. The Laravel team released v13.24.0 on August 4, 2026.
dominantColor()on images, plus adominantbackground forcontain()androtate()- AVIF and HEIC input,
toHeic()output, and both formats in theimagevalidation rule modelKeys()on the Eloquent query builder- New
array_keysvalidation rule with an:unexpectedplaceholder - A fix for quadratic rule expansion in
foo.*.barvalidation
What's New
Dominant Color Detection in the Image API
The first-party image API can now tell you the average color of an image. dominantColor() downsamples the image to a single pixel and returns that pixel as a hex string:
use Illuminate\Support\Facades\Image; $color = Image::fromPath(storage_path('app/photo.jpg'))->dominantColor(); // "#8a6f4c"
That is the value you would use for a placeholder background while a photo loads, or to tint a card behind its own artwork. The result is memoized per image instance, and if the pipeline has pending transformations, they run before the color is sampled, so you get the color of the image you are about to store rather than the original.
The same sampling is available as a background value. Passing dominant to contain() or rotate() fills the letterbox bars or the rotation corners with the image's own dominant color instead of a color you picked in advance:
Image::fromUpload($request->file('photo')) ->contain(1200, 800, background: 'dominant') ->store('photos');
Contributed by @dr-codswallop in #60932.
AVIF and HEIC Input, HEIC Output
The image API exposed AVIF output through toAvif(), but rejected AVIF files on the way in, and HEIC and HEIF were rejected in both directions even though Intervention Image ships an encoder for them. Photos coming straight off an iPhone are HEIC, so uploads had to be converted before they reached the framework.
AVIF, HEIC, and HEIF are now accepted as inputs, toHeic() and optimize('heic') are available as outputs, and all three formats are recognized by the image validation rule:
Image::fromPath(storage_path('app/photo.heic')) ->usingImagick() ->cover(1200, 800) ->toAvif() ->quality(80) ->store('photos');
Both changes build on the image pipeline covered in our practical guide to Laravel's image processing. optimize('heif') and the image/heif MIME type are normalized to HEIC, so files are stored with the canonical .heic extension. Image::extension() also learned the image/x-avif, image/x-heic, and image/heif aliases. HEIC processing requires an Imagick build with the HEIC codec compiled in. Contributed by @riasvdv in #60922.
modelKeys() on the Eloquent Query Builder
Eloquent collections have had modelKeys() for a long time, but getting the same array straight off a query meant naming the primary key yourself with pluck('id'). The query builder now has the method too:
$ids = Post::query()->where('published', true)->modelKeys(); // [1, 2, 3]
It plucks the qualified key name from the model, so a custom $primaryKey and joined queries both resolve correctly without hardcoding a column. Contributed by @ziadoz in #60924.
array_keys Validation Rule
Rule::array(['sort', 'direction']) already rejects arrays containing unexpected keys, but it reports the failure with the generic message "The options field must be an array," which does not tell the reader which of the two checks failed. The new array_keys validation rule answers only the key question and says so:
$request->validate([ 'options' => Rule::arrayKeys(['sort', 'direction']),]); // or as a string$request->validate([ 'options' => 'array_keys:sort,direction',]);
Given ['sort' => 'name', 'colour' => 'red'], the failure message is "The options field must only contain the following keys: sort, direction." The keys are permitted, not required, so this constrains what may appear rather than demanding everything be present. required_array_keys still covers the other half.
Two placeholders are available for custom messages: :values for the accepted keys, and :unexpected for the keys that actually caused the failure, which is usually the more useful one in an API response:
$request->validate( ['options' => Rule::arrayKeys(['sort', 'direction'])], ['options.array_keys' => 'The :attribute field may not contain :unexpected.'],); // The options field may not contain colour.
Because the rule reports as ArrayKeys in $validator->failed(), it composes with array when you want both checks reported independently. Rule::array() and its message are untouched. Contributed by @nebarg in #60918.
Wildcard Validation No Longer Stalls on Large Arrays
Expanding foo.*.bar rules was quadratic in the number of expanded attributes. ValidationRuleParser::explodeWildcardRules() accumulated results by reassigning the return value of mergeRules(), which took the accumulated set by value, so copy-on-write cloned the entire rule set once per expanded attribute. All of the time was spent in Validator::make(), before a single rule ran.
The numbers from the pull request, using 17 rules under one wildcard:
| Items | Before | After |
|---|---|---|
| 1,000 | 0.98s | 0.11s |
| 4,000 | 18.59s | 0.47s |
| 8,000 | 85.13s | 0.98s |
At 8,000 items the payload is only about 1.1 MB, so post_max_size and web server body limits never fire. A max:500 rule on the array did not help either, since expansion happened before it was evaluated.
The merge body moved into a private method that takes the accumulator by reference. mergeRules() and mergeRulesForAttribute() keep their signatures, but explodeWildcardRules() no longer routes through the latter, so an override there will no longer affect wildcard expansion. Contributed by @mariomka in #60908.
Arr::forget() Removing the Wrong Element
Arr::forget() reset its internal reference back to the top-level array only after handling an exact top-level key match. After processing a dotted key, the reference was left pointing at a nested array, so the next key in the list resolved against that nested array:
$array = ['users' => ['name' => 'Joe', 'id' => 1], 'id' => 99]; Arr::forget($array, ['users.name', 'id']); // before: ['users' => [], 'id' => 99]// after: ['users' => ['id' => 1]]
The wrong element was removed and the requested one kept, with no error. Moving the reset to the top of the loop fixes it for everything that delegates to the helper, including Arr::except(), Collection::except(), data_forget(), and Uri::withoutQuery(). Contributed by @AlessioGiacobbe in #61023.
Other Fixes and Improvements
CompiledRouteCollectionrebuilt everyRouteobject on eachget(),getByAction(), andgetRoutesByMethod()call, which made 404s expensive on cached routes becausecheckForAlternateVerbs()callsget()once per HTTP verb. Lookup indexes built from the raw route attributes bring a 404 against 2,000 cached routes from 71ms to 2.5ms (#60909)- An accessor for an existing attribute that was also listed in
$appendsreceivednullinstead of the stored value, so$model->priceand$model->toArray()['price']disagreed (#60921) Str::replace()andStr::remove()delegate tostr_ireplace()for case-insensitive matching, which only folds ASCII, soStr::replace('ž', 'X', 'Žltý kôň', false)matched nothing. Multibyte search terms now use a/iupattern; ASCII terms take the original path (#60882)- Resolving a relationship attribute inside a
Relation::noConstraints()closure skipped thewhereclause on the primary key, so aBelongsTocould return the first row in the table instead of the right one. A newRelation::withConstraints()wraps attribute resolution (#60931) PostgresGrammar::wrapJsonPathAttributes()interpolated JSON path attributes into quoted SQL literals without escaping single quotes, unlike every other driver (#60923)- Two aliases pointing at each other made
Container::getAlias()recurse until memory exhaustion; it now throws aLogicException(#60974) NumberPromptwas the only prompt type without a fallback, sonumber()threw on Windows, in non-interactive contexts, and in tests instead of degrading to a Symfony question (#60959)ShouldBeUniqueUntilProcessingjobs now track the cache lock owner and release throughrestoreLock(), so a retry cannot release a lock acquired by a newer dispatch while middleware-released jobs still get their lock back (#60906)Guardedattribute resolution was skipped onPivot, which overrides$guardedwith[]and so failed the['*']default check (#60952)- Job delays are respected when bulk pushing to
QueueFake(#60916) andFailoverQueue(#60950), and batch testing fakes use immutable timestamps (#60912) LengthAwarePaginatorguards against division by zero whenperPageis 0, andCursorPaginatorreindexes items consistently on both directions (#60968)- Most Artisan commands moved from the
$nameplusgetArguments()/getOptions()style to a single$signaturestring, backed by characterization tests that record every command's name, aliases, arguments, and options. Four commands stayed on the array style because they have options with required values, which the signature DSL cannot express (#60926) - URL validation logic was adjusted (#61009), and several PHP 8.5 null array offset deprecations were resolved (#60964, #60954, #60987)
Upgrade Notes
No breaking changes are expected for typical applications. Two changes are worth checking if you extend framework internals: explodeWildcardRules() no longer calls mergeRulesForAttribute(), so an override of that method will not affect wildcard expansion, and Container::getAlias() now throws a LogicException on circular aliases that previously exhausted memory. HEIC support depends on your Imagick build including the HEIC codec.
Two pull requests merged during this cycle were reverted before release, the if-related rector rules (#61027) and a Str::uuid()->toString() conversion (#61028). Review the changelog for PR-by-PR details when upgrading.
References