i have a simple question when building a value object that has a collection of value objects inside it with a specific type how do you construct the object ?
to take an example lets say you have a picture that take multiple dimensions
Option 1 :
Class Picture implements valueObject{
public function __construct(array $dimensions){
foreach($dimensions as $dimension){
// check if instance of `dimension` value object
}
}
}
Option 2 :
Class Picture implements valueObject{
public function __construct(DimensionCollection $dimensions){
}
}
Class DimensionCollection implements Traversable{
public function add(Dimension $dimension){
// add to array
}
}
Option two off-course seems more logical but is there another pattern that is better taken this from DDD preceptive ?
This first hing you have to do is list all the concepts that come to your mind when analyzing the domain. Here I see:
Picture
Dimension
Dimensions
Even, maybe behind the Dimensions object probably you have a generic (abstract) value-object Collection concept that can feed Dimensions as well as many other strongly-typed collections.
Even you could have an Interfaces\Collection to allow any element in your domain to accept "generic collections of things" if you are doing it quick and don't want to string-type something until you refine your model.
Provided PHP does not have class templates, like in C++ and you can't do something like class Dimensions extends Collection<Dimension> what I do is to force the type by strictily checking the elements at input.
Therefore my collections are conscious of what is the class they hold, and the interface reflects that via the getItemClassName() method.
Collection interfaceHere is what my basic Interfaces\Collection looks like:
<?php
declare( strict_types = 1 );
namespace XaviMontero\ThrasherPortage\Base\Collection\Interfaces;
interface Collection extends \Countable, \IteratorAggregate, \ArrayAccess
{
public function getItemClassName() : string;
public function getItem( int $index );
}
NOTE that the getItem() does not declare an explicit return type. I will override the return type in the classes implementing the interface.
CollectionHere is what my basic abstract Collection (which will be implemented by other type-specific classes) looks like:
<?php
declare( strict_types = 1 );
namespace XaviMontero\ThrasherPortage\Base\Collection;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\ImmutabilityException;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\InvalidTypeException;
use XaviMontero\ThrasherPortage\Base\Collection\Exceptions\OutOfRangeException;
abstract class Collection implements Interfaces\Collection
{
// Based on http://aheimlich.dreamhosters.com/generic-collections/Collection.phps
protected $itemClassName;
protected $items = [];
/**
* Creates a new typed collection.
* @param string $itemClassName string representing the class name of the valid type for the items.
* @param array $items array with all the objects to be added. They must be of the class $itemClassName.
*/
public function __construct( string $itemClassName, array $items = [] )
{
$this->itemClassName = $itemClassName;
foreach( $items as $item )
{
if( ! ( $item instanceof $itemClassName ) )
{
throw new InvalidTypeException();
}
$this->items[] = $item;
}
}
public function getItemClassName() : string
{
return $this->itemClassName;
}
public function getItem( int $index )
{
if( $index >= $this->count() )
{
throw new OutOfRangeException( 'Index: ' . $index );
}
return $this->items[ $index ];
}
public function indexExists( int $index ) : bool
{
if( $index >= $this->count() )
{
return false;
}
return true;
}
//---------------------------------------------------------------------//
// Implementations //
//---------------------------------------------------------------------//
/**
* Returns the count of items in the collection.
* Implements countable.
* @return integer
*/
public function count() : int
{
return count( $this->items );
}
/**
* Returns an iterator
* Implements IteratorAggregate
* @return \ArrayIterator
*/
public function getIterator()
{
return new \ArrayIterator( $this->items );
}
public function offsetSet( $offset, $value )
{
throw new ImmutabilityException();
}
public function offsetUnset( $offset )
{
throw new ImmutabilityException();
}
/**
* get an offset's value
* Implements ArrayAccess
* @see get
* @param integer $offset
* @return mixed
*/
public function offsetGet( $offset )
{
return $this->getItem( $offset );
}
/**
* Determine if offset exists
* Implements ArrayAccess
* @see exists
* @param integer $offset
* @return boolean
*/
public function offsetExists( $offset ) : bool
{
return $this->indexExists( $offset );
}
}
This is strongly based on http://aheimlich.dreamhosters.com/generic-collections/Collection.phps with the following changes:
Collection itself is a value-object, so it is immutable.
offsetSet() and offsetUnset() are forbidden.I have a set of 4 collection-based exceptions, 3 of which may be thrown by the Base\Collection (the other is just an abstract catch-all).
abstract class CollectionException extends \RuntimeException
class ImmutabilityException extends CollectionException
class InvalidTypeException extends CollectionException
class OutOfRangeException extends CollectionException
The names are pretty descriptive (I hope ;) ).
NOTE that the __construct takes 2 parameters: The classname of the objects that would be stored, and a generic array of objects to put inside the collection. An input type of a function cannot be overriden in subclasses, so there is not any "nice way" in PHP to do this other than eating a generic array and testing the type by looping over it.
NOTE that still the public function getItem( int $index ) does not declare a return type. This is done on purpose. You may override the output type. The "generic" collection may return "anything" at this point of implementation.
Dimensions collection.Now that you have a generic typed Collection base object (which is abstract) a collection needs to be a collection of something. In your case a collection of dimension objects.
Let's assume there is a Dimension namespace and there you have Dimension and Dimensions.
Here's what Dimensions should look like:
<?php
declare( strict_types = 1 );
namespace XaviMontero\ThrasherPortage\Dimension;
use XaviMontero\ThrasherPortage\Base\Collection\Collection;
class Dimensions extends Collection
{
public function __construct( array $items = [] )
{
parent::__construct( Dimension::class, $items );
}
public function getItem( int $index ) : Dimension
{
return parent::getItem( $index );
}
}
Here you can see that:
parent.__constructor wrapper takes an array of things (hopefully of Dimension objects) and tells the parent to convert itself to a collection of Dimension::class by hardcoding that the Dimensions can't hold any other thing other than Dimension. This ensures no consumer of this class can brak anything.Dimension so any consumer of Dimensions is strictly sure that any object returned by the collection is of type Dimension.Picture classNo changes from your code, except that I like to name collections with a plural (as for instance like in Dimensions) instead of calling them with a collection suffic (as for instance like in DimensionCollection ).
In addition I don't have a basic ValueObject type, as it does not add anything to me. Even the equals() is not useful in a base class because you do not force a concrete type in the abstract base class, and if you do place a generic value object as the type of the equals, then you would be allowing to compare apples with oranges. So for me value objects are just plain classes with no setters.
Finally, with a strongly-typed value-object immutable collection, it is very safe to give it aawy via a getDimensions() method, because nobody will be able to alter its contents (remember the throw new ImmutabilityException(); in the basic Collection class).
Class Picture
{
/** @var Dimensions */
private $dimensions;
public function __construct( Dimensions $dimensions )
{
$this->dimensions = $dimensions;
}
public function getDimensions() : Dimensions
{
return $this->dimensions;
}
}
Now you are ready to consume it by building pictures and querying them.
I will use TDD notation: expected to create the values, sut = system under test, and actual to see what I got, so I can unit-test this.
Just build everything in the constructors:
$expectedDimension1 = new Dimension( $whateverParamsTakesDimension1 );
$expectedDimension2 = new Dimension( $whateverParamsTakesDimension2 );
$expectedDimension3 = new Dimension( $whateverParamsTakesDimension3 );
$expectedDimensions = new Dimensions( [ $expectedDimension1, $expectedDimension2, $expectedDimension3 ] );
$sut = new Picture( $expectedDimensions );
All the types are perfectly set:
$actualDimensions = $sut->getDimensions(); // Forces a Dimensions type.
$actualDimension1 = $actualDimensions->getItem( 0 ); // Forces a Dimension type.
$actualDimension2 = $actualDimensions->getItem( 1 );
$actualDimension3 = $actualDimensions->getItem( 2 );
PictureTest:$this->assertSame( $expectedDimensions, $actualDimensions );
$this->assertSame( $expectedDimension1, $actualDimension1 );
$this->assertSame( $expectedDimension2, $actualDimension2 );
$this->assertSame( $expectedDimension3, $actualDimension3 );
If you are purist you would
Dimensions in DimensionsTest.Dimensions in Picture.$actualDimensions in PictureTest.assertSame( $a, $b ); but you would assertTrue( $a->equals( $b ) ); and have an equals() not only in Dimension but also in the base collection which loops on each item and does an equals() on each element.I just skipped this part as it is out of the scope of the question, which was only on how to make the collection.
In fact, in the method exposed you are combining the two methods you post in your question.
On one side you are using strong-types.
On the other you are also looping on the elements to check their type.
But using an abstract base-collection which receives the type, and having implementations that force the input types (in the __construct()) and the output types in the getItem() you are allowed to shift the single responsability into the proper place (SOLID rulez).
It is not a responsability of the picture to check the types of the Dimension objects. Who then? It is the Dimensions who should do it (via hardcoding the type passed to the basic collection). And then have the basic collection to make the loop that you post as the first example, which, by language limitations, must be done at runtime in PHP.
Now that you have this, if you want to expand your model from
Picture
Dimension
Dimensions
into
Picture
Pictures
Dimension
Dimensions
it is as easy as creating a Pictures that extends the Base\Collection and forces the type to Picture. As easy as that.
Et voilà. Done.
Complete, strongly typed, fully testable. What else would one want?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With