PHP: What do "Undefined variable" and "Undefined array key" mean?
These three messages are probably the most common ones in PHP altogether. They always mean the same thing: you are accessing something that does not exist. More important than making the message go away is the question of why it does not exist.
The three variants
Warning: Undefined variable $name
Warning: Undefined array key "port"
Warning: Undefined property: Config::$host
In PHP 8 these are warnings. Before PHP 8 they were notices, which is why
online you mostly find the spelling “Notice: Undefined index”. The behaviour has
not changed: the expression evaluates to null and your script carries on.
And that is really the problem. The message is not the bug, it is the pointer to
it. The actual bug is that you go on calculating with null.
What you should not do
The first reflex is usually one of two routes — and both are wrong.
Do not switch the messages off:
<?php
error_reporting(E_ALL & ~E_WARNING); // bad idea
?>
That gets rid of every other warning too, and those you want to see.
Do not prefix an @:
<?php
$value = @$notSet;
?>
That only suppresses the output. The error itself still happens. And @ is
slow on top of that, because PHP still creates the error in full and only
discards it afterwards.
Just how little @ suppresses becomes apparent as soon as a custom error
handler is involved:
@$notYetSet : NULL (no output appeared)
custom handler sees : Undefined variable $alsoNotSet
error_reporting() there : 4437
So the handler is called despite the @. If you use set_error_handler()
anywhere — and every framework does — the suppressed error ends up in your log
unless you check for it yourself:
<?php
set_error_handler(function (int $errno, string $errstr) {
if (!(error_reporting() & $errno)) {
return false; // was suppressed with @
}
// log it here
});
?>
Note the number 4437 above. Before PHP 8, @ set the value of
error_reporting() to 0, and old code checks for error_reporting() === 0
accordingly. Since PHP 8 it instead sets the set of error classes that can
never be suppressed anyway — and that adds up to 4437, not 0. So the old test
no longer catches anything.
What to do instead
The most honest route is to initialise the variable:
<?php
$sum = 0;
foreach ($values as $value) {
$sum += $value;
}
?>
With data from outside — $_GET, $_POST, JSON, a database — you cannot do
that, because you do not decide what arrives. That is what the null coalescing
operator is for:
<?php
$port = $config['port'] ?? 3306;
?>
Since PHP 7 this is the standard route and in the vast majority of cases the right one.
The case where ?? does the wrong thing
Now for the point that is rarely mentioned. ?? and isset() do not check
“does the key exist”, they check “does it exist and is it not null”.
<?php
$a = ['key' => null];
?>
isset($a['key']) : false
array_key_exists('key', $a) : true
$a['key'] ?? 'fallback' : 'fallback'
The key is there. isset() still says false, and ?? returns the fallback.
Most of the time that is exactly what you want. Sometimes it is not — namely
when null carries a meaning of its own in your data. Take a database column:
“NULL” means “deliberately no value” there, and that is something else than
“column not queried”. Or JSON from an API in which {"discount": null} and a
missing discount mean different things.
For those cases:
<?php
if (array_key_exists('discount', $data)) {
// field was there, value may be null
}
?>
array_key_exists() only works on arrays, not on objects. For object
properties there is property_exists().
empty() is more generous still
empty() is a popular substitute for isset(). It checks something else,
though, namely “empty” on top of that:
empty(0 ) = true isset = true
empty('0' ) = true isset = true
empty('' ) = true isset = true
empty([] ) = true isset = true
empty(NULL ) = true isset = false
empty('text') = false isset = true
The line with '0' is the one that does damage in practice. A form field
containing 0 — a quantity, an ID, a postcode in a country whose postcodes
start with one — counts as empty to empty(). And values from $_POST are
always strings.
Rule of thumb: use empty() only when you genuinely mean “not set or empty”.
In every other case use isset() or ??.
Summary
- The message is the pointer, not the bug. The bug is the
nullyou keep calculating with afterwards. - Initialise variables wherever you can.
- Use
??for data from outside. - Use
array_key_exists()whennullis a meaningful value. empty()treats0and'0'as empty — dangerous with form data.@only suppresses the output; a custom error handler is still called.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).