Skip to content

Commit

Permalink
limit serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
juliangut committed Mar 6, 2020
1 parent 3b8b9ee commit 5dee1e3
Show file tree
Hide file tree
Showing 11 changed files with 111 additions and 46 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class MyDTO implements DTO, MyDTOInterface

If you just need a plain DTO object it gets a lot easier, this boilerplate code is already in place for you by extending `Gears\DTO\AbstractDTO` or `Gears\DTO\AbstractScalarDTO` classes

Constructors are declared protected forcing you to create "named constructors", this have a very useful side effect, you get to type-hint all your DTO parameters
Protected constructors force you to create "named constructors", this has a very useful side effect, you get to type-hint all your DTO parameters

```php
use Gears\DTO\AbstractScalarDTO;
Expand Down Expand Up @@ -109,11 +109,11 @@ class MyDTO extends AbstractScalarDTO
}
```

The difference between `Gears\DTO\AbstractDTO` and `Gears\DTO\AbstractScalarDTO` is that the later ensures all payload is either a scalar value (null, string, int, float or bool) or an array of scalar values. It's purpose is to ensure the DTO can be securely serialized, it is the perfect match to create Domain Events, or CQRS Command/Query. Other than that both classes are exactly the same
The difference between `Gears\DTO\AbstractDTO` and `Gears\DTO\AbstractScalarDTO` is that the later ensures all payload is either a scalar value (null, string, int, float or bool) or an array of scalar values. Its purpose is to ensure the object can be securely serialized, it is the perfect match to create Domain Events, or CQRS Commands/Queries

Finally `Gears\DTO\AbstractDTOCollection` is a special type of DTO that only accepts a list of elements, being this elements implementations of DTO interface itself. This object is meant to be used as a return value when several DTOs should be returned, for example from a DDBB request
Finally `Gears\DTO\AbstractDTOCollection` is a special type of DTO that only accepts a list of elements, being these elements implementations of DTO interface itself. This object is meant to be used as a return value when several DTOs should be returned, for example from a DDBB query result

You can take advantage of magic method __call on DTO objects to access parameters. If you plan to use this feature it's best to annotate this magic accessors at class level with `@method` phpDoc tag, this will help you're IDE auto-completion
You can take advantage of magic method __call on DTO objects to access parameters. If you plan to use this feature it's best to annotate this magic accessors at class level with `@method` phpDoc tag, this will help your IDE auto-completion

## Contributing

Expand Down
15 changes: 7 additions & 8 deletions src/AbstractDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Gears\DTO;

use Gears\DTO\Exception\DTOException;
use Gears\Immutability\ImmutabilityBehaviour;

/**
Expand All @@ -37,24 +38,24 @@ final protected function __construct(array $parameters)
}

/**
* @return mixed[]
* @return string[]
*/
final public function __sleep(): array
{
return ['payload'];
throw new DTOException(\sprintf('DTO "%s" cannot be serialized', static::class));
}

final public function __wakeup(): void
{
$this->assertImmutable();
throw new DTOException(\sprintf('DTO "%s" cannot be unserialized', static::class));
}

/**
* @return array<string, mixed>
*/
final public function __serialize(): array
{
return ['payload' => $this->payload];
throw new DTOException(\sprintf('DTO "%s" cannot be serialized', static::class));
}

/**
Expand All @@ -64,9 +65,7 @@ final public function __serialize(): array
*/
final public function __unserialize(array $data): void
{
$this->assertImmutable();

$this->setPayload($data['payload']);
throw new DTOException(\sprintf('DTO "%s" cannot be unserialized', static::class));
}

/**
Expand All @@ -76,6 +75,6 @@ final public function __unserialize(array $data): void
*/
final protected function getAllowedInterfaces(): array
{
return [DTO::class, \Serializable::class];
return [DTO::class];
}
}
15 changes: 7 additions & 8 deletions src/AbstractDTOCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Gears\DTO;

use Gears\DTO\Exception\DTOException;
use Gears\DTO\Exception\InvalidCollectionTypeException;
use Gears\DTO\Exception\InvalidParameterException;
use Gears\Immutability\ImmutabilityBehaviour;
Expand Down Expand Up @@ -66,24 +67,24 @@ final public function getIterator(): \Traversable
}

/**
* @return mixed[]
* @return string[]
*/
final public function __sleep(): array
{
return ['payload'];
throw new DTOException(\sprintf('DTO collection "%s" cannot be serialized', static::class));
}

final public function __wakeup(): void
{
$this->assertImmutable();
throw new DTOException(\sprintf('DTO collection "%s" cannot be unserialized', static::class));
}

/**
* @return array<string, mixed>
*/
final public function __serialize(): array
{
return ['payload' => $this->payload];
throw new DTOException(\sprintf('DTO collection "%s" cannot be serialized', static::class));
}

/**
Expand All @@ -93,9 +94,7 @@ final public function __serialize(): array
*/
final public function __unserialize(array $data): void
{
$this->assertImmutable();

$this->setPayload($data['payload']);
throw new DTOException(\sprintf('DTO collection "%s" cannot be unserialized', static::class));
}

/**
Expand Down Expand Up @@ -158,6 +157,6 @@ abstract protected function getAllowedType(): string;
*/
final protected function getAllowedInterfaces(): array
{
return [DTOCollection::class, \Serializable::class];
return [DTOCollection::class];
}
}
39 changes: 23 additions & 16 deletions src/AbstractScalarDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/**
* Abstract immutable and only scalar values Data Transfer Object.
*/
abstract class AbstractScalarDTO implements DTO
abstract class AbstractScalarDTO implements DTO, \Serializable
{
use ImmutabilityBehaviour, ScalarPayloadBehaviour {
ScalarPayloadBehaviour::__call insteadof ImmutabilityBehaviour;
Expand All @@ -39,54 +39,61 @@ final protected function __construct(array $parameters)
}

/**
* @return mixed[]
* @return array<string, mixed>
*/
final public function __sleep(): array
final public function __serialize(): array
{
return ['payload'];
return ['payload' => $this->payload];
}

final public function __wakeup(): void
/**
* @param array<string, mixed> $data
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
final public function __unserialize(array $data): void
{
$this->assertImmutable();

$this->setPayload($data['payload']);
}

/**
* @return array<string, mixed>
* {@inheritdoc}
*/
final public function __serialize(): array
final public function serialize(): string
{
return ['payload' => $this->payload];
return \serialize($this->payload);
}

/**
* @param array<string, mixed> $data
* {@inheritdoc}
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @param mixed $serialized
*/
final public function __unserialize(array $data): void
final public function unserialize($serialized): void
{
$this->assertImmutable();

$this->setPayload($data['payload']);
$this->payload = \unserialize($serialized, ['allowed_classes' => false]);
}

/**
* {@inheritdoc}
*
* @param mixed $value
*/
final protected function checkParameterType(&$value): void
final protected function checkParameterType($value): void
{
if ($value instanceof DTOCollection) {
$value = \iterator_to_array($value->getElements());
if ($value instanceof DTO) {
$value = $value->getPayload();
}

if (\is_array($value)) {
foreach ($value as $val) {
$this->checkParameterType($val);
}
} elseif ($value !== null && !\is_scalar($value) && !$value instanceof self) {
} elseif ($value !== null && !\is_scalar($value)) {
throw new InvalidScalarParameterException(\sprintf(
'Class "%s" can only accept scalar payload parameters, "%s" given',
self::class,
Expand Down
21 changes: 21 additions & 0 deletions src/Exception/DTOException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* dto (https://github.com/phpgears/dto).
* General purpose immutable Data Transfer Objects for PHP.
*
* @license MIT
* @link https://github.com/phpgears/dto
* @author Julián Gutiérrez <juliangut@gmail.com>
*/

declare(strict_types=1);

namespace Gears\DTO\Exception;

/**
* DTOException class.
*/
class DTOException extends \LogicException
{
}
2 changes: 1 addition & 1 deletion src/Exception/InvalidCollectionTypeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
/**
* InvalidCollectionTypeException class.
*/
class InvalidCollectionTypeException extends \RuntimeException
class InvalidCollectionTypeException extends DTOException
{
}
2 changes: 1 addition & 1 deletion src/Exception/InvalidMethodCallException.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
/**
* InvalidMethodCallException class.
*/
class InvalidMethodCallException extends \RuntimeException
class InvalidMethodCallException extends DTOException
{
}
2 changes: 1 addition & 1 deletion src/Exception/InvalidParameterException.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
/**
* InvalidParameterException class.
*/
class InvalidParameterException extends \RuntimeException
class InvalidParameterException extends DTOException
{
}
21 changes: 21 additions & 0 deletions tests/DTO/AbstractDTOCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Gears\DTO\Tests;

use Gears\DTO\Exception\DTOException;
use Gears\DTO\Exception\InvalidCollectionTypeException;
use Gears\DTO\Exception\InvalidParameterException;
use Gears\DTO\Tests\Stub\AbstractDTOCollectionInvalidTypeStub;
Expand Down Expand Up @@ -57,4 +58,24 @@ public function testCreation(): void
static::assertSame($elements, \iterator_to_array($stub->getElements()));
static::assertSame($elements, \iterator_to_array($stub->getIterator()));
}

public function testNoSerialization(): void
{
$this->expectException(DTOException::class);
$this->expectExceptionMessage(
'DTO collection "Gears\DTO\Tests\Stub\AbstractDTOCollectionStub" cannot be serialized'
);

\serialize(AbstractDTOCollectionStub::fromElements([]));
}

public function testNoDeserialization(): void
{
$this->expectException(DTOException::class);
$this->expectExceptionMessage(
'DTO collection "Gears\DTO\Tests\Stub\AbstractDTOCollectionStub" cannot be unserialized'
);

\unserialize('O:46:"Gears\DTO\Tests\Stub\AbstractDTOCollectionStub":0:{}');
}
}
17 changes: 17 additions & 0 deletions tests/DTO/AbstractDTOTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Gears\DTO\Tests;

use Gears\DTO\Exception\DTOException;
use Gears\DTO\Tests\Stub\AbstractDTOStub;
use PHPUnit\Framework\TestCase;

Expand All @@ -37,4 +38,20 @@ public function testAcceptDTO(): void

static::assertInstanceOf(AbstractDTOStub::class, $stub->getObject());
}

public function testNoSerialization(): void
{
$this->expectException(DTOException::class);
$this->expectExceptionMessage('DTO "Gears\DTO\Tests\Stub\AbstractDTOStub" cannot be serialized');

\serialize(AbstractDTOStub::fromArray([]));
}

public function testNoDeserialization(): void
{
$this->expectException(DTOException::class);
$this->expectExceptionMessage('DTO "Gears\DTO\Tests\Stub\AbstractDTOStub" cannot be unserialized');

\unserialize('O:36:"Gears\DTO\Tests\Stub\AbstractDTOStub":0:{}');
}
}
15 changes: 8 additions & 7 deletions tests/DTO/AbstractScalarDTOTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace Gears\DTO\Tests;

use Gears\DTO\Exception\InvalidScalarParameterException;
use Gears\DTO\Tests\Stub\AbstractScalarDTOCollectionStub;
use Gears\DTO\Tests\Stub\AbstractScalarDTOStub;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -53,13 +52,15 @@ public function testAcceptDTO(): void
static::assertInstanceOf(AbstractScalarDTOStub::class, $stub->getObject());
}

public function testAcceptDTOCollection(): void
public function testSerialization(): void
{
$stub = AbstractScalarDTOStub::fromArray([
'collection' => AbstractScalarDTOCollectionStub::fromElements([AbstractScalarDTOStub::fromArray([])]),
]);
$stub = AbstractScalarDTOStub::fromArray(['parameter' => 100]);

$serialized = \version_compare(\PHP_VERSION, '7.4.0') >= 0
? 'O:42:"Gears\DTO\Tests\Stub\AbstractScalarDTOStub":1:{s:7:"payload";a:1:{s:9:"parameter";i:100;}}'
: 'C:42:"Gears\DTO\Tests\Stub\AbstractScalarDTOStub":28:{a:1:{s:9:"parameter";i:100;}}';

static::assertCount(1, $stub->getCollection());
static::assertInstanceOf(AbstractScalarDTOStub::class, $stub->getCollection()[0]);
static::assertSame($serialized, \serialize($stub));
static::assertSame(100, (\unserialize($serialized))->getParameter());
}
}

0 comments on commit 5dee1e3

Please sign in to comment.