6

In Zend Framework 1 I had several mappers which inherited a setDbTable and getDbTable from a parent Mapper class.

Now in ZF2 a face the problem that I need the service manager in a model and I do not have a clue as how to get it:

    class Mapper
    {
        protected $tableGateway;
        protected $module = 'application';

        public function setTableGateway($table)
            {
                if (is_string($table)) {
                    $class = $this->module . '\Model\DbTable\\' . ucfirst($table);
                    $sm = $this->getServiceLocator(); <= Fatal error: Call to undefined method Mapper::getServiceLocator()
                    $tableGateway = (class_exists($class)) ? $sm->get($class) : $sm->get(new TableGateway($table));
                } else {
                    $tableGateway = $table;
                }

                if (!$tableGateway instanceof Zend\Db\TableGateway\AbstractTableGateway) {
                    throw new \Exception('Invalid table data gateway provided');
                }
                $this->tableGateway = $tableGateway;
                return $this;
            }

        // more code

The line:

$sm = $this->getServiceLocator();

gives a fatal error:

Call to undefined method Application\Model\Mapper\Doc::getServiceLocator()

How would I get the service manager in my model? Or am I not doing things the ZF2 way? I know how to get the service manager in my controller and pass the tableGateway to the mapper but that seems like a lot of duplication of code to me.

akond
  • 15,865
  • 4
  • 35
  • 55
tihe
  • 2,452
  • 3
  • 25
  • 27
  • 1
    It is generally a very bad idea to inject service managers in your models. Try to inject the dependency in your class instead of relying on the service container to be injected. It's the "Dependency injection" vs "Service container" discussion and it would be a good idea to read some of this matter and understand why they call this type of injection often an anti-pattern. – Jurian Sluiman Dec 24 '12 at 21:15
  • @Jurian Sluiman: difficult discussion for me as I learned only recently about dependency injection, but I think I get the basic idea. What about Andy's answer below (which I accepted because it works) ? Would that be an anti-pattern as well in your opinion? – tihe Dec 27 '12 at 12:04
  • you can have a good read with this article: http://blog.ircmaxell.com/2012/08/object-scoping-triste-against-service.html (for me, Anthony Ferrara is a very well respected developer). It is not really that black/white, but a similar question is asked here on SO about the same matter: http://stackoverflow.com/questions/9011787/why-the-service-locator-is-a-anti-pattern-in-the-following-example – Jurian Sluiman Dec 29 '12 at 11:59

2 Answers2

8

First of all, I think you mean that you want to access the service manager from a mapper class and not a model. I would refrain from doing the latter. Please see my comment to Raj's answer for more details.

Secondly, there are many ways to go about this. In this answer, I will give you an example of one approach and merely mention another.

Looking at the service manager's documentation (scroll a bit down), it states that a default initializer is added as default. This initializer checks to see if an object that is being retrieved from the service manager implements Zend\ServiceManager\ServiceLocatorAwareInterface. If it does, the service manager is injected into the object. Thus, this can happen automatically if you simply implement the interface in your mapper classes. You could use an abstract base class to avoid rewriting this for every mapper class. Perhaps something like the below.

Base mapper class:

namespace User\Mapper;

use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class AbstractMapper implements ServiceLocatorAwareInterface {
    protected $service_manager;

    public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
    {
        $this->service_manager = $serviceLocator;
    }

    public function getServiceLocator()
    {
        return $this->service_manager;
    }
}

Mapper class:

namespace User\Mapper;

use User\Mapper\AbstractMapper;

class UserMapper extends AbstractMapper {
    public function doSomething() {
        $sm = $this->getServiceLocator();
        $sm->get('something');
    }
}

Since the service manager is injected when the initializer is run, the mapper should be retrieved from the service manager. If you do not want/have to inject anything into your mapper class, then an invokable can suffice. They can be added under the invokables key, which is nested under the service_manager key in module_name/config/module.config.php. Or, it can be configured in the module class' getServiceConfig method. However, your mapper classes will most likely have some dependencies now or in the future, so you will probably want to use a factory instead. This way, you could for instance have the service manager inject a table gateway into the mapper classes.

// Remember to include the appropriate namespaces
return array(
    'factories' => array(
        'User\Mapper\UserMapper' => function($service_manager) {
            $table_gateway = $service_manager->get('User\Model\DbTable\UserGateway');
            return new UserMapper($table_gateway);
        },
    ),
);

The above can be added in a getServiceConfig method within the module's Module.php file - or add the factories key within the service_manager key in module_name/config/module.config.php. You will still have to add a factory that creates the database gateway; the above is just an example.

This is how I would go about it. Of course one could simply have a getter and setter method for the service manager in the mapper class and access these from the controller (the controller has a getServiceLocator method) and inject it like that. I would not go with that approach myself, though.

ba0708
  • 10,180
  • 13
  • 67
  • 99
2

Note: As Jurian Sluiman and andy124 pointed out in their comments, never inject service manger and depend on service manager inside your domain specific model/object, which is not a good practice as this will make your domain specific object rigid and impact on portability. The solution below is more specific to the question asked

I follow the below steps to get sevicelocator(service manager) in my class, models, etc
1. create class which implement ServiceMangerAwareInterface
2. Define the entry in serviceconfig in your Module.php like this

    public function getServiceConfig(){
    return array(
        'factories' => array(
                    /* Models */
            'MyModule\MyModel' =>  function($sm) {
                return new Models\MyModel($sm);
            },
                    /* Entities */
            'MyModule\MyEntity1' =>  function($sm) {
                return new Entity\MyEntity1($sm);
            },
            'MyModule\MyEntity2' =>  function($sm) {
                return new Entity\MyEntity2($sm);
            },
        .
        .
        .
        ),
    );
`
  1. Access your model or class with service manager like below (ex. under controller)
    $model = $this->getServiceLocator()->get('MyModule\MyModel');
    
You can also do the service manager configuration in module.config.php
    'service_manager' => array(
        'factories' => array(
             'MyModel' => 'MyModule\Models\MyModel',
             'MyEntity' => 'MyModule\Entity\MyEntity',
         ),
    ),
    
4. Access your model or class with service manager like below (ex. under controller)
    $model = $this->getServiceLocator()->get('MyModel');
    
Raj
  • 1,156
  • 11
  • 15
  • You have some errors in your answer: 1. you don't need to implement `ServiceManagerAwareInterface` if you have your own factory where you inject the SM already. 2. n general the idea to inject the SM in your domain models is a very, very bad idea and 3. In your `config.php` (where you probably mean the `module.config.php`) you use the model as an invokable service but you use the factory service. Because of these things I down vote you, but you can easily adjust these things in your answer to let me remove it again. – Jurian Sluiman Dec 25 '12 at 09:08
  • @Jurian Sluiman thanks for pointing the errors, I had updated my answer, another you had stated **in general the idea to inject the SM in your domain models is a very, very bad idea** could you highlight why it's a bad idea, can you point any article related to it, I like to know more about it, thanks – Raj Dec 25 '12 at 14:01
  • 1
    @Raj Personally, I try to keep my models clean from any framework specific behavior such that it is not combined with the business logic. In this way, the portability of the business logic is preserved and the logic is isolated. I do not have so much practical experience with ZF2 yet, but off the top of my head, I cannot think of a scenario where I would need the service manager in my models. Using dependency injection, the models need not be concerned with _how_ the dependencies are resolved and does not need to resolve them. This should probably be done from within a service layer. – ba0708 Dec 26 '12 at 14:46
  • 1
    @andy124, I got the point, after both of your comments I re-factored all my model's and entities such that there are free from service managers and I had used service manager factory to inject all dependency for the models and the Model's responsible for injecting dependency to underlying objects (ex Entities), thanks much. – Raj Dec 27 '12 at 16:11
  • Raj it is much what andy124 says. See my comment on top at the question itself. Especially in your domain layer, I would only rely on domain objects and nothing much more actually. – Jurian Sluiman Dec 29 '12 at 12:01
  • @Jurian Sluiman, I mentioned (both) which include you. Thanks much for pointing out this, after reading your comment I researched more on this subject and now was much more clear on this concept, I will take care not to inject serviceManager to any of my models or underlying(domain) objects – Raj Dec 29 '12 at 13:34