JavaScript-style object literals in PHP

March 20th, 2011. Tagged: JavaScript, php

The object literal notation in JavaScript looks like:

var fido = {name: "Fido", barks: true};

or

var fido = {};
fido.name = "Fido";
fido.barks = true;

From assoc arrays to objects

In PHP you would call that an associative array.

$fido = array(
  'name' => "Fido",
  'barks' => true
);

And you can easily make it an object too:

$fido = (object)$fido;
echo gettype($fido); // "object"

Or if you want to start with a blank object and add stuff to it:

$fido = (object)array();

or

$fido = new StdClass();

and then

$fido->name = "Fido";
$fido->barks = true;

A little explanation maybe: objects in JavaScript are hashes, maps, whatever you decide to call them. Objects in PHP were an afterthought in the language and (at least initially) were not much more than "fancy arrays". Fancy associative arrays (hashes, maps, whatever you call them).

Objects in PHP need a class, but the new stdClass() lets you start quickly without the class {...} jazz. Same for casting an array (upgrading it in its fanciness) to an object with (object)array().

So far - so good. What about methods?

Methods anyone?

JavaScript doesn't care about properties versus methods. It's all members of an object (like elements of an assoc array). Only if a member happens to be a function, it's invokable.

fido.say = function () {
  if (this.barks) {
    return "Woof!";
  }
};
 
fido.say(); // "Woof!"

Turns out, since PHP 5.3 there are closures in PHP too. So you can do:

$fido->say = function() {
  if ($this->barks) {
    return "Woof";
  }
};

The difference is that $fido->say() won't work. Two reasons for that:

  1. say is not a method. It's a property. For PHP it matters. You can however assign the property say to a new variable $callme. This variable is now a closure object. As such you can invoke it:
    $callme = $fido->say;
    echo $callme();

    Note the $ in $callme().

  2. the above will also fail because $this is an weird context and doesn't point to the object $fido. But you can use $self and point it to the global object $fido.

So that's a little .... unpretty, but it works:

$fido = (object)array();
$fido->name = "Fido";
$fido->barks = true;
 
$fido->say = function() {
  $self =& $GLOBALS['fido'];
  if ($self->barks) {
    return "Woof";
  }
};
 
$callme = $fido->say;
echo $callme(); // "Woff!"

And a sprinkle of magic

We can make this prettier with the help of a little PHP magic. PHP has some magic methods going on and one of these is the __call() method. If you implement it in a class, then it will be invoked whenever someone tries to call a method that doesn't exist.

In our case $fido->say is not a method. So __call can intercept $fido->say() calls and invoke the $fido->say property as a closure object. Closures are callable and call_user_func() and call_user_func_array() work fine with them. So all in all we should make this work:

$fido = new JSObject();
$fido->name = "Fido";
$fido->barks = true;
 
$fido->say = function($self) {
  if ($self->barks) {
    return "Woof";
  }
};
 
echo $fido->say();

As you can see, very JavaScript-esque. Except that $this is $self and will always be the first argument passed to every method. The secret sauce to make this happen is the JSObject() class.

class JSObject {
  function __call($name, $args) {
    if (is_callable($this->$name)) {
      array_unshift($args, $this);
      return call_user_func_array($this->$name, $args);
    }
  }
}

Nice and easy. Namely:

  1. __call takes the name of the missing method and any arguments.
  2. It checks whether there's a callable property with the same name (a closure object property).
  3. It adds $this to the arguments list and calls the closure.

Yupee! Now you can haz moar class-less JS-like PHP objects 🙂

(Note that $this->$name is not a typo and should not be $this->name because it's a dynamic property name.)

And one more thing

If we add a constructor to JSObject, it can accept any properties at creation time. So you can be even closer to JavaScript and allow both creating an "empty" object and adding to it later, or creating an object and adding properties simultaneously.

The slightly modified JSObject:

class JSObject {
  function __construct($members = array()) {
    foreach ($members as $name => $value) {
      $this->$name = $value;
    }
  }
  function __call($name, $args) {
    if (is_callable($this->$name)) {
      array_unshift($args, $this);
      return call_user_func_array($this->$name, $args);
    }
  }
}

And example use:

$fido = new JSObject(array(
  'name' => "Fido",
  'barks'=> true,
  'say'  => function($self) {
    if ($self->barks) {
      return "Woof";
    }
  }
));
 
echo $fido->say(); // "Woff"

This is pretty close to what you can have in JavaScript (adding $ and ' even though we can do without them), only changing a few things like -> to . and => to :

$fido = {
  'name' : "Fido",
  'barks': true,
  'say'  : function() {
    if (this.barks) {
      return "Woof";
    }
  }
};
$fido.say(); // Woof

JS and PHP look like twins now don't they.

JS for PHP devs at confoo.ca

This was extracted from a talk I gave at the confoo.ca conference a week or so ago. Below are the slides:

JavaScript for PHP developers

View more presentations from Stoyan Stefanov

Comments? Feedback? Find me on Twitter, Mastodon, Bluesky, LinkedIn, Threads