Building Complex Models Using Hydrators: Aggregate / Strategies

I've previously built complex models (an object containing many other types of models) using a service layer with different mappers passed in as a 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 example is simplified. On my domain, I use several services used in factories to create a complex graph of objects.

After reading a very interesting MaltBlue article on aggregate hydrators, I tried to apply this approach to simplify the object creation process. I love the idea of ​​creating a HydratingResulset with the RowObjectPrototype set on the returned object.

I guess I need some pointers on how to make this work in the real world. For example, when using AggregateHydrator, I can load the users' Appointment history based on the user id passed to 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 example, the User Address gigator looks 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 great. While using the AggregateHydrator approach means that every object that has an address as a property means that it will need its own hydrator. So a different (almost identical) Hydrator would be needed if I was building a model of a company that also had an address, since the aforementioned Hydrator is hardcoded to populate the User model (and expects data containing a userId key). This means that each relationship / interaction (has-a) will need its own hydrator to create it. This is normal? So I will need UserAddressHydrator, CompanyAddressHydrator, SupplierAddressHydrator, AppointmentAddressHydrator - all nearly identical to the above code, just filling in a different object?

It would be much cleaner to have one AddressHydrator that takes an addressId and returns an Address model. This led me to take a look at the hydrator strategies which seem to be ideal, although they only work on one value, so they cannot, for example, look at the input array to see if the pk / fk / ident file exists and is loaded based on that.

I would appreciate an explanation of this approach, it looks like I got lost along the way.

+3


source to share


1 answer


You are absolutely right. Hydrator strategies only work on one value that corresponds to a member of your organization. Therefore, you need to add a few strategies to your hydrator. Alternatively, you can inherit from \Zend\Hydrator\AbstractHydrator

and overwrite the method addStrategy()

to handle arrays with multiple names. With this solution, you can set the same Hydrator as the values ​​of the array being added to addStrategy()

.

Simple hydration strategy

This example demonstrates the use of a simple hydration 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 of a hydrator factory for a custom object. In this factory, the normal hydrator is called ClassMethods

. This type of hydrator assumes methods for getting and setting in your object to hydrate the members of the entity. Additionally, strategies are added for the recipients of the participants and the company.

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

      

The strategy just allows you to add the kind of entities. If you have an address or corporate key in your data, the address object 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, a method setAddress

takes an object AddressEntity

as a parameter. This object will be generated by the strategy added to the hydrator ClassMethods

.



This is followed by a call to the hydrator with some data that will lead to the creation of the nestet complex UserEntity

.

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

      

Use in result sets

To make this clearer, here's a small example of 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 class is TableGateway

initialized with a prototype, which is HydratingResultSet

. He uses a hydrator from UserHydratorFactory

. Hydrator strategies make sense when complex data is directly retrieved from a database or other source like a webservice that returns nested data.

Conclusion

For me personally, working with hydration strategies makes more sense than working with cumulative hydration. Of course, you can add at most one name to the strategy. Otherwise, you can, as I said, overwrite the method addStrategy

in the inherited class as per your requirement. Hydrator strategies are less coded in my eyes.

+2


source







All Articles