4
\$\begingroup\$

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->assertEquals(
        $expectedProperties,
        $propertyNames
    );
}

Getters

I also want that all properties are available via getters:

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

    $this->assertEquals(
        'Simone',
        $dto->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' => 'Simone',
        'sfadfsa' => 'Simone',
    ));

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

    $this->assertEquals(
        $expectedProperties,
        $dto->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' => 'Simone',
        'surname' => 'Gentili',
        'sadfasdrname' => 'Gentili',
    ));

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

    $this->assertEquals(
        $dto->asArray(),
        $unserialized->asArray()
    );

    $this->assertEquals(
        array(
            'name' => 'Simone',
            'surname' => 'Gentili',
        ),
        $unserialized->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->properties = $properties;
    }

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

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

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

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

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

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

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

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

        return $properties;
    }

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

        return self::getPropertyNames();
    }

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

        return self::getPropertyNames();
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ you do know php5.3 is kind of no longer supported? It stopped getting security updates in 2014! php.net/supported-versions.php \$\endgroup\$ – Pinoniq Jun 12 '17 at 13:11
  • \$\begingroup\$ Yes I know. ... \$\endgroup\$ – sensorario Jun 12 '17 at 13:24
  • \$\begingroup\$ It's no longer supported by the PHP team, but Red Hat should still be somewhat supporting 5.3 with some security updates as part of the RHEL 6 operating system for another three years. \$\endgroup\$ – bdsl Oct 21 '17 at 22:27
1
\$\begingroup\$

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->name) while you get the internal array's key ($this->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->getName() / $person->setName('Simone') as well as $personName = $person->name / $person->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->assertEquals(
        $expectedProperties,
        $propertyNames
    );
}
public function testProvidePropertyViaGeneralGetter()
{
    $dto = Person::createFromArray(array(
        self::PERSON_NAME => 'Simone',
    ));

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

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

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

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

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

    $this->assertEquals(
        $dto->asArray(),
        $unserialized->asArray()
    );

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

Production code :

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

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

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

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

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

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

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

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

        $propertyNames = array();
        foreach($reflectedProperties as $reflectedProperty) {
            $propertyNames[] = $reflectedProperty->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->get($propertyName);
        }

        return $values;
    }

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

        foreach ($propertyNames as $propertyName) {
            $propertyValue = $this->get($propertyName);
            $this->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+.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.