I am wondering about the best practices for loading complex objects.
To begin with, i'm going to outline some boilerplate before getting to the problem.
Assume the following: A simple domain model Client is loaded using a tablegateway, with factories used at every stage to inject dependencies:
namespace My\Model\Client;
class Client implements InputFilterProviderInterface
{
/**#var integer*/
protected $id;
/**#var InputFilter*/
protected $inputFilter;
/**#var Preferences */
protected $preferences;
/**#var Orders*/
protected $orders;
/**#var Contacts*/
protected $contacts;
}
A factory for this Client object:
namespace My\Model\Client;
class ClientFactory implements FactoryInterface
{
public function($container, $requestedName, $options)
{
$client = new Client();
$client->setInputFilter($container->get('InputFilterManager')->get('ClientInputFilter'));
return $client;
}
}
Next the mapper factory, which uses a TableGateway:
namespace My\Model\Client\Mapper;
class ClientMapperFactory implements FactoryInterface
{
public function __invoke($container, $requestedName, $options)
{
return new ClientMapper($container->get(ClientTableGateway::class));
}
}
The TableGatewayFactory:
namespace My\Model\Client\TableGateway
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$hydrator = new ArraySerialisable();
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$tableGateway = new TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
return $tableGateway;
Note the use of a HydratingResultset to return fully formed Client objects from the ResultSet.
This all works nicely.
Now the Client object has several related objects as properties, so whilst using the HydratingResultSet, i'm going to add an AggregateHydrator to load them:
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
**$hydrator = $container->get('HydratorManager')->get(ClientHydrator::class);**
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$tableGateway = new TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
return $tableGateway;
}
Finally, the Clients hydrator factory:
class ClientHydratorFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
//base ArraySerializable for Client object hydration
$arrayHydrator = new ArraySerializable();
$arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());
$aggregateHydrator = new AggregateHydrator();
$aggregateHydrator->add($arrayHydrator);
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsAddressHydrator::class));
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsOrdersHydrator::class));
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsPreferencesHydrator::class));
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsContactsHydrator::class));
return $aggregateHydrator;
}
}
... with the gist of the above hydrators being like:
class ClientsAddressHydrator implements HydratorInterface
{
/** #var AddressMapper */
protected $addressMapper;
public function __construct(AddressMapper $addressMapper){
$this->addressMapper = $addressMapper;
}
public function extract($object){return $object;}
public function hydrate(array $data, $object)
{
if(!$object instanceof Client){
return;
}
if(array_key_exists('id', $data)){
$address = $this->addressMapper->findClientAddress($data['id']);
if($address instanceof Address){
$object->setAddress($address);
}
}
return $object;
}
}
Finally we're at the issue. The above works perfectly and will load quite cleanly a Client object with all the related objects fully formed. But i have some resources where the entire object graph is not needed - for instance, when viewing a table of all clients - there is no need for any more information to be loaded.
So i've been thinking of ways of using the factories to choose which dependencies to include.
Solution 1
A factory for each use case. If only the Client data is needed (with no dependencies), then create a series of factories ie ClientFactory, SimpleClientFactory, ComplexClientFactory, ClientWithAppointmentsFactory etc. Seems redundant and not very reusable.
Solution 2
Use the options param defined in the FactoryInterface to pass "loading" options to the hydrator factory, eg:
class ViewClientDetailsControllerFactory implements FactoryInterface
{
//all Client info needed - full object graph
public function __invoke($container, $requestedName, $options)
{
$controller = new ViewClientDetailsController();
$loadDependencies = [
'loadPreferences' => true,
'loadOrders' => true,
'loadContacts' => true
];
$clientMapper = $container->get(ClientMapper::class, '', $loadDependencies);
return $controller;
}
}
class ViewAllClientsControllerFactory implements FactoryInterface
{
//Only need Client data - no related objects
public function __invoke($container, $requestedName, $options)
{
$controller = new ViewAllClientsController();
$loadDependencies = [
'loadPreferences' => false,
'loadOrders' => false,
'loadContacts' => false
];
$clientMapper = $container->get(ClientMapper::class, '', $loadDependencies);
return $controller;
}
}
The mapper factory passes the options to the tablegateway factory, that passes them on to the hydrator factory:
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$hydrator = $container->get('HydratorManager')->get(ClientHydrator::class, '', $options);
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$tableGateway = new TableGateway('clients', $container->get(Adapter::class), null, $resultSet);
return $tableGateway;
}
Finally, we can define here how much info to load into the Client:
class ClientHydratorFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
//base ArraySerializable for Client object hydration
$arrayHydrator = new ArraySerializable();
$arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());
$aggregateHydrator = new AggregateHydrator();
$aggregateHydrator->add($arrayHydrator);
if($options['loadAddress'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsAddressHydrator::class));
}
if($options['loadOrders'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsOrdersHydrator::class));
}
if($options['loadPreferences'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsPreferencesHydrator::class));
}
if($options['loadContacts'] === true){
$aggregateHydrator->add($container->get('HydratorManager')->get(ClientsContactsHydrator::class));
}
return $aggregateHydrator;
}
}
This seems to be a clean solution, as the dependencies can be defined per request. However i don't think that this is using the options param as intended - the documentation states that this parameter is supposed to be for passing constructor params to the object, not defining what logic the factory should use to load dependencies.
Any advice, or alternative solutions to achieve the above, would be great. Thanks for reading.
Creating a big palette of all possible combinations would not be just a nightmare, but a declared suicide.
Using options
I wouldn't suggest you this option either. I mean, it's not that bad, but it has a major issue: everytime you instantiate your hydrator, you should remember to pass those options, or you'll get an "empty hydrator". Same logic applies to everything that uses those hydrators.
Since you actually want to remove hydrators you don't need, I'd suggest to avoid this solution, because this way you are always forced to declare which hydrators you need (and, honestly, I'll always forget to do it.. ^^ ).
If you add a new hydrator, you'll have to go through your project and add new options. Not really worth the effort...
That's why I propose you the next solution
Removing unnecessary hydrators
In 99% of the cases, hydrators are used by mappers. Thus, I think it would be cleanier to have a mapper which, by default, returns always the same kind of data (->a single hydrator), but that it can be modified to remove a certain set of hydrators.
Inside the AggregateHydrator, all hydrators are converted into listeners and attached to EventManager. I had some issue while trying to get all events, so I turned on creating an aggregate hydrator with the option to detach an hydrator:
class DetachableAggregateHydrator extends AggregateHydrator
{
/**
* List of all hydrators (as listeners)
*
* #var array
*/
private $listeners = [];
/**
* {#inherit}
*/
public function add(HydratorInterface $hydrator, int $priority = self::DEFAULT_PRIORITY): void
{
$listener = new HydratorListener($hydrator);
$listener->attach($this->getEventManager(), $priority);
$this->listeners[get_class($hydrator)] = $listener;
}
/**
* Remove a single hydrator and detach its listener
*
* #param string $hydratorClass
*/
public function detach($hydratorClass)
{
$listener = $this->listeners[$hydratorClass];
$listener->detach($this->getEventManager());
unset($listener);
unset($this->listeners[$hydratorClass]);
}
}
Then, in the TableGatewayFactory:
class ClientTableGatewayFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$hydrator = $container->get('HydratorManager')->get(ClientHydrator::class);
$rowObjectPrototype = $container->get(Client::class);
$resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
$adapter = $container->get(Adapter::class);
$tableGateway = new TableGateway('clients', $adapter, null, $resultSet);
return $tableGateway;
}
}
And the ClientHydratorFactory:
class ClientHydratorFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$aggregateHydrator = new DetachableAggregateHydrator();
$arrayHydrator = new ArraySerializable();
$arrayHydrator->addStrategy('dateRegistered', new DateTimeStrategy());
$aggregateHydrator->add($arrayHydrator);
$hydratorManager = $container->get('HydratorManager');
$aggregateHydrator->add($hydratorManager->get(ClientsAddressHydrator::class));
$aggregateHydrator->add($hydratorManager->get(ClientsOrdersHydrator::class));
$aggregateHydrator->add($hydratorManager->get(ClientsPreferencesHydrator::class));
$aggregateHydrator->add($hydratorManager->get(ClientsContactsHydrator::class));
return $aggregateHydrator;
}
}
You just need to make tablegateway accessible by outstide the mapper:
class ClientMapper
{
private $tableGateway;
// ..
// Other methods
// ..
public function getTableGateway(): TableGateway
{
return $this->tableGateway;
}
}
And now you're able to choose which hydrators you don't want to attach.
Let's say you have two controllers:
ClientInfoController, where you need clients and their address, preferences and contacts
ClientOrdersController, where you need clients with their orders
Their factories will be:
class ClientInfoController implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$clientMapper = $container->get(ClientMapper::class);
// Orders are unnecessary
$resultSetPrototype = $clientMapper->getTableGateway()->getResultSetPrototype();
$resultSetPrototype->getHydrator()->detach(ClientsOrdersHydrator::class);
return $aggregateHydrator;
}
}
class ClientOrdersController implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$clientMapper = $container->get(ClientMapper::class);
// Orders are unnecessary
$resultSetPrototype = $clientMapper->getTableGateway()->getResultSetPrototype();
$resultSetPrototype->getHydrator()->detach(ClientsAddressHydrator::class);
$resultSetPrototype->getHydrator()->detach(ClientsPreferencesHydrator::class);
$resultSetPrototype->getHydrator()->detach(ClientsContactsHydrator::class);
return $aggregateHydrator;
}
}
I have a custom validator, extending Zend AbstractValidator. The thing is, i want to include Doctrine EntityManager, but i keep failing! I tried to make a Factory for my Validator, but it doesn't seem to work. Help!! What am I doing wrong?
Validator:
$this->objectRepository stays empty, while i expect content.
namespace Rentals\Validator;
use Rentals\Response;
use Zend\Validator\AbstractValidator;
use Zend\Stdlib\ArrayUtils;
class ExistentialQuantification extends AbstractValidator
{
const NO_ENTITY_ID = 'noEntityId';
const ENTITY_NOT_FOUND = 'entityNotFound';
const INVALID_ID = 'invalidId';
protected $messageTemplates = [
self::NO_ENTITY_ID => 'The input does not contain an entity id.',
self::ENTITY_NOT_FOUND => 'The entity could not be found.',
self::INVALID_ID => 'The input does not contain an entity id.',
];
protected $objectRepository;
public function __construct(array $options)
{
$this->objectRepository = $options['object_repository'];
parent::__construct($options);
}
public function isValid($value)
{
if ($value === null) {
return true;
}
if (! isset($value->id)) {
$this->error(self::NO_ENTITY_ID);
return false;
}
$entityClass = $this->getOption('entity_class');
$controller = new Controller();
$entity = (new FactoryInterface)(EntityManager::class)->find($entityClass, $entity->id);
if (! $entity instanceof $entityClass) {
$this->error(self::ENTITY_NOT_FOUND);
return false;
}
if (! $entity->getId()) {
$this->error(self::NO_ENTITY_ID);
return false;
}
return true;
}
}
Factory:
namespace Rentals\Validator;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\MutableCreationOptionsInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Stdlib\ArrayUtils;
class ExistentialQuantificationFactory implements FactoryInterface, MutableCreationOptionsInterface
{
protected $options = [];
public function setCreationOptions(array $options)
{
$this->options = $options;
}
public function createService(ServiceLocatorInterface $serviceLocator)
{
if (! isset($this->options['object_manager'])) {
$this->options['object_manager'] = 'doctrine.entitymanager.orm_default';
}
$objectManager = $serviceLocator->get($this->options['object_manager']);
$objectRepository = $objectManager->getRepository($this->options['entity_class']);
return new ExistentialQuantification(ArrayUtils::merge(
$this->options, [
'objectManager' => $objectManager,
'objectRepository' => $objectRepository
]
));
}
}
Module config:
<?php
return [
'service_manager' => [
'factories' => [
'Rentals\\Validator\\ExistentialQuantification' => 'Rentals\\Validator\\ExistentialQuantificationFactory'
]
]
];
?>
What if you change your config entry like the following example?
return [
'validators' => [
'factories' => [
ExistentialQuantification::class => ExistentialQuantificationFactory::class,
],
],
];
This change will result in further changes for your factory, because the service locator for the entity manager differs from the one you injected.
namespace Application\Validator\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\MutableCreationOptionsInterface;
use Zend\ServiceManager\MutableCreationOptionsTrait;
use Zend\ServiceManager\ServiceLocatorInterface;
class ExistentialQuantificationFactory implements FactoryInterface, MutableCreationOptionsInterface
{
use MutableCreatinOptionsTrait;
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $serviceLocator->getServiceLocator();
if (! isset($this->creationOptions['object_manager'])) {
$this->creationOptions['object_manager'] = 'doctrine.entitymanager.orm_default';
}
$objectManager = $parentLocator->get($this->creationOptions['object_manager']);
$objectRepository = $objectManager->getRepository($this->creationOptions['entity_class']);
return new ExistentialQuantification(ArrayUtils::merge(
$this->options, [
'objectManager' => $objectManager,
'objectRepository' => $objectRepository
]
));
}
}
What I 've done here? First I implemented the MutableCreationOptionsTrait class. This trait implements the needed functions for working with creation options. But this is just a little hint for avoiding unnecessary work.
Because of setting the validator class as validator in the config, we have to use the parent service locator for getting the entity manager. The inherited service locator just provides access to validators.
Now you can try to access your validator in your controller like in the following examaple.
$validator = $this->getServiceLocator()
->get('ValidatorManager')
->get(ExistentialQuantification::class, [
'entity_class' => YourEntityClass::class,
]);
\Zend\Debug\Debug::dump($validator, __METHOD__);
The validator manager should return your validator so that you can test it.
I am unable to load service locatior on a custom class, see my error code below, any advice would be appriciated
namespace Application\Helper;
use Zend\Mail\Message;
use Zend\Mail\Transport\Smtp as SmtpTransport;
use Zend\Mime\Message as MimeMessage;
use Zend\Mime\Part as MimePart;
use Zend\Mail\Transport\SmtpOptions;
use Zend\Mail\Transport\Sendmail;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class EmailHelper implements ServiceLocatorAwareInterface{
protected $serviceLocator;
public function testEmail($email){
$config = $this->serviceLocator->get('Config');
print_r($config); exit;
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
}
Call to a member function get() on null in
/var/www/html/engsvc_dev/module/Application/src/Application/Helper/EmailHelper.php
on line 23
In order to inject the ServiceManager instance which is an implementation of Zend\ServiceManager\ServiceLocatorInterface you need to instantiate the class like this:
$emailHelper = $this->getServiceLocator()->get('EmailHelper');
Also don't forget to register the class in Module.php
public function getServiceConfig()
{
return array(
'invokables' => array(
'EmailHelper' => 'Application\Helper\EmailHelper'
)
);
}
I am in the process of trying to learn OO/Zend Framework over standard PHP.. I want to scream and write a mysql query instead of using the TableGateway method.
I have been following tutorials and have successfully printed out a table and some fields, although with the way I have gone about doing this, I am totally lost in how I should make this a join with another table and print out some fields there.
For example.
Table Fields
customer Idx, Company
contact Idx, First_Name
This is my customersController where I assume the work is carried out
namespace Customers\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\DB\TableGateway\TableGateway;
class CustomersController extends AbstractActionController
{
protected $customersTable;
public function indexAction()
{
return new ViewModel(array('customer' => $this->getCustomersTable()->select()));
//return new ViewModel(array('customers' => $this->fetchJoin()->select()));
}
public function addAction()
{
}
public function editAction()
{
}
public function deleteAction()
{
}
public function getCustomersTable()
{
if (!$this->customersTable) {
$this->customersTable = new TableGateway (
'customer', //table name
$this->getServiceLocator()->get('Zend\DB\Adapter\Adapter')
);
}
return $this->customersTable;
}
}
Am I on the right track here?
If you need to make joins read about Zend\Db\Sql and Zend\Db\Select
which you can read about here
http://framework.zend.com/manual/2.0/en/modules/zend.db.sql.html
An example would be:
In your model(that extends the TableGateway or the AbstractTableGateway)
in Some function you can have something like(this is from a project) :
$sql = new \Zend\Db\Sql\Sql($this->getAdapter());
$select = $sql->select()
->from('event_related_events')
->columns(array())
->join('event_invitees', 'event_invitees.event_id =
event_related_events.related_event_id')
->where(array('event_related_events.event_id' => $eventId));
$selectString = $sql->getSqlStringForSqlObject($select);
$results = $this->getAdapter()->query($selectString, \Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
Then you can loop over the results and do what you need to.
Taking a look at more powerful ORM like Doctrine or Propel may also help, but may be an overkill for a small/hobby project.
EDIT: Answer for what was asked in comments
For Using expression(if, case etc) directly you can use something like :
$sql->select()
->from('table')
->columns(array(
'sorter' => new Expression('(IF ( table.`something` >= 'otherthing', 1, 0))'),
'some_count' => new Expression('(count(*))'),
)
)
Explaining the last line in SQL terms, it would be:
count(*) AS some_count
So this is my controller, basically from the Album example but now it will display customers from the customer table.
<?php
namespace Customers\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Customers\Model\Customers;
use Customers\Form\CustomersForm;
class CustomersController extends AbstractActionController
{
protected $customersTable;
public function indexAction()
{
return new ViewModel(array(
'customer' => $this->getCustomersTable()->fetchAll(),
));
}
public function addAction()
{
}
public function editAction()
{
}
public function deleteAction()
{
}
public function getCustomersTable()
{
if (!$this->customersTable) {
$sm = $this->getServiceLocator();
$this->customersTable = $sm->get('Customers\Model\CustomersTable');
}
return $this->customersTable;
}
}
?>
The indexAction calls the getCustomersTable method which goes to the model (CustomersTable) and executes the "query" there.
<?php
namespace Customers\Model;
use Zend\Db\TableGateway\TableGateway;
class CustomersTable
{
protected $tableGateway;
public function __construct(TableGateway $tableGateway)
{
$this->tableGateway = $tableGateway;
}
public function fetchAll()
{
$resultSet = $this->tableGateway->select();
return $resultSet;
}
public function getCustomers($id)
{
}
public function saveCustomers(customers $customers)
{
}
public function deleteCustomers($id)
{
}
}
?>
So from your example, I should be trying to implement this into the fetchAll in the model?
Thanks for the help.
$sql = new \Zend\Db\Sql\Sql($this->getAdapter());
$select = $sql->select()
->from('customer')
->columns(array())
->join('contact', 'contact.Idx = customer.Idx')
->where(array('contact.Idx' => $eventId));
$selectString = $sql->getSqlStringForSqlObject($select);
$results = $this->getAdapter()->query($selectString, \Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
i have created a custom validator but when I want to use it, it seems that it is never executed!
the validator :
class sfTestUrlValidator extends sfValidatorUrl {
public function initialize($context, $parameters = null) {
// Initialize parent
parent::initialize($context);
}
public function execute(&$value, &$error) {
if($value == "http://www.librosweb.es/")
{
//$error = "noooooooooooooo";
return true;
}
else return false;
}
}
in the configure method of a form, i do like that :
public function configure() {
.....
....
'url' => new sfTestUrlValidator(),
You need to override sfValidatorBase::doClean method and not some not-existent execute method and throw exception intead of returning true/false. Have a look at any validator class, e.g. sfValidatorString. However in your case, I would simply use sfValidatorChoice with one choice
public function configure()
{
'url' => new sfValidatorChoice(array('choices' => array(
'your.website.url',
)));
}