Entity Manager in Service in Zend Framework 2

2019-03-11 13:03发布

问题:

I have written a custom service for my module. This service provides public static functions which should validate a given token.

Now i want to implement another public static function which checks if an Doctrine-Entity exists or not. For this case i need the object-manager or the service-locator in my service.

class ApiService 
{
    const KEY_LENGTH = 10;
    const USE_NUMBERS = true;
    const USE_CHARS = true;

    public static function isValid($apiKey) {
        $isValid = false;
        # more code tbd
        $isValid = self::exists($apiKey);
        return $isValid;
    }

    public static function exists($apiKey) {
    # Insert Object-Manager here

        $validator = new \DoctrineModule\Validator\ObjectExists(array(
           'object_repository' => $objectManager->getRepository('Application\Entity\User'),
           'fields' => array('email')
        )); 
    }
}
  1. Is it "best-practice" to implement my functions as public static and call them as static methods?

  2. What is the best practice to inject the object-manager into my doesEntityExist() function?

回答1:

The best approach would be to completely remove the static methods from your class here. ZF2 makes it really easy to fetch services by their name, so you shouldn't really need static methods for such a use case.

First of all, clean up your service:

namespace MyApp\Service;

use Doctrine\Common\Persistence\ObjectRepository;
use DoctrineModule\Validator\ObjectExists;

class ApiService
{
    // ...

    protected $validator;

    public function __construct(ObjectRepository $objectRepository)
    {
        $this->validator = new \DoctrineModule\Validator\ObjectExists(array(
           'object_repository' => $objectRepository,
           'fields'            => array('email')
        )); 
    }

    public function exists($apiKey)
    {
        return $this->validator->isValid($apiKey);
    }

    // ...
}

Now define a factory for it:

namespace MyApp\ServiceFactory;

use MyApp\Service\ApiService;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class ApiServiceFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $entityManager = $serviceLocator->get('Doctrine\ORM\EntityManager');
        $repository = $entityManager->getRepository('Application\Entity\User');

        return new ApiService($repository);
    }
}

Then map the service name to the factory (usually in your module):

namespace MyApp;

use Zend\ModuleManager\Feature\ConfigProviderInterface;

class Module implements ConfigProviderInterface
{
    public function getConfig()
    {
        return array(
            'service_manager' => array(
                'factories' => array(
                    'MyApp\Service\ApiService'
                        => 'MyApp\ServiceFactory\ApiServiceFactory',
                ),
            ),
        );
    }
}

NOTE: you may want to simply use a closure instead of defining a separate factory class, but having factory classes gives you a small performance boost when you're not using the service. Also, using a closure in configuration means you cannot cache the merged configuration, so consider using the method suggested here.

Here's an example without the factory class (again, consider using the approach explained above):

namespace MyApp;

use Zend\ModuleManager\Feature\ServiceProviderInterface;

class Module implements ServiceProviderInterface
{
    public function getServiceConfig()
    {
        return array(
            'factories' => array(
                'MyApp\Service\ApiService' => function ($sl) {
                    $entityManager = $serviceLocator->get('Doctrine\ORM\EntityManager');
                    $repository = $entityManager->getRepository('Application\Entity\User');

                    return new MyApp\Service\ApiService($repository);
                },
            ),
        );
    }
}

Now you can use the service in your controllers:

class MyController extends AbstractActionController
{
    // ...

    public function apiAction()
    {
        $apiService = $this->getServiceLocator()->get('MyApp\Service\ApiService');

        if ( ! $apiService->isValid($this->params('api-key')) {
            throw new InvalidApiKeyException($this->params('api-key'));
        }

        // ...
    }

    // ...
}

You can also retrieve it wherever you have the service manager:

$validator = $serviceLocator->get('MyApp\Service\ApiService');

As an additional suggestion, consider simplifying your service. Since isValid is already a method of your validator, you could simply return the validator itself (hereby using the closure method for simplicity):

namespace MyApp;

use Zend\ModuleManager\Feature\ServiceProviderInterface;
use DoctrineModule\Validator\ObjectExists;

class Module implements ServiceProviderInterface
{
    public function getServiceConfig()
    {
        return array(
            'factories' => array(
                'MyApp\Validator\ApiKeyValidator' => function ($sl) {

                    $entityManager = $serviceLocator->get('Doctrine\ORM\EntityManager');
                    $repository = $entityManager->getRepository('Application\Entity\User');
                    new ObjectExists(array(
                       'object_repository' => $objectRepository,
                       'fields'            => array('email')
                    )); 
                },
            ),
        );
    }
}


回答2:

Personally, I'd make the service a 'service' and put it in the ServiceManager. In addition I'd consider refactoring the code. Right now you have a dependency on the ObjectExists validator, which in turn depends on and entity repository, and that depends on the entity manager. It would be much simpler to compose the validator outside the service and inject it from a factory. That way, if you ever need to use a different validator, you just hand it a different one.

class ApiService
{
    protected $validator;

    public function isValid($apiKey)
    {
         // other code
         $isValid = $this->exists($apiKey);
    }

    public function exists($apiKey)
    {
        return $this->getValidator()->isValid($apiKey);
    }

    public function setValidator(\Zend\Validator\AbstractValidator $validator)
    {
         $this->validator = $validator;
         return $this;
    }

    public function getValidator()
    {
        return $this->validator;
    }
}

In Module.php create the service as a factory method, or better still as a factory class, but that's left as an exercise for you :)

public function getServiceConfig()
{
    return array(
        'factories' => array(
            'ApiService' => function($sm) {
                $em = $sm->get('Doctrine\ORM\EntityManager');
                $repo = $em->getRepository('Application\Entity\User');
                $validator = new \DoctrineModule\Validator\ObjectExists($repo, 
                   array('fields' => array('email')));
                $service = new ApiService();
                $service->setValidator($validator);
                return $service;
            },
        ),
    );
}

Now if you need a different EntityManager, a different Entity repository, or even a whole different validator you only need to change a couple of lines above rather than having to delve into your services code.