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. 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 >
