Created
February 26, 2013 10:00
-
-
Save MihailoJoksimovic/5037402 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment