Friday 26 September 2014

Looking at PHP's OOP from a CFMLer's perspective: object serialisation

G'day:
This continues my series which is thusfar:


The next section in the PHP OO docs is actually "Magic Methods" (oh please), but two of these are __sleep() and __wakeup(), and these relate to object serialisation, so I decided to have a look at how this works, and __sleep() and __wakeup() at the same time.



Firstly, here's a quick CFML example (this will run only on Railo, not ColdFusion, as it leverages stuff only Railo have thusfar implemented):

// Movie.cfc

component {

    public Movie function init(required title, required year, required Person director) {
        variables.title   = title
        variables.year    = year
        variables.director= director

        return this
    }

    public function get() {
        return [
            title   = title,
            year    = year,
            director= director.getFullName()
        ]
    }

    public function getDirector(){
        return director
    }

}

// Person.cfc
component {

    public Person function init(required firstName, required lastName){
        variables.firstName= firstName
        variables.lastName = lastName
        return this
    }

    public function getFullName()  {
        return "#variables.firstName# #variables.lastName#"
    }

}

<cfscript>
// movie.cfm

movie = new Movie("Once Were Warriors", 1994, new Person("Lee", "Tamahori"))

serialisedMovie = serialize(movie)
dump(serialisedMovie)
echo("<hr>")

deserialisedMovie = evaluate(serialisedMovie)

dump(var=deserialisedMovie.get(), label=getMetadata(deserialisedMovie).fullName)

director = deserialisedMovie.getDirector()
dump(var=director.getFullName(), label=getMetadata(director).fullName)
</cfscript>


Here we have a simple Movie class which has title, year and director, with the director being an instance of another class Person. I use Railo's serialize() function to serialise the movie, have a look at what it outputs, and then deserialise it again using - of all things - evaluate(). I then dump out the results of the deserialisation to check it's actually worked properly.

Here's the output:

stringevaluateComponent('scribble.shared.scratch.php.www.experiment.oo.serialisation.Movie','62aa1eb53f27541110d76b787d585ef6',{},{'DIRECTOR':evaluateComponent('scribble.shared.scratch.php.www.experiment.oo.serialisation.Person','b459ac28aa7dd48158ddfb3466213bfe',{},{'LASTNAME':'Tamahori','FIRSTNAME':'Lee'}),'YEAR':1994,'TITLE':'Once Were Warriors'})

scribble.shared.scratch.php.www.experiment.oo.serialisation.Movie
Array
1
stringOnce Were Warriors
2
number1994
3
stringLee Tamahori
scribble.shared.scratch.php.www.experiment.oo.serialisation.Person
stringLee Tamahori

Excellent.

OK, now for PHP. Here's code roughly analogous to the CFML code above:

<?php
// Movie.class.php

class Movie
{
    use Message;

    private $title;
    private $year;
    private $director;

    public function __construct($title, $year, $director)
    {
        $this->title = $title;
        $this->year = $year;
        $this->director = $director;
    }

    public function get()
    {
        return [
            "title" => $this->title,
            "year"  => $this->year,
            "director" => $this->director->getFullName()
        ];
    }

}


<?php
// Person.class.php

class Person
{
    private $firstName;
    private $lastName;

    public function __construct($firstName, $lastName)
    {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function getFullName()
    {
        return "$this->firstName $this->lastName";
    }

}


<?php
// movie.php

require_once "./app.php";

$movie = new Movie("Once Were Warriors", 1994, new Person("Lee", "Tamahori"));

$serialisedMovie = serialize($movie);


echo "<pre>$serialisedMovie</pre><hr>";

$deserialisedMovie = unserialize($serialisedMovie);

new dBug([get_class($deserialisedMovie), $deserialisedMovie->get()]);

And the result is equivalent too:

O:5:"Movie":3:{s:12:"Movietitle";s:18:"Once Were Warriors";s:11:"Movieyear";i:1994;s:15:"Moviedirector";O:6:"Person":2:{s:17:"PersonfirstName";s:3:"Lee";s:16:"PersonlastName";s:8:"Tamahori";}}

[get_class($deserialisedMovie), $deserialisedMovie->get()] (array)
0Movie
1
array
titleOnce Were Warriors
year1994
directorLee Tamahori

Nice one.

__sleep() / __wakeup()

What about these __sleep() and __wakeup() methods? They don't actually play much of a part in the serialisation - which clearly works fine without them - but are just basically event handlers. If you need to do anything before serialising, or before deserialising. The docs use examples of, say, committing data before serialising, or reestablishing DB connections etc before deserialising.

Here I've updated Movie and Person to have stub methods which simply report they were called:

<?php
// Movie.class.php

class Movie
{
    // ... snipped for clarity ...

    public function __sleep()
    {
        SELF::message(__CLASS__, __FUNCTION__, func_get_args());
        return ["title", "year", "director"];
    }

    public function __wakeup()
    {
        SELF::message(__CLASS__, __FUNCTION__, func_get_args());
    }

}

<?php
// Person.class.php

class Person
{
    // ... snipped for clarity ...

    public function __sleep()
    {
        SELF::message(__CLASS__, __FUNCTION__, func_get_args());
        return ["firstName", "lastName"];
    }

    public function __wakeup()
    {
        SELF::message(__CLASS__, __FUNCTION__, func_get_args());
    }

}

Output:

Movie->__sleep() called with arguments:
[]


Person->__sleep() called with arguments:
[]


O:5:"Movie":3:{s:12:"Movietitle";s:18:"Once Were Warriors";s:11:"Movieyear";i:1994;s:15:"Moviedirector";O:6:"Person":2:{s:17:"PersonfirstName";s:3:"Lee";s:16:"PersonlastName";s:8:"Tamahori";}}

Person->__wakeup() called with arguments:
[]


Movie->__wakeup() called with arguments:
[]


[get_class($deserialisedMovie), $deserialisedMovie->get()] (array)
0Movie
1
array
titleOnce Were Warriors
year1994
directorLee Tamahori

You can see that the __sleep() handler from both Movie and Person were called during the serialisation process, and __wakeup() for both called during deserialisation.

Cool.

Note how I'm returning a list of properties from __sleep():

return ["title", "year", "director"];

That's the list of properties that will be included in the serialisation. I'm just gonna change that to:

return ["title", "year"];

And run this test instead:

<?php
// movieWithoutDirector.php

require_once "./app.php";

$movie = new Movie("Once Were Warriors", 1994, new Person("Lee", "Tamahori"));

$serialisedMovie = serialize($movie);
echo "<pre>$serialisedMovie</pre><hr>";

$deserialisedMovie = unserialize($serialisedMovie);
echo "<pre>";
var_dump($deserialisedMovie);
echo "</pre>";

And the output:

Movie->__sleep() called with arguments:
[]


O:5:"Movie":2:{s:12:"Movietitle";s:18:"Once Were Warriors";s:11:"Movieyear";i:1994;}

Movie->__wakeup() called with arguments:
[]


object(Movie)#4 (3) {
  ["title":"Movie":private]=>
  string(18) "Once Were Warriors"
  ["year":"Movie":private]=>
  int(1994)
  ["director":"Movie":private]=>
  NULL
}

Here the director property has been completely ignored from the serialisation process, and accordingly when it was deserialised the director is null.

Serializable

There is another approach one can take with this. PHP has a Serializable interface, which requires two methods being implemented: serialize() and unserialize(). In this case, when an object is (de)serialised, these methods are called instead.

Here's an updated version of the Movie and Person classes:

<?php
// Movie.class.php

class Movie implements Serializable
{
    // ... snipped for clarity ...

    public function serialize()
    {
        SELF::message(__CLASS__, __FUNCTION__, func_get_args());
        $arrayToSerialise = $this->get();
        return json_encode($arrayToSerialise);
    }

    public function unserialize($serialized)
    {
        SELF::message(__CLASS__, __FUNCTION__, func_get_args());
        $deserialisedArray = json_decode($serialized);

        $this->title = $deserialisedArray->title;
        $this->year = $deserialisedArray->year;

        $director = Person::unpackFullName($deserialisedArray->director);
        $this->director = new Person($director["firstName"], $director["lastName"]);
    }
 
    // ... snipped for clarity ...

}


<?php
// Person.class.php

class Person
{
    // ... snipped for clarity ...

    public static function unpackFullName($fullName)
    {
        $asArray = explode(" ", $fullName);
        return [
            "firstName" => $asArray[0],
            "lastName" => $asArray[1]
        ];
    }

}

There's nothing particularly gripping here (and note the added method to Person is just to assist unserialize() from Movie), I simply use JSON as the serialisation method. Here's the output:

Movie->serialize() called with arguments:
[]


C:5:"Movie":68:{{"title":"Once Were Warriors","year":1994,"director":"Lee Tamahori"}}

Movie->unserialize() called with arguments:
["{\"title\":\"Once Were Warriors\",\"year\":1994,\"director\":\"Lee Tamahori\"}"]


[get_class($deserialisedMovie), $deserialisedMovie->get()] (array)
0Movie
1
array
titleOnce Were Warriors
year1994
directorLee Tamahori


Note that __sleep() and __wakeup() are not called when using Serialisable (this is documented, and by design).

That's about it, as far as my findings go with PHP's object serialisation. Other than some dodgy function names, the implementation seems fine to me.

--
Adam