isset($array[$key]) on a possibly-null elementisset is false when the key is missing *or* the value is null, so
after a successful isset the element is the non-null half of T|null.
Inside isset, reading a key that may be absent is the condition, not an
undefined-offset error (vimeo/psalm#10756).
References:
- psalm#10756| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.11Phan 6.0.11 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 | Fail | arrays_isset_nullable_fetch.php:29: Argument 1 ($value) is $array['key'] of type ?int but \Conformance\Tests\ArraysIssetNullableFetch\takesInt() takes int defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/arrays_isset_nullable_fetch.php:19 (expected type to be non-nullable) [PhanTypeMismatchArgumentNullable] Expectation diffLine 29: Expected silence (quiet probe), got ["Argument 1 ($value) is $array['key'] of type ?int but \\Conformance\\Tests\\ArraysIssetNullableFetch\\takesInt() takes int defined at /Users/megurine/repo/php/php-typing-conformance/conformance/tests/arrays_isset_nullable_fetch.php:19 (expected type to be non-nullable) [PhanTypeMismatchArgumentNullable]"] |
| phpstan | 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 | Pass reported Lv.4+ | arrays_isset_nullable_fetch.php:30: Strict comparison using === between int and null will always evaluate to false. [reported-from-level=4] With strict-rulesarrays_isset_nullable_fetch.php:30: Strict comparison using === between int and null will always evaluate to false. |
| psalm | 6.20.9Psalm 6.20.9@f67170ac3c3b931f5088681e21d6c8babc601c08 next: 7.0.4 |
Pass (pzoom≠) | arrays_isset_nullable_fetch.php:30: int does not contain null [DocblockTypeContradiction] arrays_isset_nullable_fetch.php:30: Docblock-defined type int for $array['key'] is never null [RedundantConditionGivenDocblockType] pzoom (Psalm port)No diagnostics from pzoom. |
| mago | 1.59.0mago 1.59.0 | Pass | arrays_isset_nullable_fetch.php:30: Redundant `===` comparison: left-hand side is never identical to right-hand side. [redundant-comparison] arrays_isset_nullable_fetch.php:30: Redundant ternary operator: condition is always falsy. [impossible-condition] |
| mir | 0.91.0mir 0.91.0 | Fail | arrays_isset_nullable_fetch.php:29: PossiblyNullArgument: Argument $value of takesInt() might be null [MIR0104] Expectation diffLine 29: Expected silence (quiet probe), got ["PossiblyNullArgument: Argument $value of takesInt() might be null [MIR0104]"] |
| phpantom | 0.11.1phpantom_lsp 0.11.1 | Pass | No diagnostics reported. |
| intelephense | 1.18.5intelephense 1.18.5 | Pass | No diagnostics reported. |
| phpactor | 2026.10.07.0Phpactor 2026.10.07.0 | Pass | No diagnostics reported. |
| phpy | 1.0.19490phpy 1.0.19490 | Pass | No diagnostics reported. |
| qodana | 262.11879.104PhpStorm 262.11879.104 | Pass | arrays_isset_nullable_fetch.php:30: Condition is always 'false' because 'isset($array['key'])' is already 'true' at this point [PhpConditionAlreadyCheckedInspection] |
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Fail | arrays_isset_nullable_fetch.php:26: param $array miss matched with phpdoc type <<array<string, int|null>>> [funcParamTypeMissMatch] arrays_isset_nullable_fetch.php:29: potential null array access in parameter value of function Conformance\Tests\ArraysIssetNullableFetch\takesInt [notNullSafetyFunctionArgumentArrayDimFetch] arrays_isset_nullable_fetch.php:37: param $array miss matched with phpdoc type <<array<string, int>>> [funcParamTypeMissMatch] arrays_isset_nullable_fetch.php:40: potential null array access in parameter value of function Conformance\Tests\ArraysIssetNullableFetch\takesInt [notNullSafetyFunctionArgumentArrayDimFetch] Expectation diffLine 29: Expected silence (quiet probe), got ["potential null array access in parameter value of function Conformance\\Tests\\ArraysIssetNullableFetch\\takesInt [notNullSafetyFunctionArgumentArrayDimFetch]"] Line 40: Expected valid value to be accepted, got ["potential null array access in parameter value of function Conformance\\Tests\\ArraysIssetNullableFetch\\takesInt [notNullSafetyFunctionArgumentArrayDimFetch]"] Line 26: Unexpected errors ["param $array miss matched with phpdoc type <<array<string, int|null>>> [funcParamTypeMissMatch]"] Line 37: Unexpected errors ["param $array miss matched with phpdoc type <<array<string, int>>> [funcParamTypeMissMatch]"] |
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Pass | No diagnostics reported. |
| phpstan-strict | 2.3.5PHPStan - PHP Static Analysis Tool 2.3.5 | Pass | arrays_isset_nullable_fetch.php:30: Strict comparison using === between int and null will always evaluate to false. |
| psalm-next | 7.0.4Psalm 7.0.4@fa5b1f6e665cb743838ffd865b8fc96207f9409f | Pass | arrays_isset_nullable_fetch.php:30: int does not contain null [DocblockTypeContradiction] arrays_isset_nullable_fetch.php:30: Docblock-defined type int for $array['key'] is never null [RedundantConditionGivenDocblockType] |
<?php
declare(strict_types=1);
namespace Conformance\Tests\ArraysIssetNullableFetch;
/**
* `isset($array[$key])` on a possibly-null element.
*
* PHP `isset` is false when the key is missing *or* the value is `null`, so
* after a successful `isset` the element is the non-null half of `T|null`.
* Inside `isset`, reading a key that may be absent is the condition, not an
* undefined-offset error (vimeo/psalm#10756).
*
* References:
* - https://github.com/vimeo/psalm/pull/10756
*/
function takesInt(int $value): void
{
}
/**
* @param array<string, int|null> $array
*/
function afterIsset(array $array): void
{
if (isset($array['key'])) {
takesInt($array['key']); // Q: isset excludes null
echo $array['key'] === null ? 'null' : 'int'; // E?: after isset, === null is always false
}
}
/**
* @param array<string, int> $array
*/
function insideIsset(array $array): void
{
if (isset($array['missing'])) { // Q: offset access inside isset is not an undefined-key error
takesInt($array['missing']); // V
}
}