Bug report
When an array{...}-shaped value is combined with an array<string, mixed> via the += (union) operator, PHPStan widens the entire result to non-empty-array<string, mixed> (plus hasOffsetValue(...) for the always-present keys) rather than preserving the already-known keyed prefix. The optional keys are dropped, and the value no longer matches a declared return type that uses an open ...<string, mixed> tail.
This breaks the common "build a known prefix, then fold in caller-supplied extras" pattern, where the declared return type is array{knownKey: T, ...<string, mixed>}.
Code snippet that reproduces the problem
https://phpstan.org/r/b7b3d1b4-bbb4-46bc-af95-4f5650e9a9d4
<?php declare(strict_types = 1);
use function PHPStan\dumpType;
/**
* @param array<string, mixed> $extras
* @return array{a: string, b: string, c: string, d?: string, ...<string, mixed>}
*/
function build(?string $d, array $extras): array
{
$out = [];
$out['a'] = 'foo';
$out['b'] = 'bar';
$out['c'] = 'baz';
if (null !== $d) {
$out['d'] = $d;
}
dumpType($out); // array{a: 'foo', b: 'bar', c: 'baz', d?: string}
$out += $extras;
dumpType($out); // non-empty-array<string, mixed>&hasOffsetValue('a', 'foo')&hasOffsetValue('b', 'bar')&hasOffsetValue('c', 'baz')
return $out;
}
Config: level 10, strict-rules + bleeding-edge, treatPhpDocTypesAsCertain: true.
Expected output
After $out += $extras, the keyed prefix should survive. += keeps left-hand keys and adds only right-hand keys not already present, so the result should stay assignable to the declared return type, e.g.:
array{a: 'foo', b: 'bar', c: 'baz', d?: string, ...<string, mixed>}
and build() should report no errors.
Actual output
Function build() should return array{a: string, b: string, c: string, d?: string, ...<string, mixed>} but returns non-empty-array<string, mixed>.
The second dumpType shows the cause: array{...} += array<string, mixed> collapses to non-empty-array<string, mixed>&hasOffsetValue(...). The hasOffsetValue intersections are kept for the three required keys, but the optional d? and the array-shape structure are lost, so the result no longer matches the declared shape with its open ...<string, mixed> tail.
Bug report
When an
array{...}-shaped value is combined with anarray<string, mixed>via the+=(union) operator, PHPStan widens the entire result tonon-empty-array<string, mixed>(plushasOffsetValue(...)for the always-present keys) rather than preserving the already-known keyed prefix. The optional keys are dropped, and the value no longer matches a declared return type that uses an open...<string, mixed>tail.This breaks the common "build a known prefix, then fold in caller-supplied extras" pattern, where the declared return type is
array{knownKey: T, ...<string, mixed>}.Code snippet that reproduces the problem
https://phpstan.org/r/b7b3d1b4-bbb4-46bc-af95-4f5650e9a9d4
Config: level 10, strict-rules + bleeding-edge,
treatPhpDocTypesAsCertain: true.Expected output
After
$out += $extras, the keyed prefix should survive.+=keeps left-hand keys and adds only right-hand keys not already present, so the result should stay assignable to the declared return type, e.g.:and
build()should report no errors.Actual output
The second
dumpTypeshows the cause:array{...} += array<string, mixed>collapses tonon-empty-array<string, mixed>&hasOffsetValue(...). ThehasOffsetValueintersections are kept for the three required keys, but the optionald?and the array-shape structure are lost, so the result no longer matches the declared shape with its open...<string, mixed>tail.