Welcome to Abdul Malik Ikhsan's Blog

Using Direct ArrayObject instance as ObjectPrototype in Zend\Db

Posted in Zend Framework 2, Zend Framework 3 by samsonasik on May 25, 2017

When creating a table model for ZF2 or ZF3 application with Zend\DB, direct ArrayObject instance can be usefull as ResultSet object prototype. We can no longer need to create an individual class that has getArrayCopy() or exchangeArray() for data transformation.

For example, we have the following table model:

<?php
namespace Application\Model;

use Zend\Db\TableGateway\AbstractTableGateway;

class CountryTable
{
    public static $table = 'country';
    private $tableGateway;

    public function __construct(AbstractTableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function getCountriesInAsia()
    {
        $select  = $this->tableGateway->getSql()->select();
        $select->where([
            'continent' => 'ASIA'
        ]);

        return $this->tableGateway->selectWith($select);
    }
}

The ArrayObject usage we can use is:

new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);

So, we can build the factory for above table model as follows:

<?php
namespace Application\Model;

use ArrayObject;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\TableGateway\TableGateway;

class CountryTableFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $resultSetPrototype = new HydratingResultSet();
        $resultSetPrototype->setObjectPrototype(
             new ArrayObject([], ArrayObject::ARRAY_AS_PROPS)
        );
        
        $tableGateway =  new TableGateway(
            CountryTable::$table,
            $container->get('Zend\Db\Adapter\Adapter'),
            null,
            $resultSetPrototype
        );

        return new CountryTable($tableGateway);
    }
}

and register it into service_manager under factories:

<?php
namespace Application;

return [
    // ...
    'service_manager' => [
        'factories' => [
            Model\CountryTable::class => Model\CountryTableFactory:class,
        ],
    ],
];

When retrieving the data, you can do the followings:

use Application\Model\CountryTable;

$countryTable    = $container->get(CountryTable::class);
$countriesInAsia = $countryTable->getCountriesInAsia();

foreach ($countriesInAsia as $key => $row) {

    // dump a copy of the ArrayObject
    var_dump($arrayCopy = $row->getArrayCopy());

    // echoed column as property
    echo $row->name; // with value "INA"
    echo $row->iso;  // with value "ID"
    echo $row->continent; // with value "ASIA"

    // echoed as array with provided key
    echo $row['name']; // with value "INA"
    echo $row['iso'];  // with value "ID"
    echo $row['continent']; // with value "ASIA"

    // modify data via exhangeArray
    $row->exchangeArray(array_merge(
		$arrayCopy,
		[
			'name' => 'INDONESIA',
		]
	));

    // or modify its data by its property
    $row->name = 'INDONESIA';
    // or modify its data by its index array
    $row['name'] = 'INDONESIA';

    echo $row->name; // now has value "INDONESIA"
    echo $row['name']; // now has value "INDONESIA"
}

Bonus:

To avoid repetitive creating factory class for each table model, we can create an abstract factory for it:

<?php

namespace Application\Model;

use ArrayObject;
use Interop\Container\ContainerInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\TableGateway\TableGateway;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;

class CommonModelTableFactory implements AbstractFactoryInterface
{
    public function canCreate(ContainerInterface $container, $requestedName)
    {
        return ((substr($requestedName, -5) === 'Table') && class_exists($requestedName));
    }

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $tableModel = '\\' . $requestedName;

        $resultSetPrototype = new HydratingResultSet();
        $resultSetPrototype->setObjectPrototype(
            new ArrayObject([], ArrayObject::ARRAY_AS_PROPS)
        );

        $tableGateway =  new TableGateway(
            $tableModel::$table,
            $container->get('Zend\Db\Adapter\Adapter'),
            null,
            $resultSetPrototype
        );

        return new $tableModel($tableGateway);
    }
}

So, now, we can have 1 abstract factory for all table model services:

<?php
namespace Application;

return [
    // ...
    'service_manager' => [
        'abstract_factories' => [
            Model\CommonModelTableFactory:class,
        ],
    ],
];

That’s it ๐Ÿ˜‰

Advertisements

7 Responses

Subscribe to comments with RSS.

  1. Dejan Rajic said, on June 19, 2017 at 5:41 pm

    Thank you very much for this example!
    It helped a lot when we started migration from ZF2 => ZF3.
    Cause we used DB from ServiceLocator, which is no longer supported.

    What we have now, is case that we need to set in every other module (but Application module, as we set Factory there) in config/module.config.php this:

    ‘controllers’ => [
    ‘factories’ => [
    Controller\IndexController::class => Model\CommonModelTableFactory::class ,
    ],
    ],

    (only different controller, of course)

    Is it possible that we can set this somewhere globally?

    For example:
    ‘controllers’ => [
    ‘factories’ => [
    Controller\IndexController::class => Model\CommonModelTableFactory::class,
    Controller\AlbumController::class => Model\CommonModelTableFactory::class,
    //…
    ],
    ],

    So all modules, and all classes have access to this Factory?

    We tried to put in main app config/global.php, and in config/modules.config.php, but no luck…

    Thank you very much!

    • samsonasik said, on June 19, 2017 at 6:25 pm

      You need to define `Model\CommonModelTableFactory::class` under service_manager, please read again.

      This article is nothing to do with controller. You need to inject the controller with the Table model service that auto-created via the abstract_factories, please read https://samsonasik.wordpress.com/2013/01/02/zend-framework-2-cheat-sheet-service-manager/ and zf servicemanager documentation about how to inject dependency with factory https://docs.zendframework.com/zend-servicemanager/

      • Dejan Rajic said, on June 19, 2017 at 7:44 pm

        This solved everything:
        ‘controllers’ => [
        ‘abstract_factories’ => [
        \Zend\Mvc\Controller\LazyControllerAbstractFactory::class ,
        ],
        ],

        Zend has this LazyControllerAbstractFactory that is great for me!

        Cause I needed only to have my DB locator in all my Controllers in all Modules, and now I do. ๐Ÿ™‚

        Thanks!
        Your article was great starting point!

  2. worthwhileindustries said, on June 28, 2017 at 8:14 am

    I always see TableGateway Pattern used with one table. What about using joins?

    • samsonasik said, on July 4, 2017 at 9:45 pm

      You can do join with something like:

      $select = $this->tableGateway->getSql()->select();
      $select->join('continent', 'continent.continent_id = country.continent_id');
      
      return $this->tableGateway->selectWith($select);
      
  3. Axel said, on July 4, 2017 at 8:14 pm

    Instead of using “return ((substr($requestedName, -5) === ‘Table’) && class_exists($requestedName));” in the “CommonModelTableFactory::canCreate()”, it seems to be more cleaner to define a (empty) Interface like “CommonModelTableInterface”, which is implemented by the table-model-classes (e.g. “class CountryTable implements CommonModelTableInterface”). In “CommonModelTableFactory::canCreate()” we could now do a check like “return class_exists($requestedName) && in_array(CommonModelTableInterface::class, class_implements($requestedName));”


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: