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 null you keep calculating with afterwards.
  • Initialise variables wherever you can.
  • Use ?? for data from outside.
  • Use array_key_exists() when null is a meaningful value.
  • empty() treats 0 and '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, ...).