Skip to content

Instantly share code, notes, and snippets.

@MihailoJoksimovic
Created February 26, 2013 10:00
Show Gist options
  • Save MihailoJoksimovic/5037402 to your computer and use it in GitHub Desktop.
Save MihailoJoksimovic/5037402 to your computer and use it in GitHub Desktop.
On Tue, Feb 19, 2013 at 3:37 AM, roberto blanko <robertoblanko@gmail.com> 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