zendframework/zend-db

RowGatewayFeature and HydratingResultSet

Opened this issue · 6 comments

4 years ago @RalfEggert wrote a post http://zend-framework-community.634137.n4.nabble.com/Combine-TableGateway-RowGateway-and-Entity-classes-td4658325.html about combining RowGatewayFeature with HydratingResultSet.
It is still not working. I think it may be commonly used feature - work with hydrated object and then save it.

@xorock

…work with hydrated object and then save it.

The hydrated object is your own target object. If you want to have the save or delete methods in your target object, then you must implement the Zend\Db\RowGateway\RowGatewayInterface in the target object.

Please look at the documentation: "Row Gateways - ActiveRecord Style Objects". (Important info is missing here: you must implement the exchangeArray method in your target object!)

For this solution the HydratingResultSet is not needed.

But I see problem in the ResultSet class:

public function setArrayObjectPrototype($arrayObjectPrototype)
{
    if (!is_object($arrayObjectPrototype)
        || (!$arrayObjectPrototype instanceof ArrayObject && !method_exists($arrayObjectPrototype, 'exchangeArray'))
    ) {
        throw new Exception\InvalidArgumentException('Object must be of type ArrayObject, or at least implement exchangeArray');
    }
}

https://github.com/zendframework/zend-db/blob/master/src/ResultSet/ResultSet.php#L65

So the next steps are:

  1. Extend the unit tests for setArrayObjectPrototype method to check side effects
  2. Update the condition in setArrayObjectPrototype method
  3. Update the documentation and the code example

If you want to use the HydratingResultSet:

  1. Implement the RowGatewayInterface in your target object
  2. A new RowGatewayFeature class is needed
  3. A new RowGateway class is needed

@froschdesign A little time has passed since the last comment but I've encountered the same issue.

I have an Active Record style implementation, but sometimes I want to transform the database result using a hydrator - for example:-

public function initialize()
{
    parent::initialize();

    $resultSetPrototype = $this->resultSetPrototype;

    $hydrator = $resultSetPrototype->getHydrator();
    $hydrator->addStrategy('data', new JsonStrategy());
}

There doesn't seem to be too much required to make the Zend\Db\TableGateway\Feature\RowGatewayFeature a little more versatile

  • Line 39 expects an instance of ResultSet which instead could be ResultSetInterface
  • Line 75 uses a method setArrayObjectPrototype which could instead be aliased to setObjectPrototype meaning that method is standardised across the ResultSet (maybe even part of the interface?)
  • Finally, the RowGatewayFeature also locks in the use of a RowGateway class when it would be infinitely more useful for it to make that class configurable (setRowGatewayPrototype and getRowGatewayPrototype) to create instances of my own classes

Slight refactoring to allow the flexibility per above. The only other change needed would be to ResultSet to change the method names

<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Db\TableGateway\Feature;

use Zend\Db\ResultSet\ResultSet;
use Zend\Db\RowGateway\RowGatewayInterface;
use Zend\Db\TableGateway\Exception;

class RowGatewayFeature extends AbstractFeature
{
    protected $primaryKey;
    protected $rowGatewayPrototype = ResultSet::class;

    /**
     * @param null $primaryKey
     */
    public function __construct()
    {
        $args = func_get_args();

        if (isset($args[0])) {
            if (is_string($args[0]) {
                $this->setPrimaryKey($args[0]);
            } elseif ($args[0] instanceof RowGatewayInterface) {
                $this->setRowGatewayPrototype($args[0]);
            }
        }
    }

    public function getPrimaryKey()
    {
        $this->constructorArguments = func_get_args();
    }

    public function setPrimaryKey($primaryKey)
    {
        $this->primaryKey = $primaryKey;
    }

    public function getRowGatewayPrototype()
    {
        $this->constructorArguments = func_get_args();
    }

    public function setRowGatewayPrototype($prototype)
    {
        $this->constructorArguments = func_get_args();
    }

    public function postInitialize()
    {
        $primaryKey = $this->getPrimaryKey();
        if (null === $primaryKey) {
            // get from metadata feature
            $metadata = $this->tableGateway->featureSet->getFeatureByClassName(
                'Zend\Db\TableGateway\Feature\MetadataFeature'
            );
            if ($metadata === false || ! isset($metadata->sharedData['metadata'])) {
                throw new Exception\RuntimeException(
                    'No information was provided to the RowGatewayFeature and/or no MetadataFeature could be consulted '
                    . 'to find the primary key necessary for RowGateway object creation.'
                );
            }
            $primaryKey = $metadata->sharedData['metadata']['primaryKey'];
        }

        $rowGatewayPrototype = $this->getRowGatewayPrototype();
        if (is_string($rowGatewayPrototype)) {
            $rowGatewayPrototypeClass = $rowGatewayPrototype;
            $rowGatewayPrototype = new $rowGatewayPrototypeClass(
                $primaryKey,
                $this->tableGateway->table,
                $this->tableGateway->adapter
            );
        }

        if (! $rowGatewayPrototype instanceof RowGatewayInterface) {
            throw new Exception\RuntimeException(
                'This feature ' . __CLASS__ . ' expects the RowGateway to be an instance of Zend\Db\RowGateway\RowGatewayInterface'
            );
        }

        if (! $this->tableGateway->resultSetPrototype instanceof ResultSetInterface) {
            throw new Exception\RuntimeException(
                'This feature ' . __CLASS__ . ' expects the ResultSet to be an instance of Zend\Db\ResultSet\ResultSetInterface'
            );
        }

        /** @var $resultSetPrototype ResultSetInterface */
        $resultSetPrototype = $this->tableGateway->resultSetPrototype;
        $resultSetPrototype->setObjectPrototype($rowGatewayPrototype);
    }
}

@simon-mundy
I'm sorry, but the topic is already very old. Please create a pull request for your proposal, add a description of the improvement, and also add unit tests.

@froschdesign OK will do. It's been a while and a bit rusty but will get onto it ASAP

This repository has been closed and moved to laminas/laminas-db; a new issue has been opened at laminas/laminas-db#101.