Code for a Person DTO, with tests My needs are to manage DTO in a php5.3 application. I've written few lines of code in TDD with phpunit 6. The scenario I want to implement is a request with following fields:


name
surname
password
alias
email
address
civic_number
country


And I need to create DTO's with


credentials
info
address


Provide DTO properties

Each DTO should provide its own properties.

public function testProvideDtoPropertyNames()
{
    $propertyNames = Person::getPropertyNames();

    $expectedProperties = array(
        'name',
        'surname',
    );

    $this-&gt;assertEquals(
        $expectedProperties,
        $propertyNames
    );
}


Getters

I also want that all properties are available via getters:

public function testProvidePropertyViaGeneralGetter()
{
    $dto = Person::createFromArray(array(
        'name' =&gt; 'Simone',
    ));

    $this-&gt;assertEquals(
        'Simone',
        $dto-&gt;get('name')
    );
}


Filtering

The main purpose here is to filter all fields and just keep consideration of the DTO's properties:

public function testDTOAcceptOnlyItsOwnProperties()
{
    $dto = Person::createFromArray(array(
        'name' =&gt; 'Simone',
        'sfadfsa' =&gt; 'Simone',
    ));

    $expectedProperties = array(
        'name' =&gt; 'Simone',
        'surname' =&gt; null,
    );

    $this-&gt;assertEquals(
        $expectedProperties,
        $dto-&gt;asArray()
    );
}


Serialization

Serializing and un serializing I want that my code works fine and without side effect.

public function testSerializationKeepSameProperties()
{
    $dto = Person::createFromArray(array(
        'name' =&gt; 'Simone',
        'surname' =&gt; 'Gentili',
        'sadfasdrname' =&gt; 'Gentili',
    ));

    $serialized = serialize($dto);
    $unserialized = unserialize($serialized);

    $this-&gt;assertEquals(
        $dto-&gt;asArray(),
        $unserialized-&gt;asArray()
    );

    $this-&gt;assertEquals(
        array(
            'name' =&gt; 'Simone',
            'surname' =&gt; 'Gentili',
        ),
        $unserialized-&gt;asArray()
    );
}


Are those tests good? I hope yes but any suggestions are welcome.

Finally, ... the code:

A DTO can be easily done with a simple class with public properties:

class Person extends Dto
{
    public $name;
    public $surname;
}


Here the production code:

abstract class Dto
{
    private $properties;

    private function __construct(array $properties)
    {
        $this-&gt;properties = $properties;
    }

    public static function createFromArray($properties)
    {
        return new static($properties);
    }

    public function set($propertyName, $propertyValue)
    {
        $this-&gt;$propertyName = $propertyValue;
    }

    public function get($propertyName)
    {
        return isset($this-&gt;properties[$propertyName])
            ? $this-&gt;properties[$propertyName]
            : null;
    }

    public static function getPropertyNames()
    {
        $reflected = new ReflectionClass(new static(array()));

        $properties = $reflected-&gt;getProperties(
            ReflectionProperty::IS_PUBLIC
        );

        return array_map(function ($item) {
            return $item-&gt;getName();
        }, $properties);
    }

    public function asArray()
    {
        $properties = array();

        foreach (static::getPropertyNames() as $itemValue) {
            $properties[$itemValue] = $this-&gt;get($itemValue);
        }

        return $properties;
    }

    public function __sleep()
    {
        foreach (self::getPropertyNames() as $propertyName) {
            $this-&gt;set(
                $propertyName,
                $this-&gt;get($propertyName)
            );
        }

        return self::getPropertyNames();
    }

    public function __wakeup()
    {
        foreach (self::getPropertyNames() as $propertyName) {
            $this-&gt;properties[$propertyName] = $this-&gt;$propertyName;
            $this-&gt;$propertyName = null;
        }

        return self::getPropertyNames();
    }
}
In the tests (and anywhere else), you should use constants to ensure correct spelling of the properties :

const PERSON_NAME = 'name';
const PERSON_SURNAME = 'surname';
...
$expectedProperties = array(self::PERSON_NAME,self::PERSON_SURNAME);


Then the Filtering and Serialization tests are very similar while they should be atomic (one test tests one thing) : you should not test Filtering whike testing Serialization.

In the production code there is one error : you set the desired property (i.e. $this-&gt;name) while you get the internal array's key ($this-&gt;properties['name']). To keep the simplicity of defining an object as a class with public properties you should drop the internal array. This will also make the __sleep and __wakeup calls much clearer.

Also a design flaw is to use the Reflection : it seems unavoidable but depending on your usage there might be performance issues. Using a caching system such as a static array will help. In particular if you look at the __wakeup and __sleep function you will notice a repeted call to the getPropertyNames method.

Finally what might be problematic is that you don't check if the property exists when setting or getting. If it does not you should throw an exception. There are magic methods in PHP5.3 (__get, __set and __call) ; you may do better using them. This way you will be able to use $personName = $person-&gt;getName() / $person-&gt;setName('Simone') as well as $personName = $person-&gt;name / $person-&gt;name = 'Simone'

Tests :

const PERSON_NAME = 'name';
const PERSON_SURNAME = 'surname';

public function testProvideDtoPropertyNames()
{
    $propertyNames = Person::getPropertyNames();

    $expectedProperties = array(
        self::PERSON_NAME,
        self::PERSON_SURNAME,
    );

    $this-&gt;assertEquals(
        $expectedProperties,
        $propertyNames
    );
}
public function testProvidePropertyViaGeneralGetter()
{
    $dto = Person::createFromArray(array(
        self::PERSON_NAME =&gt; 'Simone',
    ));

    $this-&gt;assertEquals(
        'Simone',
        $dto-&gt;getName()
    );
}
public function testDTOAcceptOnlyItsOwnProperties()
{
    $dto = Person::createFromArray(array(
        self::PERSON_NAME =&gt; 'Simone',
        'non existent property' =&gt; 'Simone',
    ));

    $expectedProperties = array(
        self::PERSON_NAME =&gt; 'Simone',
        self::PERSON_SURNAME =&gt; null,
    );

    $this-&gt;assertEquals(
        $expectedProperties,
        $dto-&gt;asArray()
    );
}
public function testSerializationKeepSameProperties()
{
    $properties = array(
        self::PERSON_NAME =&gt; 'Simone',
        self::PERSON_SURNAME =&gt; null,
    );

    $dto = Person::createFromArray($properties);

    $serialized = serialize($dto);
    $unserialized = unserialize($serialized);

    $this-&gt;assertEquals(
        $dto-&gt;asArray(),
        $unserialized-&gt;asArray()
    );

    $this-&gt;assertEquals(
        $properties,
        $unserialized-&gt;asArray()
    );
}


Production code :

class DtoException extends Exception
{
}
abstract class Dto
{
    private static $propertyNamesByClass = array();

    private function __construct(array $properties)
    {
        foreach($properties as $propertyName=&gt;$propertyValue)
            if(Static::hasPropertyName($propertyName))
                $this-&gt;set($propertyName,$propertyValue);
    }
    public function __call($method, array $arguments)
    {
        $getOrSet = substr($method, 0, 3);
        if($getOrSet != 'get' &amp;&amp; $getOrSet != 'set')
            throw new DtoException('"'.get_class($this).'" has no method "'.$method.'"');

        $propertyName = strtolower(substr($method, 3));
        if(!Static::hasPropertyName($propertyName))
            throw new DtoException('"'.get_class($this).'" has no property "'.$propertyName.'"');

        $getOrSetMethod = array($this,$getOrSet);
        array_unshift($arguments,$propertyName);
        return call_user_func_array($getOrSetMethod, $arguments);
    }
    public function __get($propertyName)
    {
        if(!Static::hasPropertyName($propertyName))
            throw new DtoException('"'.get_class($this).'" has no property "'.$propertyName.'"');

        return $this-&gt;get($propertyName);
    }
    public function __set($propertyName, $propertyValue)
    {
        if(!Static::hasPropertyName($propertyName))
            throw new DtoException('"'.get_class($this).'" has no property "'.$propertyName.'"');

        return $this-&gt;set($propertyName, $propertyValue);
    }

    public static function createFromArray($properties)
    {
        return new Static($properties);
    }

    public function set($propertyName, $propertyValue)
    {
        $this-&gt;$propertyName = $propertyValue;
    }

    public function get($propertyName)
    {
        return $this-&gt;$propertyName;
    }

    public static function getPropertyNames()
    {
        $className = get_called_class();
        if(isset(self::$propertyNamesByClass[$className]))
            return self::$propertyNamesByClass[$className];

        $reflected = new ReflectionClass($className);
        $reflectedProperties = $reflected-&gt;getProperties(
            ReflectionProperty::IS_PUBLIC
        );

        $propertyNames = array();
        foreach($reflectedProperties as $reflectedProperty) {
            $propertyNames[] = $reflectedProperty-&gt;getName();
        }

        self::$propertyNamesByClass[$className] = $propertyNames;
        return $propertyNames;
    }
    public static function hasPropertyName($propertyName)
    {
        $propertyNames = Static::getPropertyNames();
        return in_array($propertyName,$propertyNames);
    }

    public function asArray()
    {
        $values = array();

        foreach (Static::getPropertyNames() as $propertyName) {
            $values[$propertyName] = $this-&gt;get($propertyName);
        }

        return $values;
    }

    public function __sleep()
    {
        $propertyNames = self::getPropertyNames();

        foreach ($propertyNames as $propertyName) {
            $propertyValue = $this-&gt;get($propertyName);
            $this-&gt;set($propertyName, $propertyValue);
        }

        return $propertyNames;
    }

    public function __wakeup()
    {
        $propertyNames = self::getPropertyNames();

        return $propertyNames;
    }
}


I tested it on PHP5.3 and it works, however I have to tell you that it does not work on PHP5.2- or PHP7+.