This seems to be the fastest and simpliest way to use a controller as service, but I am still missing a step because it doesn't work.
Here is my code:
Controller/service:
// Test\TestBundle\Controller\TestController.php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
/**
* #Route(service="test_service")
*/
class TestController extends Controller {
// My actions
}
Use :
// Test\TestBundle\Controller\UseController.php
// ...
public function useAction() {
$testService = $this->get('test_service');
}
When I do that, I get the error
You have requested a non-existent service "test_service".
When I check the list of services with app/console container:debug, I don't see my newly created service.
What am I missing?
From Controller as Service in SensioFrameworkExtraBundle:
The #Route annotation on a controller class can also be used to assign the controller class to a service so that the controller resolver will instantiate the controller by fetching it from the DI container instead of calling new PostController() itself:
/**
* #Route(service="my_post_controller_service")
*/
class PostController
{
// ...
}
The service attribute in the annotation is just to tell Symfony it should use the specified service, instead of instantiating the controller with the new statement. It does not register the service on its own.
Given your controller:
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
/**
* #Route(service="test_service")
*/
class TestController
{
public function myAction()
{
}
}
You need to actually register the controller as a service with the test_service id:
services:
test_service:
class: Test\TestBundle\Controller\TestController
The advantage of this approach is that you can inject your dependencies into the constructor by specifying them in the service definition, and you don't need to extend the base Controller class.
See How to define controllers as services and Controller as Service in SensioFrameworkExtraBundle.
For future folks, if you decide to use controller-as-a-service, you should better inject your services into the controller via the constructor instead of getting them through a service locator. The former is considered to be an antipattern, while the latter allows easy unit testing and is simply more verbose.
So instead of:
public function useAction() {
$testService = $this->get('test_service');
}
You should:
private $yourService;
public function __construct(YourService $yourService)
{
$this->yourService = $yourService;
}
public function useAction()
{
$this->yourService->use(...);
}
Don't create shortcuts, write solid, maintainable code.
For Symfony 3.4, we don't need to register controllers as services because they are already registered as services with the default services.yml configuration.
You just have to write this :
// current controller
public function myAction() {
$test = $this->get(SomeController::class)->someAction($param);
}
Related
i try to inject Container in my RepositoryClass, but it does not work.
BaseRepository:
<?php
namespace MyApp\ApplicationBundle\Repository;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BaseRepository implements ContainerAwareInterface
{
protected $container;
public function setContainer(ContainerInterface $container=null)
{
echo "container";
var_dump($container);
}
public function __construct()
{
echo __CLASS__;
}
}
services.yml
services:
myapp.base_repository:
class: MyApp\ApplicationBundle\Repository\BaseRepository
calls:
- [ setContainer, [ '#service_container' ] ]
DefaultController:
$baseRep = new BaseRepository();
The only output that i get, is the echo FILE from the BaseRepository Construct.
The second way that i tried, is to inject the GuzzleClient self (this is the reason why i tried to inject the container, because i need my guzzle-configuraton-settings.
services.yml
myapp.base_repository:
class: MyApp\ApplicationBundle\Repository\BaseRepository
arguments: ['#csa_guzzle.client.mce']
BaseRepository:
use GuzzleHttp\Client;
class BaseRepository
{
public function __construct(Client $client)
{
var_dump($client);
echo __CLASS__;
}
}
But then i got the following error:
Type error: Argument 1 passed to
MyApp\ApplicationBundle\Repository\BaseRepository::__construct() must
be an instance of GuzzleHttp\Client, none given, called in
MyApp/src/Chameleon/DefaultBundle/Controller/DefaultController.php on
line 20
Anyone know what i can do?
Thank you!
To get the class that is managed by the Service Container you have to use said container to get the service with that id myapp.base_repository as Twifty says:
$this->get('myapp.base_repository');
// or more generally in classes implementing ContainerAwareInterface:
$this->container->get('myapp.base_repository');
If you create a new instance yourself you will have to manage all dependencies:
// In your controller extending Symfony's Controller:
$repository = new BaseRepository();
$repository->setContainer($this->container);
Similarly if you inject a Guzzle-client into the repository you have to either retrieve the service from the container or create it yourself with all the dependencies:
// $this->get() assumes you are in the controller as well
$repositoryWithClientFromServiceContainer = new BaseRepository(
$this->get('csa_guzzle.client.mce')
);
// This obviously works everywhere
$repositoryWithNewDefaultClient = new BaseRepository(
new GuzzleHttp\Client()
);
Furthermore injecting the service container into a class violates the dependency inversion you try to achieve by using the Service Container in the first place. This means, instead of making your repository ContainerAware you should only add the services you need in that repository, not the whole container. Just as you do in the 2nd example with the Guzzle-client.
Some people argue it's okay for controllers to violate that principle, but I personally prefer controller's being defined as services to be able to quickly see which dependencies they have by looking at the constructor.
As a general rule I would avoid using the ContainerAwareInterface.
Similarly if you inject a Guzzle-client into the repository you have
to either retrieve the service from the container or create it
yourself with all the dependencies:
// $this->get() assumes you are in the controller as well
$repositoryWithClientFromServiceContainer = new BaseRepository(
$this->get('csa_guzzle.client.mce')
);
// This obviously works everywhere
$repositoryWithNewDefaultClient = new BaseRepository(
new GuzzleHttp\Client()
);
Furthermore injecting the service container into a class violates the
dependency inversion you try to achieve by using the Service Container
in the first place. This means, instead of making your repository
ContainerAware you should only add the services you need in that
repository, not the whole container. Just as you do in the 2nd example
with the Guzzle-client.
Some people argue it's okay for controllers to violate that principle,
but I personally prefer [controller's being defined as services][1] to
be able to quickly see which dependencies they have by looking at the
constructor.
As a general rule I would avoid using the ContainerAwareInterface.
[1]: http://symfony.com/doc/current/cookbook/controller/service.html
Thank you.
So, it would be the better solution, if i inject only the guzzleClient, right?
As you can see, i have a few classes that extends from my BaseRepository and they need the guzzleClient.
But how is it possible to inject the guzzleClient for this scenario? If the programmer only want to create his basic "MyRep" Repositoryclass in the controller without any params.
services.yml
myapp.base_repository:
class: MyApp\ApplicationBundle\Repository\BaseRepository
arguments: ['#csa_guzzle.client.mce']
BaseRepository:
use GuzzleHttp\Client;
class BaseRepository
{
private $client = null;
public function __construct(Client $client)
{
var_dump($client);
$this->client = $client;
}
public getClient() {
return $this->client;
}
}
MyRepository:
MyRep extends BaseRepository:
use GuzzleHttp\Client;
class BaseRepository
{
public function __construct()
{
var_dump($this->getClient());
}
}
Thank you!
I have a form event subscriber which needs an entity repository.
I would like to inject this repository dependency ideally without having to use the constructors of the subscriber and its parents because this subscriber is needed in many different forms.
So basically I have the following chain :
Controller calls -> CustomManagerService instantiates-> Form instantiates -> EventSubscriber needs-> EntityRepository
the maanager is already a service. It is a pain both to transmit a constructor repository argument from the manager through the form to the subscriber and it is a pain to set each form as a service.
Why can't I instantiate the repository in the subscriber directly ? I have read it is a bad practice.
EDIT : this is what I have so far :
in my controller :
$unitRepository = $this->getDoctrine()->getRepository('UnitRepository');
$myManager = $this->get('my_manager')
$form = $myManager->createForm($unitRepository);
in myManager:
public function createForm(UnitRepository $unitRepository){
return $this->formFactory->createForm(
new xxxType($unitRepository)
}
in my form:
use MyBundle/AddUnitFieldSubscriber;
protected $unitRepository;
public function __construct(UnitRepository $unitRepository)
{
$this->unitRepository = $unitRepository;
}
public function buildform()
{
$builder->addEventSubscriber(new AddUnitFieldSubscriber($this->unitRepository));
}
in my subscriber:
protected $unitRepository;
public function __construct(UnitRepository $unitRepository)
{
$this->unitRepository = $unitRepository;
}
public function preSetData(FormEvent $event)
{
$unitRepository = $this->unitRepository;
$unitRepository->doStuff()
}
I found this extremely lenghty, and sometimes I have a form calling a subform which is the one using the eventSubscriber. if I set the forms as services, I also sometimes get errors cause I am instantiating them without the required first constructur parameter.
What would be the shortest path to do it right and to not repeat all this knowing only the subscriber need access to the repository ?
Thanks a lot !
I'm really not sure I understood everything or even anything, but I'm going to try a response.
I would suggest you to define a service SubscriberProvider which will be responsible of the instantiation of the subscriber and the injection of the repository in the subscriber (via a setter of the subscriber). You could retrieve an instance of subscriber using a method get, retrieve, create, provide or whatever you prefer of the service SubscriberProvider. You could then inject this provider in another service.
EDIT
Here is the definition of the service related to your form type:
services:
your_own_bundle.form.type.unit:
class: Your\OwnBundle\Form\Type\UnitType
arguments:
- "#doctrine"
tags:
- { name: form.type, alias: unit }
And its class:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Your\OwnBundle\Event\AddUnitFieldSubscriber;
class UnitType extends AbstractType
{
protected $unitRepository;
public function __construct(ManagerRegistry $doctrine)
{
$this->unitRepository = $doctrine->getRepository('UnitRepository');
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventSubscriber(
new AddUnitFieldSubscriber($this->unitRepository)
);
}
Then, you can use this type like that:
$builder->add('available_unit', 'unit', array());
This way, you don't have to pass the repository to your manager.
I am trying to access a service in a Symfony Controller
$session = $this->get('session');
But I get the next error:
PHP Fatal error: Call to a member function get() on a non-object
I thought that Symfony2 had the controllers defined as services by default.
Note: this question was originally asked by Dbugger, but he removed it for no reason, while it was already answered.
Using the container in controllers
get() is only a shortcut function provided by the Symfony base Controller class to access the container.
Your controller must extend this class to use this function:
namespace Acme\ExampleBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
// your actions
}
If you don't want to depend on this class (for some reasons) you can extend ContainerAware to get the container injected and use it like in the get() shortcut:
namespace Acme\ExampleBundle\Controller;
use Symfony\Component\DependencyInjection\ContainerAware;
class DefaultController extends ContainerAware
{
public function exampleAction()
{
$myService = $this->container->get('my_service');
// do something
}
}
Creating controllers on your own
Controllers are not defined as services per default, you can define them, but it's not needed to get the container. If a request is made, the routing framework determines the controller, which need to be called. Then the controller gets constructed and the container is injected via the setContainer() method.
But if you construct the controller on your own (in a test or anywhere else), you have to inject the container on your own.
$controller = new DefaultController();
$controller->setContainer($container);
// $container comes trough DI or anything else.
How can I inject a service (the service that I created) into my Controller?
A setter injection would do.
<?php
namespace MyNamespace;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
public function setMyService(MyService $myService)
{
$this->myService = $myService;
}
public function indexAction()
{
//Here I cannot access $this->myService;
//Because the setter is not called magically!
}
}
And my route settings :
// Resources/routing.yml
myController_index:
pattern: /test
defaults: { _controller: "FooBarBundle:MyController:index" }
I'm setting the service in another bundle :
// Resources/services.yml
parameters:
my.service.class: Path\To\My\Service
services:
my_service:
class: %my.service.class%
When the route is resolved, the service is not injected ( I know it shouldn't ).
I suppose somewhere in a yml file, I have to set:
calls:
- [setMyService, [#my_service]]
I am not using this Controller as a service, it's a regular Controller that serves a Request.
Edit: At this point in time, I am getting the service with $this->container->get('my_service'); But I need to inject it.
If you want to inject services into your controllers, you have to define controllers as services.
You could also take a look at JMSDiExtraBundle's special handling of controllers — if that solves your problem. But since I define my controllers as services, I haven't tried that.
When using the JMSDiExtraBundle you DON'T have to define your controller as a service (unlike #elnur said) and the code would be:
<?php
namespace MyNamespace;
use JMS\DiExtraBundle\Annotation as DI;
use Path\To\My\Service;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
/**
* #var $myService Service
*
* #DI\Inject("my_service")
*/
protected $myService;
public function indexAction()
{
// $this->myService->method();
}
}
I find this approach very nice because you avoid writing a __construct() method.
Since it's 2017 ending now and there is no tag for Symfony 3 or upcoming Symfony 4 (and I think there should not be), this problem is solvable in a native much better way.
If you are still struggling and somehow ended up on this page and not in Symfony docs, then you should know, that you do not need to declare controller as service, as it is already registered as one.
What you need to do, is check you services.yml:
# app/config/services.yml
services:
# default configuration for services in *this* file
_defaults:
# ...
public: false
Change public: false to public:true if you want all services to be public.
Or explicitly add a service and declare it public:
# app/config/services.yml
services:
# ... same code as before
# explicitly configure the service
AppBundle\Service\MessageGenerator:
public: true
And then in your controller you can get the service:
use AppBundle\Service\MessageGenerator;
// accessing services like this only works if you extend Controller
class ProductController extends Controller
{
public function newAction()
{
// only works if your service is public
$messageGenerator = $this->get(MessageGenerator::class);
}
}
Read more:
Public vs Private services
Services in Container
If you don't want to define your controller as a service, you can add a listener to the kernel.controller event to configure it just before it is executed. This way, you can inject the services you need inside your controller using setters.
http://symfony.com/doc/current/components/http_kernel/introduction.html#component-http-kernel-kernel-controller
is there any way to preprocess controller data somehow. I'm going to take param from session, validate it and assign it as controller property and use it as $this->myVar inside actions of some controller or all of them if possible. Using controller's constructor gives me nothing, I couldn't access request and session data. Thanks!
UPD:
Thanks, jkucharovic, very good solution.
Also there is a bit dirtier solution, without injecting: setContainer() method, which has been called straight after $controller = new Controller();
use Symfony\Component\DependencyInjection\ContainerAwareInterface,
Symfony\Component\DependencyInjection\ContainerInterface;
class AppServiceController extends Controller {
private $my_property;
/**
* Used as constructor
*/
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer($container);
$this->my_property = 'foo';
// your controller code
}
}
I'm not sure what you wan't to do is very usefull. A Controller instance will be created each time the controller is called. The session and request will be different each time you call the controller.
I think you should create a BaseController class extending Controller class with a shortcut method to access your MyVar value in session.
class BaseController extends Controller
{
public function getMyVar()
{
return $this->get('session')->get('MyVarSessionKey');
}
}
All your other Controller will extend from this BaseController.
To get the request, just use the shortcut method provided by Controller class, Controller::getRequest().
If you want to use services in __construct method, you have to inject that services first. Then you can use them before any other methods. For example:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session;
public function __construct(Request $request, Session $session)
{
…
}