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 am using zfcUser with Zend Framework 2.
i want to capture the login event so that i can do something with it and also redirect the user to a different page.
i know how to catch the register event. ie i place a onBootstrap in my module.php file and then do the following
$em->attach('ZfcUser\Form\RegisterFilter','init',function($e)
the entire function is like this;
public function onBootstrap(MVCEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$em = $eventManager->getSharedManager();
$em->attach(
'ZfcUser\Form\RegisterFilter',
'init',
function($e)
{ }
}
However, when i tried to catch the login event i did not have any results. i.e
$zfcServiceEvents->attach('login.post', function($e) {
echo "loggedn"; die();
});
would really appreciate some help.
thank you.
edit:
this is how i called the registered event ( i know how to called the event for a user who has just registered. but i want to call the event when a user has just succesfully logged in
public function onBootstrap(MVCEvent $e)
{
$zfcServiceEvents = $e->getApplication()->getServiceManager()->get('zfcuser_user_service')->getEventManager();
$zfcServiceEvents->attach('register', function($e) {
$form = $e->getParam('form');
$user = $e->getParam('user');
RESPONSE TO cptnk CODE
in response to the answer given by cptnk i tried the following code but it did not work;
public function onBootstrap(MVCEvent $e)
{
$sharedManager = $e->getApplication()->getEventManager()->getSharedManager();
$serviceManager = $e->getApplication()->getServiceManager();
$loginFunction = function ($e) use ($serviceManager) {
echo "eventCaptured" ; die();
};
$sharedManager->attach('ZfcUser\Service\User', 'login.post', $loginFunction);
}
i am still not able to capture the login event.Any ideas?
cptnik had a good suggestion; he asked whether i had overridden the ZFcUser-service. in responce i clarfiy that i had overridden the zfcuser. below is the code i used;
'zfcuser' => array(
// telling ZfcUser to use our own class
'user_entity_class' => 'BaseModel\Entity\User',
// telling ZfcUserDoctrineORM to skip the entities it defines
'enable_default_entities' => false,
),
/**
* Listen to the bootstrap event
*
* #return array
*/
public function onBootstrap(MvcEvent $e)
{
$serviceManager = $e->getApplication()->getServiceManager()
$loginFunction = function ($e) use ($serviceManager) {
// do something
};
$sharedManager->attach('ZfcUser\Service\User', 'login.post', $loginFunction);
}
Your code does not show what $zfcServiceEvents is or where it came from I assume it is a event manager referencing the zfcuser events. My approach is a little different since I attach a event to the zf2 shared manager. Maybe you have problems regarding that piece of code?
You can grab the sharedEventManager like so:
$sharedManager = $e->getApplication()->getEventManager()->getSharedManager();
EDIT: I was a little unclear on how to get the $serviceManager and the application within the Module.php onBootstrap function.
Try this
$events = $e->getApplication()->getEventManager()->getSharedManager();
// Handle login
$events->attach('ZfcUser\Authentication\Adapter\AdapterChain', 'authenticate.success', function($e) {
$userId = $e->getIdentity();
// do some stuff
});
In your Module.php add this:
public function onBootstrap(MvcEvent $e) {
$em = $e->getApplication()->getEventManager();
$UserListener = $e->getApplication()->getServiceManager()->get('UserListener');
$em->attachAggregate($UserListener);
}
and create src/User/Listener/UserListener.php with below code:
namespace LcUser\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LcUserListener extends AbstractListenerAggregate implements ServiceLocatorAwareInterface {
/**
* #var ServiceLocatorInterface
*/
protected $serviceManager;
/**
*
* #param ServiceLocatorInterface $serviceLocator
*/
public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
$this->serviceManager = $serviceLocator;
}
/**
*
* #return \Zend\ServiceManager\ServiceLocatorInterface
*/
public function getServiceLocator() {
return $this->serviceManager;
}
public function attach(EventManagerInterface $events) {
$sharedManager = $events->getSharedManager();
$this->listeners[] = $sharedManager->attach('ZfcUser\Authentication\Adapter\AdapterChain', 'authenticate.success', array($this, 'userLog'));
}
public function userLog(Event $e) {
$em = $this->getServiceLocator()->get('doctrine.entitymanager.orm_default');
$userlog = new UserLogin(); // User Login is my User Log table to record login details as below
$user = $em->getRepository('MyUser\Entity\User')
->findOneBy(array('id' => $e->getIdentity()));
$userlog->setGroupId($user->getId());
$userlog->setUserId($e->getIdentity());
$userlog->setEmail($_POST['identity']);
$userlog->setIpAddresses($_SERVER['SERVER_ADDR']);
$em->persist($userlog);
$em->flush();
}
}
I want a simple way to access $app and $request in my controller classes. The document says to do this,
public function action(Application $app, Request $request) {
// Do something.
}
but it doesn't look right to have to inject $app and $request to every method. Is there a way to include $app and $request to every controller by default, maybe using the constructor? I'd like to be able to use it as $this->app.
Thanks.
In the Controllers as Services part of the documentation you can see how to inject dependencies to controller classes via the constructor - in that case a repository.
It's possible :
Create a ControllerResolver.php somewhere in your project and put this inside :
namespace MyProject;
use Silex\ControllerResolver as BaseControllerResolver;
class ControllerResolver extends BaseControllerResolver
{
protected function instantiateController($class)
{
return new $class($this->app);
}
}
Then register it in your app (before $app->run();):
$app['resolver'] = function ($app) {
return new \MyProject\ControllerResolver($app, $app['logger']);
};
Now you can create a base controller for your app, for example :
namespace MyProject;
use Silex\Application;
use Symfony\Component\HttpFoundation\Response;
abstract class BaseController
{
public $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function getParam($key)
{
$postParams = $this->app['request_stack']->getCurrentRequest()->request->all();
$getParams = $this->app['request_stack']->getCurrentRequest()->query->all();
if (isset($postParams[$key])) {
return $postParams[$key];
} elseif (isset($getParams[$key])) {
return $getParams[$key];
} else {
return null;
}
}
public function render($view, array $parameters = array())
{
$response = new Response();
return $response->setContent($this->app['twig']->render($view, $parameters));
}
}
And extend it :
class HomeController extends BaseController
{
public function indexAction()
{
// now you can use $this->app
return $this->render('home.html.twig');
}
}
I need to create a service layer for Zend framework two controller functions in order to decouple the services from controllers.
You're going to need to use the ServiceManager (SM) in order to make this work properly.
This is just an example of how I have done it:
In your ModuleName/src/ModuleName/ create a folder named Service and create your ExampleService.php, Example:
namespace ModuleName\Service;
class ExampleService
{
public function SomeFunctionNameHere()
{
echo 'Hello World';
}
}
Now edit your Module.php and add the Service Layer to your invokables, IE:
public function getServiceConfig()
{
return array(
'invokables' => array(
'ModuleName\Service\ExampleService' => 'ModuleName\Service\ExampleService',
),
);
}
Now edit your ModuleNameController.php
protected $service_example;
public function indexAction()
{
$service = $this->getServiceExample()->SomeFunctionNameHere();
}
private function getServiceExample()
{
if (!$this->service_example) {
$this->service_example = $this->getServiceLocator()->get('ModuleName\Service\ExampleService');
}
return $this->service_example;
}
This should get you started.
Depending on the functionality you are looking for from your service, you might be able to create a custom Controller Plugin. For example, here's a custom controller plugin I wrote to get a user's access level.
Application/Controller/Plugin/GetAccessLevel.php
namespace Application\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
class GetAccessLevel extends AbstractPlugin implements ServiceLocatorAwareInterface
{
/**
* Set the service locator.
*
* #param ServiceLocatorInterface $serviceLocator
* #return GetAccessLevel
*/
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
/**
* Get the service locator.
*
* #return \Zend\ServiceManager\ServiceLocatorInterface
*/
public function getServiceLocator()
{
return $this->serviceLocator;
}
/**
* Takes an array of role objects and returns access level
*
* #param array of MyModule\Entity\Role objects
* #return int Access Level
*/
public function __invoke(array $roles)
{
// Default access level
$accesslevel = 0;
// Get Service Locator for view helpers
$controllerPluginManager = $this->getServiceLocator();
// Get application service manager
$serviceManager = $controllerPluginManager->getServiceLocator();
// Get application config
$config = $serviceManager->get('Config');
// Get the role associated with full access from config
$fullAccessRole = $config['appSettings']['full_access_role'];
// Does user have the role for full access?
foreach ($roles as $roleObject) {
if($roleObject->getName() == $fullAccessRole) {
$accesslevel = 1;
break;
}
}
// Return access level
return $accesslevel;
}
}
Then add the plugin to the configuration.
./module/Application/config/module.config.php
'controller_plugins' => array(
'invokables' => array(
'getAccessLevel' => 'Application\Controller\Plugin\GetAccessLevel'
)
),
Now every controller will have access to this plugin.
Some Controller
public function someAction() {
$accessLevel = $this->getAccesslevel(array('User Role Entities Go Here'));
}
In the doc it's said:"By default, the Zend Framework MVC registers an initializer that will inject the ServiceManager instance, which is an implementation of Zend\ServiceManager\ServiceLocatorInterface, into any class implementing Zend\ServiceManager\ServiceLocatorAwareInterface."
so I tried this:
interface ModelResourceInterface extends ServiceLocatorAwareInterface
{
}
interface ServiceModelResourceInterface extends ModelResourceInterface
{
public function fetch($uri, $method, $parameters, $options, $encodeType);
}
namespace Ssports\Model\Resource\Service\Http;
use Ssports\Model\Resource\Service\ServiceModelResourceInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Http\Client;
use Zend\Http\Request;
use Ssports\Model\Resource\Service\ConnectionException;
abstract class AbstractHttpServiceModelResource implements ServiceModelResourceInterface
{
/**
*
* #var Zend\ServiceManager\ServiceLocatorInterface;
*/
protected $serviceLocator;
/**
* Constructor
*/
function __construct()
{
$this->init();
}
/**
* Extend Constructor
*/
public function init()
{}
/**
* (non-PHPdoc)
*
* #see \Zend\ServiceManager\ServiceLocatorAwareInterface::setServiceLocator()
*
*/
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
/**
* (non-PHPdoc)
*
* #see \Ssports\Model\Resource\Service\ServiceModelResourceInterface::fetch()
*
*/
public function fetch($uri, $method, $parameters = null, $options = null, $encodeType = null)
{
try {
//something raise \RuntimeException
} catch (\RuntimeException $e) {
$this->getServiceLocator()->get('Log\Web');
throw new ConnectionException();
}
}
/**
* (non-PHPdoc)
*
* #see \Zend\ServiceManager\ServiceLocatorAwareInterface::getServiceLocator()
*
*/
public function getServiceLocator()
{
return $this->serviceLocator;
}
}
I extend this abstract class with some model resource class, and run it, and an exception throw to say that I'm calling get on a non-object.
Seem that the service manager is not being injected to my abstract class, and the return of getServiceLocator is NULL.
Any thing I missed to make it right?
Have you tried to use the service locator trait?
It can be found in \Zend\ServiceManager\ServiceLocatorAwareTrait
However this requires PHP 5.4 to work...
To use a trait do the following
class Class1
{
use Zend\ServiceManager\ServiceLocatorAwareTrait;
}
You can access the service locator then, or at least that is how i had to do it when i needed to load the service locator.