Symfony2: How to access to service from repository - php

I have class ModelsRepository:
class ModelsRepository extends EntityRepository
{}
And service
container_data:
class: ProjectName\MyBundle\Common\Container
arguments: [#service_container]
I want get access from ModelsRepository to service container_data. I can't transmit service from controller used constructor.
Do you know how to do it?

IMHO, this shouldn't be needed since you may easily break rules like SRP and Law of Demeter
But if you really need it, here's a way to do this:
First, we define a base "ContainerAwareRepository" class which has a call "setContainer"
services.yml
services:
# This is the base class for any repository which need to access container
acme_bundle.repository.container_aware:
class: AcmeBundle\Repository\ContainerAwareRepository
abstract: true
calls:
- [ setContainer, [ #service_container ] ]
The ContainerAwareRepository may looks like this
AcmeBundle\Repository\ContainerAwareRepository.php
abstract class ContainerAwareRepository extends EntityRepository
{
protected $container;
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
}
}
Then, we can define our Model Repository.
We use here, the doctrine's getRepository method in order to construct our repository
services.yml
services:
acme_bundle.models.repository:
class: AcmeBundle\Repository\ModelsRepository
factory_service: doctrine.orm.entity_manager
factory_method: getRepository
arguments:
- "AcmeBundle:Models"
parent:
acme_bundle.repository.container_aware
And then, just define the class
AcmeBundle\Repository\ModelsRepository.php
class ModelsRepository extends ContainerAwareRepository
{
public function findFoo()
{
$this->container->get('fooservice');
}
}
In order to use the repository, you absolutely need to call it from the service first.
$container->get('acme_bundle.models.repository')->findFoo(); // No errors
$em->getRepository('AcmeBundle:Models')->findFoo(); // No errors
But if you directly do
$em->getRepository('AcmeBundle:Models')->findFoo(); // Fatal error, container is undefined

I tried some versions. Problem was solved follows
ModelRepository:
class ModelRepository extends EntityRepository
{
private $container;
function __construct($container, $em) {
$class = new ClassMetadata('ProjectName\MyBundle\Entity\ModelEntity');
$this->container = $container;
parent::__construct($em, $class);
}
}
security.yml:
providers:
default:
id: model_auth
services.yml
model_auth:
class: ProjectName\MyBundle\Repository\ModelRepository
argument
As a result I got repository with ability use container - as required.
But this realization can be used only in critical cases, because she has limitations for Repository.
Thx 4all.

You should never pass container to the repository, just as you should never let entities handle heavy logic. Repositories have only one purpose - retrieving data from the database. Nothing more (read: http://docs.doctrine-project.org/en/2.0.x/reference/working-with-objects.html).
If you need anything more complex than that, you should probably create a separate (container aware if you wish) service for that.

I would suggest using a factory service:
http://symfony.com/doc/current/components/dependency_injection/factories.html
//Repository
class ModelsRepositoryFactory
{
public static function getRepository($entityManager,$entityName,$fooservice)
{
$em = $entityManager;
$meta = $em->getClassMetadata($entityName);
$repository = new ModelsRepository($em, $meta, $fooservice);
return $repository;
}
}
//service
AcmeBundle.ModelsRepository:
class: Doctrine\ORM\EntityRepository
factory: [AcmeBundle\Repositories\ModelsRepositoryFactory,getRepository]
arguments:
- #doctrine.orm.entity_manager
- AcmeBundle\Entity\Models
- #fooservice

Are you sure that is a good idea to access service from repo?
Repositories are designed for custom SQL where, in case of doctrine, doctrine can help you with find(),findOne(),findBy(), [...] "magic" methods.
Take into account to inject your service where you use your repo and, if you need some parameters, pass it directly to repo's method.

I strongly agree that this should only be done when absolutely necessary. Though there is a quite simpler approach possible now (tested with Symfony 2.8).
Implement in your repository "ContainerAwareInterface"
Use the "ContainerAwareTrait"
adjust the services.yml
RepositoryClass:
namespace AcmeBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use AcmeBundle\Entity\User;
class UserRepository extends EntityRepository implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function findUserBySomething($param)
{
$service = $this->container->get('my.other.service');
}
}
services.yml:
acme_bundle.repository.user:
lazy: true
class: AcmeBundle\Repository\UserRepository
factory: ['#doctrine.orm.entity_manager', getRepository]
arguments:
- "AcmeBundle:Entity/User"
calls:
- method: setContainer
arguments:
- '#service_container'

the easiest way is to inject the service into repository constructor.
class ModelsRepository extends EntityRepository
{
private $your_service;
public function __construct(ProjectName\MyBundle\Common\Container $service) {
$this->your_service = $service;
}
}

Extending Laurynas Mališauskas answer, to pass service to a constructor make your repository a service too and pass it with arguments:
models.repository:
class: ModelsRepository
arguments: ['#service_you_want_to_pass']

Related

Passing dynamic arguments to service factory in Symfony

I'm integrating Symfony into an older application having its own dependency container based on PSR-11. Been searching for a solution to merge that DI container to the one Symfony uses, but found nothing. To just make it work, I came with one "hacky" solution which I don't like.
I've created this class. It creates an instance of an old DI container inside of it:
class OldAppServiceFactory
{
private ContainerInterface $container;
public function __construct()
{
$this->container = OldContainerFactory::create();
}
public function factory(string $className)
{
return $this->container->get($className);
}
}
and added proper entries to services.yaml:
oldapp.service_factory:
class: Next\Service\LeonContainer\LeonServiceFactory
OldApp\Repository\Repository1:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\Repository\Repository1'
OldApp\Repository\Repository2:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\Repository\Repository2'
OldApp\configuration\ConfigurationProviderInterface:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- 'OldApp\configuration\ConfigurationProviderInterface'
With above hack, putting those classes in service class constructors works. Unfortunately it looks bad and it'll be pain to extend it with more of those repositories (especially when having 50 of them). Is it possible to achieve something like this in services.yaml?
OldApp\Repository\:
factory: ['#oldapp.service_factory', 'factory']
arguments:
- << PASS FQCN HERE >>
This would leave me with only one entry in services.yaml for a single namespace of the old application.
But, maybe there is other solution for my problem? Been trying with configuring Kernel.php and prepareContainer(...) method, but I also ended with nothing as the old dependencies are in one PHP file returning an array:
return array [
RepositoryMetadataCache::class => static fn () => RepositoryMetadataCache::createFromCacheFile(),
EntityCollection::class => autowire(EntityCollection::class),
'Model\Repository\*' => static function (ContainerInterface $container, RequestedEntry $entry) { ... }
];
You could probably accomplish this easily with a custom compiler pass.
First tag all the old repository classes by loading the directory where they exist:
OldApp\Repository\:
resource: '../src/OldApp/Repository/*'
autowire: false
autoconfigure: false
tags: ['oldapp_repository']
(I think that you may need to also exclude src/OldApp from the default automatic service loading. E.g.:
App\:
resource: '../src/*'
exclude: '../src/{OldApp/Repository,DependencyInjection,Entity,Tests,Kernel.php}'
... but I'm not 100% sure, test this one).
Then create a compiler pass to go through the tags and define a factory for each one:
class OldAppRepositoryCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$taggedServices = $container->findTaggedServiceIds('oldapp_repository');
foreach ($taggedServices as $serviceId => $tags) {
$definition = $container->getDefinition($serviceId);
$definition
->setFactory([new Reference('oldapp.service_factory'), 'factory'])
->addArgument($serviceId);
}
}
}
And in your Application Kernel build() method add the compiler pass:
// src/Kernel.php
namespace App;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
// ...
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new OldAppRepositoryCompilerPass());
}
}
Can't test this right at this minute, but this should get you going in the right direction. For additional details check the docs:
Working with service tags
You can check this example repo where the above is implemented and working. On this repo the OldApp namespace is outside of App and src, so no need to exclude it from automatic service loading.

Adding services to a Controller through "container.service_subscriber" not working as expected

I am trying to use the container.service_subscriber tag on my Controller to make some services available without injecting them through the constructor. In our project we don't want to use the autowiring and also can't use the autoconfigure option.
The structure of the Controller is as follow:
I have a base BaseController which extends from the AbstractFOSRestController of FOSRestBundle which has some common used methods for all my Controllers. That service will be used as parent for my other Controllers.
The service definition looks like this:
WM\ApiBundle\Controller\BaseController:
class: WM\ApiBundle\Controller\BaseController
abstract: true
arguments:
- "#service1"
- "#service2"
- ...
WM\ApiBundle\Controller\UserController:
parent: WM\ApiBundle\Controller\BaseController
public: true
#autowire: true
class: WM\ApiBundle\Controller\UserController
tags:
- { name: 'container.service_subscriber'}
- { name: 'container.service_subscriber', key: 'servicexyz', id: 'servicexyz' }
The class looks like this:
/**
* User controller.
*/
class UserController extends AbstractCRUDController implements ClassResourceInterface
{
public static function getSubscribedServices()
{
return array_merge(parent::getSubscribedServices(), [
'servicexyz' => ServiceXYZ::class,
]);
}
.......
}
The problem I have is, if I set autowire: false, it always automatically sets the full container and with this the appropriate deprecation message (as I am not setting it myself):
User Deprecated: Auto-injection of the container for "WM\ApiBundle\Controller\UserController" is deprecated since Symfony 4.2. Configure it as a service instead.
When setting autowire: true Symfony does respect the container.service_subscriber tag and only sets the partial container (ServiceLocator), which also would solve the deprecation message. I would have expected that autowiring should not make any differences in this case because I am explicitly telling the service which other services it should have.
Am I using the tags wrong or do I have a general problem in understanding how to subscribe a service to a Controller?
The basic issue is that the builtin service subscriber functionality will only inject the service locator into the constructor. A conventional controller which extends AbstractController uses autoconfigure to basically override this and uses setContainer instead of the constructor.
# ApiBundle/Resources/config/services.yaml
services:
_defaults:
autowire: false
autoconfigure: false
Api\Controller\UserController:
public: true
tags: ['container.service_subscriber']
class UserController extends AbstractController
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public static function getSubscribedServices()
{
return array_merge(parent::getSubscribedServices(), [
// ...
'logger' => LoggerInterface::class,
]);
}
public function index()
{
$url = $this->generateUrl('user'); // Works as expected
// $signer = $this->get('uri_signer'); // Fails as expected
$logger = $this->get('logger'); // Works as expected
return new Response('API Index Controller ' . get_class($this->container));
}
}
Results in:
API Index Controller Symfony\Component\DependencyInjection\Argument\ServiceLocator
Indicating that a service locator (as opposed to the global container is being injected).
You can also configure your service to use the setContainer method and eliminate the need for a constructor. Either approach will work.
Api\Controller\UserController:
public: true
tags: ['container.service_subscriber']
calls: [['setContainer', ['#Psr\Container\ContainerInterface']]]
Solution to the problem is to extend the service definition of the Controller with a call to setContainer to inject the '#Psr\Container\ContainerInterface' service:
WM\ApiBundle\Controller\BaseController:
class: WM\ApiBundle\Controller\BaseController
abstract: true
arguments:
- "#service1"
- "#service2"
- ...
calls:
- ['setContainer', ['#Psr\Container\ContainerInterface']]
WM\ApiBundle\Controller\UserController:
parent: WM\ApiBundle\Controller\BaseController
public: true
class: WM\ApiBundle\Controller\UserController
tags:
- { name: 'container.service_subscriber'}
- { name: 'container.service_subscriber', key: 'servicexyz', id: 'servicexyz' }
This will give me a ServiceLocator as container containing only the regiestered services instead of the full container without using the autowire option.
Sidenote: Setting the #service_container would inject the full container.
For completeness, there was already an issue on the symfony project where this was discussed.

Symfony2 Use parameters in EntityRepository

I am wondering if this is even a good practice. But for my project i need to get a parameter from parameters.yml and use it inside EntityRepository.
So for this I created a service but still the call is not executed.
services:
xxx_repository:
class: XXX\DatabaseBundle\Repository\CitiesRepository
calls:
- [setTheParameter, ["%the_parameter%"]]
parameters.yml
...
the_parameter: 14400
...
And inside the CitiesRepository.php I am doing the following:
class CitiesRepository extends EntityRepository
{
/**
* #var
*/
protected $theParameter;
public function setTheParameter($theParameter)
{
$this->theParameter = $theParameter;
}
....
}
But $this->theParameter is always null.
SO i have 2 questions: Is this a healthy habit? And why is the result always null?
You need to use getRepository method of the doctrine service as factory:
xxx_repository:
class: XXX\DatabaseBundle\Repository\CitiesRepository
factory: ["#doctrine", "getRepository"]
arguments: ["DatabaseBundle:City"]
calls:
- ["setTheParameter", ["%the_parameter%"]]
And then you can access to this repository as service in your controller:
$this->get('xxx_repository');

Symfony2 Outside class

I use symfony2 (2.6) and I have class to global variable to twig. For Example, class menu:
namespace Cms\PageBundle\Twig;
use Doctrine\ORM\EntityManager;
class Menu {
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function show(){
/******/
}
}
and services.yml
services:
class_menu:
class: Cms\PageBundle\Twig\Menu
arguments: ['#doctrine.orm.entity_manager']
twig_menu:
class: Cms\PageBundle\Twig\Menu
See:
ContextErrorException in Menu.php line 9:
Catchable Fatal Error: Argument 1 passed to
Cms\PageBundle\Twig\Menu::__construct() must be an instance of
Doctrine\ORM\EntityManager, none given, called in
/home/cms/public_html/app/cache/dev/appDevDebugProjectContainer.php on
line 3834 and defined
General, any class (outside) have problem with the constructor and (argument) doctrine.
Why?
Symfony2 getdoctrine outside of Model/Controller
This error is totally expected. Symfony2 expects to create service instance by invoking the __construct constructor. If you want to keep the single class in play, you will need to remove that __construct and use setter dependency injection instead.
There is an official documentation on this: Optional Dependencies: Setter Injection
Basically, you do not pass the EntityManager instance during the creation of an service instance but rather "set it later".
Hope this helps.
Update:
If you fallback to your original solution, make sure you pass EntityManager in both instances:
services:
class_menu:
class: Cms\PageBundle\Twig\Menu
arguments: ['#doctrine.orm.entity_manager']
twig_menu:
class: Cms\PageBundle\Twig\Menu
arguments: ['#doctrine.orm.entity_manager']

Symfony 2 calling non-existent service "router"

I have a service MailController which is defined like this in my config
services:
mail_controller:
class: Company\Project\Bundle\Controller\MailController
I'm calling the Service in other services
$mailController = $this->get('mail_controller');
Now the error i get is building up on this Question
The container wasn't set on the Controller, so i'm injecting one within the constructor
// MailController
public function __construct() {
$this->setContainer(new Container());
}
Now i'm getting this error:
You have requested a non-existent service "router".
I'm guessing that i need to inject further services whatsoever, but i don't know what to inject, so what do i need to further add so my Controller can work with all services?
My MailController looks like this
namespace Company\Project\Bundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\DependencyInjection\Container;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Doctrine\ORM\EntityManager;
class MailController extends Controller{
public function __construct() {
$this->setContainer(new Container());
}
//Code for mailstuff
}
You're creating a new container rather than injecting the built container so it has no services.
To use your controller you need to inject the pre made service container in to your controller through your service like so..
services:
mail_controller:
class: Company\Project\Bundle\Controller\MailController
calls:
- [ setContainer, [ #service_container ]]
.. and get rid of the setter in your __construct.
injecting the whole service container
calls: - [ setContainer, [ #service_container ]]
defeats the purpose of declaring your controller as a service.
Just inject the service(s) you need in your constructor. The constructor needs the service handed as an parameter and do not extend Controller anymore.
//MailController
use Symfony\Component\Routing\RouterInterface;
class MailController
{
private $router;
public function __construct(RouterInterface $router){
$this->router = $router;
}
//actions
}
Now you need to adjust your services.yml and extend the service with arguments describing the service you need
services:
mail_controller:
class: Company\Project\Bundle\Controller\MailController
arguments:
- #router
et voila,
only one service needed, only one service injected.
If you find yourself injecting too many services in one action, chances are your action/controller is not 'thin' enough.

Categories