PHP: What is stdClass?

stdClass is an empty class. No properties, no methods, no constructor. You usually meet it without having asked for it — as the return value of json_decode(). And then you have a few questions.

What it is

new stdClass()  : stdClass
methods         : []
parent class    : false

That is all. A class with no content that you can fill with properties.

And let us clear up the most common misunderstanding right away: stdClass is not the base class of all PHP objects. The name suggests it — “standard class” — but it is not true:

is stdClass a parent of ArrayObject? false

Unlike Java or C#, PHP has no common root class. If you want to check whether something is an object, use is_object() and not instanceof stdClass.

Where you get it from

Three routes, and you rarely chose the first one yourself:

<?php
  $a = json_decode('{"host":"localhost"}');   // stdClass
  $b = (object) ['host' => 'localhost'];      // stdClass
  $c = new stdClass();
  $c->host = 'localhost';
?>

With json_decode() it is the default. With true as the second parameter you get arrays instead — and that is usually the better choice, more on that shortly.

The business with dynamic properties

Since PHP 8.2 it has been deprecated to write properties onto an object that are not declared in the class. stdClass is exempt from this, though:

own class : Deprecated: Creation of dynamic property Eigene::$neu is deprecated
stdClass  : no message

That is why the change does not affect code working with json_decode() results, but does affect your own hand-written data container classes. For those you then need either the #[AllowDynamicProperties] attribute or — better — simply declare the properties.

In PHP 9 the deprecation becomes an error. stdClass stays exempt.

The trap with property names

And now the point I consider the most important in this article. An array can have keys that do not work as property names:

<?php
  $obj = (object) ['0' => 'null', 'mit leer' => 'x', 'ok' => 'y'];
?>

The properties are all there:

array keys : [0,"mit leer","ok"]

$obj->ok           : 'y'
$obj->{0}          : 'null'
$obj->{"0"}        : 'null'
$obj->{"mit leer"} : 'x'

But $obj->0 is a syntax error. So is $obj->mit leer. You can only reach them with curly braces — and that is something you have to think of first.

This is the reason to use json_decode($json, true) whenever the keys come from somebody else’s hand. With an array every key is reachable, no matter what it looks like:

<?php
  $data = json_decode($json, true);
  echo $data['mit leer'];   // no problem
?>

Up to PHP 7.2, incidentally, these properties were not reachable at all. Since then they at least work with the curly braces.

There and back

(array) $obj keys    : [0,"mit leer","ok"]
get_object_vars keys : [0,"mit leer","ok"]

For stdClass both return the same thing, because there is no visibility. For your own class with private or protected it looks different — there is a separate article about that.

One thing to keep in mind: the cast is not recursive, json_decode() is.

(object) on a nested array : ->innen is array
json_decode                : ->innen is stdClass

Missing properties

$k->fehlt          : NULL   (Warning: Undefined property)
$k->fehlt ?? 'std' : 'std'
isset($k->fehlt)   : false
property_exists    : false

As with arrays: ?? for the default value. property_exists() is the counterpart to array_key_exists() and the right test when null is a meaningful value.

Comparing and copying

Two things that behave differently for objects than for arrays.

First, comparison:

$p == $q  : true    (same content)
$p === $q : false   (not the same instance)
$p === $r : true

For objects === checks identity — are these two names for the same object? For arrays, by contrast, === compares the content. Anyone who confuses the two writes a comparison that always returns false.

Second, passing:

after aendere($z)                    : a = 99   <- changed, without &
reset to 1, then ersetze()           : a = 1    <- unchanged
<?php
  function aendere(stdClass $o): void { $o->a = 99; }   // has an outside effect
  function ersetze(stdClass $o): void { $o = (object) ['a' => 42]; } // does not
?>

This is often described as “objects are passed by reference”, and that is not quite right. The variable holds a handle to the object but is not itself a reference. That is why changing a property has an effect outside, while reassigning the parameter does not.

And third, copying:

after clone and modification : original n = 99   <- changed too
via serialize                : original n = 1

clone is shallow. Nested objects are not copied along but shared. If you really need a deep copy, unserialize(serialize($obj)) does it — or you write a __clone() that clones the inner objects itself.

When to use it and when not

stdClass is good for what it is meant for: a data container without behaviour whose structure you do not know in advance. A decoded API response, for instance.

As soon as the structure is fixed, your own class is the better choice:

<?php
class Config {
  public function __construct(
    public readonly string $host,
    public readonly int $port,
  ) {}
}
?>

I measured what that buys you — and learned something in the process. When reading a typo, both behave identically:

Konfig::$tipfehler   : Warning: Undefined property
stdClass::$tipfehler : Warning: Undefined property

So the typed class does not help here. The difference only shows up when writing:

stdClass : silently created
Konfig   : Deprecated: Creation of dynamic property

That is a more precise statement than the usual “typed classes catch typos”. They catch them on assignment, not on reading. In PHP 9 the deprecation becomes an error, at which point the difference is starker.

What your own class buys you beyond that: types that are actually enforced, a constructor that requires the mandatory fields, readonly, and one place where you can look up which fields exist at all. The last of those is the biggest win day to day.

Summary

  • stdClass is an empty class — and not the base class of all objects.
  • You get it from json_decode() without true and from the (object) cast.
  • It is exempt from the 8.2 deprecation for dynamic properties; your own classes are not.
  • Keys like 0 or 'mit leer' become properties reachable only via $obj->{...} — for foreign data prefer json_decode($j, true).
  • For objects === checks identity, not content.
  • clone is shallow.
  • Your own class catches typos on writing, not on reading.

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, ...).