Having seen both ways, initializers and abstract factories (which were new
to me), I’d go for the latter. However I still do not feel good about fact,
that DbAwareFactory::canCreateServiceWithName() is being called for every
service initiation. But if, as you stated, the performance impact is
minimal, I’ll go with it.
Probably a nice way to handle things would be something like this:
'abstract_factories => array(
'DbAwareFactory' => array(
'Main\Model\AccountRepository',
'Main\Model\GoogleRepository',
'Main\Model\StreamRepository',
'Main\Model\TwitterRepository',
)
),
Would pull the class names out of the AbstractFactory and give the service
manager a chance to know whether to call an abstract factory or not.
Anyways, this discussion has been very enlightening to me! Thanks for your
time to both of you!
On Tue, Feb 26, 2013 at 7:15 PM, Matthew Weier O'Phinney
<[email protected]>wrote:
> On Tue, Feb 26, 2013 at 3:46 AM, roberto blanko <[email protected]>
> wrote:
> > In terms of redundancy, this is just a simple example but should make my
> > point clear:
> >
> > return array(
> > 'service_manager' => array(
> > 'factories' => array(
> > 'Main\Model\StreamRepository' => function($sm) {
> > $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
> > return new \Main\Model\StreamRepository($sm, $dbAdapter);
> > },
> > 'Main\Model\AccountRepository' => function($sm) {
> > $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
> > return new \Main\Model\AccountRepository($sm,
> $dbAdapter);
> > },
> > 'Main\Model\UserRepository' => function($sm) {
> > $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
> > return new \Main\Model\TwitterRepository($sm,
> $dbAdapter);
> > },
> > 'Main\Model\ProjectRepository' => function($sm) {
> > $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
> > return new \Main\Model\GoogleRepository($sm, $dbAdapter);
> > },
> > ),
> > ),
> > );
> >
> > Each of my repositories (which extends AbstractTableGateway) needs to
> have
> > the DB adapter injected. It would make things faster, if factories could
> be
> > defined based on inherited classes. E.g. in the case above the factories
> > for all classes inheriting from Main\Model\AbstractRepository need to
> have
> > the same factory. Or is there a solution for this already? I could use an
> > initializer class but this wouldn’t perform very well, would it? Since
> the
> > initializer would be called whether I needed it or not.
>
> There are two approaches you could use here.
>
> First is an initializer. There is actually very little performance
> impact from that, and it would reduce the above to four lines (one
> line for each class, defined as invokables).
>
> Second is to use an AbstractFactory. The idea behind the
> AbstractFactory is when you have many classes that have the same
> creational pattern. You might define it like this:
>
> use Zend\ServiceManager\AbstractFactoryInterface;
> use Zend\ServiceManager\ServiceLocatorInterface;
>
> class DbAwareFactory implements AbstractFactoryInterface
> {
> protected $serviceClasses = array(
> 'Main\Model\AccountRepository',
> 'Main\Model\GoogleRepository',
> 'Main\Model\StreamRepository',
> 'Main\Model\TwitterRepository',
> );
>
> public function
> canCreateServiceWithName(ServiceLocatorInterface $services, $name,
> $requestedName)
> {
> return in_array($name, $this->serviceClasses);
> }
>
> public function createServiceWithName(ServiceLocatorInterface
> $services, $name, $requestedName)
> {
> $dbAdapter = $services->get('Zend\Db\Adapter\Adapter');
> return new $name($services, $dbAdapter);
> }
> }
>
> You would then register it like:
>
> 'abstract_factories => array(
> 'DbAwareFactory',
> ),
>
> Both initializers and abstract factories were designed with this
> problem in mind. While I like initializers, Marco Pivetta points out
> that (a) if you're not aware they exist, they may lead to hard to
> trace issues when injections you don't expect happen, and (b) if you
> do not register an initializer, but thought it was already, again, you
> can have some potential hard-to-trace issues. If you feel those might
> be problems, the abstract factory is the way to go.
>
> > On Mon, Feb 25, 2013 at 3:08 PM, Matthew Weier O'Phinney
> > <[email protected]>wrote:
> >
> >> On Sat, Feb 23, 2013 at 4:31 PM, roberto blanko <
> [email protected]>
> >> wrote:
> >> > Matthew, thanks for this elaborate answer. Makes a lot of things
> clearer
> >> > for me.
> >> >
> >> > So, in that case I stick to injecting everything I need. This really
> >> bloats
> >> > my module.config.php with a lot of redundant stuff and slows me down
> >> quite
> >> > a bit, but I see your point.
> >>
> >>
> >> What kind of redundant stuff are you seeing? Depending on what it is,
> >> and whether others report similarly, we may be able to come up with
> >> some solutions to reduce repetition.
> >>
> >>
> >> > On Tue, Feb 19, 2013 at 5:08 PM, Matthew Weier O'Phinney
> >> > <[email protected]>wrote:
> >> >
> >> >> On Tue, Feb 19, 2013 at 3:37 AM, roberto blanko <
> >> [email protected]>
> >> >> wrote:
> >> >> > one problem drives me nuts, since I started working with ZF2 (never
> >> >> worked
> >> >> > with Version 1):
> >> >> >
> >> >> > I'm in need of the service manager all the time. E.g. to access my
> >> config
> >> >> > in config/autoload/local.php via $sm->get('Config'). I need the
> >> service
> >> >> > manager everywhere. Controllers, models, you name it. And I don’t
> >> want to
> >> >> > pass it around by hand, which would make everything ugly.
> >> >> >
> >> >> > Now I’ve started to implement ServiceLocatorAwareInterface in most
> >> >> classes.
> >> >>
> >> >> Don't.
> >> >>
> >> >> The best option is to define factories for your classes that inject
> >> >> the dependencies for you. When you do that, you no longer have hidden
> >> >> dependencies, and you have everything you need up front. When you
> have
> >> >> an instance of the object, it's fully configured.
> >> >>
> >> >> If you need configuration, you're doing it wrong -- that
> configuration
> >> >> should either be injected, or the objects the configuration
> >> >> defines/configures should be injected.
> >> >>
> >> >> As an example, let's consider a common controller. Let's say it makes
> >> >> use of a TableGateway, and you have one or more methods in that
> >> >> TableGateway that return a paginator instance. You want to be able to
> >> >> specify how many results per page the paginator should use. And for
> >> >> insert()/update() operations, you want this information tied to a
> form
> >> >> so that the validation is done correctly; however, you want a
> >> >> different form based on the operation (new vs. edit). One element of
> >> >> the form needs a DB instance in order to validate.
> >> >>
> >> >> In ZF1, you'd likely do the following:
> >> >>
> >> >> * Pull the "db" resource from the bootstrap (actually, a DB adapter).
> >> >> * Create a TableGateway instance, and inject the DB adapter.
> >> >> * Pull configuration from the registry or the bootstrap.
> >> >> * Use that configuration to tell the TableGateway how many items per
> >> >> page to return for Paginator instances.
> >> >> * You'd create a different form for each operation, and inject the DB
> >> >> adapter you pulled.
> >> >>
> >> >> This is a fair bit of work. And it really, really doesn't belong in
> >> >> your controller, any of it.
> >> >>
> >> >> Let's look at how to do it in ZF2.
> >> >>
> >> >> First, I'd create a factory for the TableGateway.
> >> >>
> >> >> 'my-table-gateway' => function ($services) {
> >> >> $db = $services->get('db');
> >> >> $config = $services->get('config');
> >> >> $perPage = isset($config['per_page']) ? $config['per_page'] :
> >> 10;
> >> >> $tableGateway = new MyTableGateway($db, $perPage);
> >> >> return $tableGateway;
> >> >> }
> >> >>
> >> >> Note that in the _factory_ I'm pulling the configuration, but then
> I'm
> >> >> using the values I retrieve from that in order to construct my table
> >> >> gateway -- this decouples the TableGateway from my configuration.
> Also
> >> >> note that I'm retrieving the adapter from the service manager --
> >> >> dependencies are defined as additional services, and I consume them
> in
> >> >> my factories. How is that dependency defined? As a factory:
> >> >>
> >> >> 'db' => 'Zend\Db\Adapter\AdapterFactoryService',
> >> >>
> >> >> Next, let's consider my form and validators. In 2.1, we added the
> >> >> ability to define form elements, filters, and validators via plugin
> >> >> managers which are managed via the application service manager. This
> >> >> means that I can create factories for my forms that consume these. By
> >> >> default, if you
> >> >>
> >> >> 'validators' => array('factories' => array(
> >> >> 'MyRecordExists' => function ($validators) {
> >> >> $services = $validators->getServiceLocator();
> >> >> $db = $services->get('db');
> >> >> return new \Zend\Validator\Db\RecordExists(array(
> >> >> 'adapter' => $db,
> >> >> 'table' => 'some_table',
> >> >> 'field' => 'some_field',
> >> >> ));
> >> >> },
> >> >> )),
> >> >> 'services' => array('factories' => array(
> >> >> 'MyCustomForm' => function($services) {
> >> >> $validators = $services->get('ValidatorPluginManager');
> >> >> $validatorChain = new \Zend\Validator\ValidatorChain();
> >> >> $validatorChain->setPluginManager($validators);
> >> >>
> >> >> $inputFilterFactory = new \Zend\InputFilter\Factory();
> >> >>
> >> $inputFilterFactory->setDefaultValidatorChain($validatorChain);
> >> >> $inputFilter = new \Zend\InputFilter\InputFilter();
> >> >> $inputFilter->setFactory($inputFilterFactory);
> >> >>
> >> >> return new MyCustomForm('my-custom-form',
> >> >> array('input_filter' => $inputFilter));
> >> >> },
> >> >> )),
> >> >>
> >> >> In the first case, we've provided a factory for
> >> >> Zend\Validator\Db\RecordExists that ensures that it is configured
> with
> >> >> a DB adapter, and the table and field names we require; note that it
> >> >> uses its own service name, which allows us to have multiple instances
> >> >> of the RecordExists validator with different configurations. In the
> >> >> second case, we create a factory for our form. In there, we create an
> >> >> input filter instance that has an InputFilter factory passed to it;
> >> >> that factory is seeded with a validator chain that has our custom
> >> >> validator plugins in it.
> >> >>
> >> >> Note that all of this is decoupled from our controller. This allows
> us
> >> >> to test any piece of it individually, as well as to re-use it in
> areas
> >> >> outside our controller if desired.
> >> >>
> >> >> Now, for the controller:
> >> >>
> >> >> 'controllers' => array('factories' => array(
> >> >> 'MyController' => function ($controllers) {
> >> >> $services = $controllers->getServiceLocator();
> >> >> $tableGateway = $services->get('my-table-gateway');
> >> >> $form = $services->get('MyCustomForm');
> >> >> $controller = new MyController();
> >> >> $controller->setTableGateway($tableGateway);
> >> >> $controller->setForm($form);
> >> >> return $controller;
> >> >> },
> >> >> )),
> >> >>
> >> >> We grab dependencies, instantiate our controller, and inject the
> >> >> dependencies. Nice and clean.
> >> >>
> >> >> Inside our controller, we simply use those dependencies:
> >> >>
> >> >> public function newAction()
> >> >> {
> >> >> $this->form->setValidationGroup('username', 'password',
> >> >> 'confirmation');
> >> >>
> $this->form->setData($this->getRequest()->getPost()->toArray());
> >> >> if (!$this->form->isValid()) {
> >> >> return new ViewModel(array(
> >> >> 'form' => $this->form,
> >> >> 'success' => false,
> >> >> ));
> >> >> }
> >> >> $this->table->insert($this->form->getData());
> >> >> $this->redirect()->toRoute('user/profile');
> >> >> }
> >> >>
> >> >> The controller contains no logic for creating the dependencies, or
> >> >> even fetching them; it simply has setters:
> >> >>
> >> >> protected $table;
> >> >>
> >> >> public function setTableGateway(TableGateway $tableGateway)
> >> >> {
> >> >> $this->table = $tableGateway;
> >> >> }
> >> >>
> >> >> This allows the controller to be tested easily, and keeps the messy
> >> >> logic of obtaining dependencies where it should be -- in factories,
> >> >> elsewhere.
> >> >>
> >> >> Notice that I never pass around the service manager or service
> >> >> locator. If a class needs a dependency, I create a factory for that
> >> >> class, and use that factory to fetch dependencies and inject them.
> >> >> This keeps the logic clean inside the individual classes, as they are
> >> >> only operating on the dependencies passed to them; they don't worry
> >> >> about instantiating dependencies, or about fetching them. They simply
> >> >> assume they have them.
> >> >>
> >> >> > But this has the two downsides for me:
> >> >> >
> >> >> > 1. The classes which make use of my ServiceLocatorAware classes
> need
> >> to
> >> >> > have acces to the service locater, as well, in order to instantiate
> >> the
> >> >> > objects. So even more ServiceLocatorAware classes and even more
> >> >> invokables
> >> >> > to be added to module.config.php.
> >> >>
> >> >> As noted, don't use ServiceLocatorAware. Always pass in dependencies.
> >> >>
> >> >> Second, you _should_ define services for the service manager. This is
> >> >> a good practice. Those services will only be instantiated if the
> >> >> current request needs them. Furthermore, defining them means that
> >> >> someone later can provide _substitutions_ for them. This is
> >> >> tremendously powerful -- it allows developers to extend your class,
> >> >> and still have it injected where it needs to be. (I've done this to
> >> >> work around issues I've found in the past!)
> >> >>
> >> >> > 2. The service manager is a property of all ServiceLocatorAware
> >> classes,
> >> >> > which can be difficult if you want to persist a ServiceLocatorAware
> >> model
> >> >> > e.g. to the session.
> >> >>
> >> >> Again, don't make things ServiceLocatorAware. And if you do, don't
> >> >> persist them to the session. You should persist very little to the
> >> >> session, and your models typically should be plain old PHP objects
> >> >> anyways, without knowledge of persistence. If you're tying them to
> the
> >> >> persistence layer directly, use hydrators to extract information as
> >> >> well as hydrate them, if you need to serialize them into the session.
> >> >>
> >> >> > Now I'm starting to get the feeling the I haven’t really understood
> >> ZF2
> >> >> and
> >> >> > the service manager. Injecting everything cannot be the solution,
> can
> >> it?
> >> >>
> >> >> Yes, it is. Because it solves the problems of re-usability,
> >> >> substitution, and dependency resolution -- all of which were problems
> >> >> in ZF1.
> >> >>
> >> >> > Should I really inject e.g. my global config into all classes?
> >> >>
> >> >> No. Extract the configuration you need inside a factory, and use that
> >> >> to create an instance. Your objects should only get exactly what they
> >> >> need, no more, no less.
> >> >>
> >> >> > And if you
> >> >> > don’t want to have plain entity models but business models with
> some
> >> >> > process logic in them, you cannot abstain from the service manager
> >> >> either.
> >> >>
> >> >> There are ways to do this, too - service layers can compose
> >> >> context-specific service managers, or be injected with the
> >> >> services/configuration that the domain layer may need in order to
> >> >> operate. You can then use this information when constructing domain
> >> >> object instances, to create prototypes of domain objects, or to seed
> >> >> factories. There are many options here.
> >> >>
> >> >>
> >> >> --
> >> >> Matthew Weier O'Phinney
> >> >> Project Lead | [email protected]
> >> >> Zend Framework | http://framework.zend.com/
> >> >> PGP key: http://framework.zend.com/zf-matthew-pgp-key.asc
> >> >>
> >>
> >>
> >>
> >> --
> >> Matthew Weier O'Phinney
> >> Project Lead | [email protected]
> >> Zend Framework | http://framework.zend.com/
> >> PGP key: http://framework.zend.com/zf-matthew-pgp-key.asc
> >>
> >> --
> >> List: [email protected]
> >> Info: http://framework.zend.com/archives
> >> Unsubscribe: [email protected]
> >>
> >>
> >>
>
>
>
> --
> Matthew Weier O'Phinney
> Project Lead | [email protected]
> Zend Framework | http://framework.zend.com/
> PGP key: http://framework.zend.com/zf-matthew-pgp-key.asc
>