Creating complex models using hydrators: aggregate / strategies

1k Views Asked by At

I had previously built complex models (an object containing many other types of model) using a service layer with different mappers passed in as the constructor.

eg.

class UserService{
    public function __construct(UserMapper $userMapper, AddressMapper $addressMapper, AppointmentsMapper $appointmentsMapper){}
    public function loadById($id) : User {
        $user = $this->userMapper->find($id);
        $appointments = $this->appointmentsMapper->findByUser($user);
        $user->setAppointments($appointments);
        $address = $this->addressMapper->findByUser($user);
        $user->setAddress($address);
        //..etc..
    }
}

The above is a simplified example. In my domain i am using multiple services used in factories to create a complex object graph.

After reading the very interesting article on MaltBlue about aggregate hydrators, i've tried to adopt this approach to simplify the object creation process. I love the idea of creating a HydratingResulset with the RowObjectPrototype set to the object to be returned.

I think i need some pointers on how to make this work in the real world. For instance, when using an AggregateHydrator, i can load a users Appointment history based on the user id passed into the hydrator.

class UserModelHydratorFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator) {
        $serviceManager = $serviceLocator->getServiceLocator();

        /**
         * Core hydration
         */
        $arrayHydrator = new ArraySerializable();
        $arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());

        $aggregateHydrator = new AggregateHydrator();
        $aggregateHydrator->add($arrayHydrator);
        $aggregateHydrator->add($serviceLocator->get('Hydrator\Address'));
        $aggregateHydrator->add($serviceLocator->get('Hydrator\Appointments'));
        return $aggregateHydrator;
    }
}

...with, for instance, the User Address hydrator looking like this:

class UserAddressHydrator implements HydratorInterface{
    protected $locationMapper;

    public function __construct(LocationMapper $locationMapper){
        $this->locationMapper = $locationMapper;
    }

    public function hydrate(array $data, $object){
        if(!$object instanceof User){
            return;
        }

        if(array_key_exists('userId', $data)){
            $object->setAddress($this->locationMapper->findByClientId($data['userId']));
        }
        return $object;
    }
}

This works perfectly. Although using the AggregateHydrator approach, it means that every object that has an Address as a property means it will needs it's own hydrator. So another (near-identical) hydrator will be needed if i were building a Company model, that also had an Address, since the above Hydrator is hard-coded to populate a User model (and expects the data containing a userId key). This means for each relationship / interaction (has-a) will need it's own hydrator to generate this. Is that normal? So i would need a UserAddressHydrator, CompanyAddressHydrator, SupplierAddressHydrator, AppointmentAddressHydrator - all near-identical to the above code, just populating a different object?

It would be much cleaner to have a single AddressHydrator, that takes an addressId and returns the Address model. This led me to look at hydrator strategies, which seems to be perfect, although they only work on a single value so cannot, for instance, look through the incoming array to see if a pk/fk / identifying key exists and load based on that.

I'd appreciate some clarification on this approach, it feels like i've got lost along the way.

1

There are 1 best solutions below

3
On

You are absolutely right. Hydrator strategies only work on a single value that corresponds to a member of your entity. So you have to add several strategies to your hydrator. On the other hand you can inherit from \Zend\Hydrator\AbstractHydrator and overwrite the addStrategy() method to handle arrays with more than one name. With that solution you can set the same Hydrator to the values of the array you add to addStrategy().

Simple Hydrator Strategy

This example shows the usage of a simple hydrator strategy.

class UserHydratorFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $oServiceLocator)
    {
        $oHydrator = (new ClassMethods(false))
            ->addStrategy('address', new AddressHydratorStrategy())
            ->addStrategy('company', new AddressHydratorStrategy());

        return $oHydrator;
    }
}

This is an example hydrator factory for the user entity. In this factory a normal ClassMethods hydrator is called. This type of hydrator assumes getter and setter methods in your entity for hydratring the entity members. Additionally there are added to strategies for the members adress and company.

class AddressHydratorStrategy extends DefaultStrategy
{
    public function hydrate($aData)
    {
        return (new ClassMethods(false))
            ->hydrate($aData, new AdressEntity())
    }
}

The strategy simply allows adding kind of sub-entities. If there is a address or company key in your data, the address entity will be added to these members.

class UserEntity
{
    protected $name;

    protected $address;

    protected $company;

    public function getName() : string
    {
        return $this->name;
    }

    public function setName(string $sName) : UserEntity
    {
        $this-> name = $sName;
        return $this;
    }

    public function getAddress() : AddressEntity
    {
        return $this->address;
    }

    public function setAddress(AddressEntity $oAddress) : UserEntity
    {
        $this->address = $oAddress;
        return $this;
    }

    public function getCompany() : AddressEntity
    {
        return $this->company;
    }

    public function setCompany(AddressEntity $oCompany) : UserEntity
    {
        $this->company = $oCompany;
        return $this; 
    }
}

Have you noticed the type hints? For example the setAddress method takes a AddressEntity object as parameter. This object will be generated by the strategy we added to the ClassMethods hydrator.

Next follows the call of the hydrator with some data wich will result in a nestet complex UserEntity object.

$oUserHydrator = $this->getServiceLocator(UserHydrator::class);
$oUserHydrator->hydrate(
    [
        'name' => 'Marcel',
        'address' => 
        [
            'street' => 'bla',
            'zipcode' => 'blubb',
        ],
        'company' => 
        [
            'street' => 'yadda',
            'zipcode' => 'yadda 2',
        ],
    ], 
    new UserEntity()
);

Usage in Result Sets

To make it more clear here 's a small example how I use hydrators and strategies directly in result sets.

class UserTableGateway extends TableGateway
{
    public function __construct(Adapter $oAdapter, $oUserHydrator)
    {
        $oPrototype = new HydratingResultSet(
            $oHydrator,
            new UserEntity()
        );

        parent::__construct('user_table', $oAdapter, null, $oPrototype);
    }

    public function fetchUser()
    {
        // here a complex join sql query is fired
        // the resultset is of the hydrated prototype
    }
}

In this example the TableGateway class is initialized with a Prototype, which is a HydratingResultSet. It uses the hydrator from the UserHydratorFactory. Hydrator strategies make sense, when complex data is directly fetched from a database or another source like a webservice, which returns nested data.

Conclusion

For me personally working with hydrator strategies makes more sense than working with the aggregate hydrator. Sure, you can add not more than one name to the strategy. Otherwise you can, like I said in the beginning, overwrite the addStrategy Method in an inherited class according to your requirements. Hydrator strategies are less coding in my eyes.