PHP: How do you convert an object into an array?

The cast (array) $object is the answer you find everywhere. With a stdClass it does work. With a class of your own it gives you keys you can no longer address.

The simple case

A stdClass, as returned by json_decode(), can be cast directly:

<?php
  $obj = new stdClass();
  $obj->host = 'localhost';
  $obj->port = 3306;

  print_r((array) $obj);
?>
Array
(
    [host] => localhost
    [port] => 3306
)

As long as all properties are public, that is the end of it.

With your own classes it gets ugly

Now the same thing with a real class:

<?php
class Server {
  public string  $host     = 'localhost';
  protected int  $port     = 3306;
  private string $password = 'secret';
}

$arr = (array) new Server();
?>

The array has three entries. They are just not named the way you would think:

host                (length 4)
\0*\0port           (length 7)
\0Server\0password  (length 16)

Those are NUL bytes in the keys. The scheme is:

  • protected\0 + * + \0 + name
  • private\0 + class name + \0 + name

The practical consequence:

isset($arr['port'])      : false
isset($arr["\0*\0port"]) : true

So you can only get at the value if you rebuild the NUL-byte key exactly. And for that you need to know which class declared the property — with inheritance that is not necessarily the object’s own class.

The display is particularly annoying. print_r() renders the key like this:

[port:protected] => 3306

That looks as if the key were port, with a piece of extra information appended. It is not. print_r() interprets the NUL-byte scheme for you and turns it into something readable. Anyone who then reaches for $arr['port'] will be searching for a while.

Incidentally, the cast is not the only place where visibility interferes. json_encode() only takes the public properties along:

json_encode($server) : {"host":"localhost"}

serialize() does take all of them, but uses the same NUL-byte names for it:

O:6:"Server":3:{s:4:"host";...s:7:"\0*\0port";...s:16:"\0Server\0password";...}

The s:7: and s:16: are the length markers — the same numbers as above.

get_object_vars() — and where you call it

get_object_vars() returns the properties with clean keys. Which ones you see, however, depends on where you call the function from:

from outside : ["host"]
from inside  : ["host","port","password"]

From outside the class you only see public. Inside the class you see everything — and get normal keys without NUL bytes.

That points the way to the clean solution: you put the conversion inside the class.

<?php
class Server {
  public string  $host     = 'localhost';
  protected int  $port     = 3306;
  private string $password = 'secret';

  public function toArray(): array {
    return get_object_vars($this);
  }
}
?>

It also has the advantage that you decide what goes out. The password is probably not something you want in that array at all.

The cast is not recursive

The next stumbling block. With nested objects the cast only converts the topmost level:

(array) $outer : ['inner'] is stdClass  <- still an object
via JSON       : ['inner'] is array

That is why you find this detour recommended everywhere:

<?php
  $arr = json_decode(json_encode($obj), true);
?>

It does in fact work recursively. json_decode() with true as its second parameter returns arrays instead of stdClass throughout:

json_decode($json)        : stdClass
->db is                   : stdClass
json_decode($json, true)  : array
['db'] is                 : array

But the JSON detour costs you something

Namely type information. The round trip through JSON only knows the types JSON knows:

1.0 becomes          : 1 (int)
PHP_INT_MAX survives : true
DateTimeImmutable    : {"date":"2026-08-28 00:00:00.000000","timezone_type":3,"timezone":"UTC"}

The first line is the one you meet in practice. A price of 19.0 comes back as int 19, because JSON does not distinguish between 19 and 19.0. As long as you only do arithmetic with it, that goes fine. With a === comparison or a function signature typed float it shows up.

And every object that is more than a plain data container loses its type. The DateTimeImmutable turns into an array of three fields. That cannot be converted back.

One thing that does work, by the way: PHP_INT_MAX survives the detour correctly. That is occasionally claimed otherwise.

What I recommend

  • stdClass from json_decode(): pass true as the second parameter right away, then there is nothing to convert.
  • Your own class: write a toArray() method. Ten lines, but predictable.
  • A foreign class you cannot change: get_object_vars() if the public properties are enough for you. Otherwise the Reflection API.
  • The JSON detour only if you do not care about types.

Summary

  • (array) only works smoothly for stdClass or purely public classes.
  • protected becomes "\0*\0name", private becomes "\0Class\0name".
  • print_r() shows [port:protected] — that is not the key name.
  • get_object_vars() sees everything from inside, only public from outside.
  • The cast is not recursive, json_decode(..., true) is.
  • The JSON detour turns 1.0 into an int and objects into arrays.

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