How to setup class hierarchy with constructor injected services - php

I have a few logic classes implemented as services. They do some similar stuff, so I wanted to setup a hierarchy:
namespace \Company\Bundle\Service;
abstract class ParentLogic {
protected $user;
public function __construct($security_token_storage) {
$this->user = $security_token_storage->getToken()->getUser();
}
}
class ChildLogic extends ParentLogic {
}
Here is how I have the services setup
services:
parentlogic:
abstract: true
class: Company\Bundle\Service\ParentLogic
arguments: [#security.token_storage]
childlogic:
class: Company\Bundle\Service\ChildLogic
parent: parentlogic
however when I try to use the childlogic service in a controller
namespace Company\Bundle\Controller;
class TestController {
static public function getLogicService() {
return $this->get('childlogic');
}
}
I get an error saying the argument to the constructor is missing:
Warning: Missing argument 1 for Company\Bundle\Service\parentlogic::__construct(), called in /file/path/to/company/app/cache/dev/classes.php on line 2220 and defined
Is this possible? If so what am I doing wrong, or better how is it done correctly?

What Cerad said in the comment is probably true. Constructor arguments are not used on parent services. We have a similar setup with controllers where we have an AbstractController as a parent service and concrete controllers that inherit from AbstractController. However in our service definition we add any dependencies to the parent using setter-calls:
services:
abstract_controller:
class: Bundle\Controller\AbstractController
abstract: true
calls:
- [setSomething, ['#something']]
- [setAnotherThing, ['#anotherThing']]
concrete_controller:
class: Bundle\Controller\ConcreteController
parent: abstract_controller
Works like a charm.
I reckon if you wish to use constructor arguments instead you should specify them explicitly in your child service definitions as well since on creation of the child service the parent constructor will be called.

Related

Sharing dependancies in Symfony services without passing parameters to sub classes

A lot of my services rely on a PDO connection to an external database (for reasons specific to my application, it made sense to use this strategy over using Doctrine). To start a PDO connection, each services needs a data source name, username and password. This leaves my services.yml containing much of the same arguments for each service:
#AppBundle\Resources\config\services.yml
# ...
QueryDataBuilderHelper:
class: AppBundle\Services\QueryDataBuilderHelper
arguments: [ "%database_host%", "%database_user%", "%database_password%" ]
ZipCodeClass:
class: AppBundle\Services\ZipCodeClass
arguments: [ "%database_host%", "%database_user%", "%database_password%" ]
# ...
Is it possible to define the connection somewhere and reference it in all services without passing parameters to each one?
The Symfony cookbook recommends using parent services and extending the parent services. When I try to use the subclass, the subclass doesn't pull the arguments of the superclass:
#AppBundle\Resources\config\services.yml
DBConnectionHelper:
class: AppBundle\Services\DBConnectionHelper
arguments: [ "%database_host%", "%database_user%", "%database_password%" ]
DBSubClass:
class: AppBundle\Services\DBSubClass
parent: DBConnectionHelper
arguments: [ "%unrelated_parameter%" ]
//AppBundle\Services\SuperClassService.php
namespace AppBundle\Services;
class DBConnectionHelper
{
public function __construct($dsn, $user, $password){
$this->DB_connection = new \PDO($dsn, $user, $password);
}
}
//AppBundle\Services\DBSubClass.php
namespace AppBundle\Services;
class DBSubClass extends DBConnectionHelper
{
public function __construct($unrelated_param){
//Calling parent::__construct() here will require the parameters again.
//Which is what I am trying to avoid...
// Outputs a notice that DB_Connection isn't set.
var_dump($this->DB_Connection());
$this->unrelated_param = $unrelated_param;
}
}
I've used the constructor here instead of setters because these dependancies are not optional. The Symfony docs suggest that setters should only be used when dependancies are optional.
What you need to use here is called an Abstract Service. Luckily for you, Symfony is providing a way to achieve this. The options you're looking for are abstract and parent. You can read the entire chapter here.
I will try to briefly explain the example from documentation, so that you can grasp the idea.
# ...
services:
# ...
mail_manager:
abstract: true
calls:
- [setMailer, ["#my_mailer"]]
- [setEmailFormatter, ["#my_email_formatter"]]
newsletter_manager:
class: "NewsletterManager"
parent: mail_manager
greeting_card_manager:
class: "GreetingCardManager"
parent: mail_manager
Basically what you need to do is this:
Create an abstract class where you will define all common properties/methods.
Then configure these common method calls in your service file accordingly and add the option abstract. Since your service is an abstract one it means that it cannot be instantiated, so class option here is omitted.
After that create a service like you always do, and don't forget to extend the abstract class. Then register that service in your service.yml file and add the parent option to it, by setting the name of your abstract service.
Repeat the step above as many times as you wish for each child service and you should be good to go.
If you have any questions, leave a comment. Hope this can help you out.
Rather than extend your DBConnectionHelper class, you could inject it as a dependancy via your constructor.
//AppBundle\Services\DBSubClass.php
namespace AppBundle\Services;
class DBSubClass
{
private $dbConnectionHelper;
public function __construct($dbConnectionHelper, $unrelated_param)
{
$this->dbConnectionHelper = $dbConnectionHelper;
$this->unrelated_param = $unrelated_param;
}
}
You can then get your connection using $this->dbConnectionHelper->DB_Connection().
You would also need to configure your service as:
DBSubClass:
class: AppBundle\Services\DBSubClass
arguments: [ "#DBConnectionHelper", "%unrelated_parameter%" ]

Argument 1 passed to __construct must be an instance of Services\ProductManager, none given

in service.yml
test_product.controller:
class: MyBundle\Controller\Test\ProductController
arguments: ["#product_manager.service"]
in controller
class ProductController extends Controller
{
/**
* #var ProductManager
*/
private $productManager;
public function __construct(ProductManager $productManager){
$this->productManager = $productManager;
}
}
in routing.yml
test_product_addNew:
path: /test/product/addNew
defaults: { _controller:test_product.controller:addNewAction }
I want to use ProductManger in contructor to do some stuff but it gives me this error
Catchable Fatal Error: Argument 1 passed to
MyBundle\Controller\Test\ProductController::__construct()
must be an instance of MyBundle\Services\ProductManager,
instance of Symfony\Bundle\TwigBundle\Debug\TimedTwigEngine given,
called in
..../app/cache/dev/appDevDebugProjectContainer.php
on line 1202 and defined
I am new to symfony, any help is appreciated
You have inverted the logical of services.
First, it's your manager wich must be defined as a service because it's it you will need to call from controller.
// services.yml
product_manager:
class: MyBundle\Path\To\ProductManager
Then call directly your manager defined as a service in your controller.
// Controller
class ProductController extends Controller
{
[...]
$this->get('product_manager');
[...]
}
And you do not need to overload __construct() methode. Only call ->get(any_service) where you need it.
Also your route is wrong. You have to define controller from is namespace.
// routing.yml
test_product_addNew:
path: /test/product/addNew
defaults: { _controller:MyBundle:Product:addNew }
Since Symfony 3.3 (released May 2017) you can use contructor injection and autowiring with ease:
# services.yml
services
_defaults:
autowire: true
MyBundle\Controller\Test\ProductController: ~
Keep rest as you already had.
Do you want to know more about there features? Check this post with examples.

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']

Accessing $this->container in controller throws Catchable Fatal Error exception?

I have the following controller:
namespace Acme\CompanyBundle\Controller;
use Symfony\Component\DependencyInjection\Container;
/**
* Company controller.
*
*/
class CompanyController extends Controller
{
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function getData()
{
$userObj = $this->container->get('security.context')->getToken()->getUser();
}
}
In my services.yml file, I have injected Container class:
parameters:
acme.controller.company.class: Acme\ContainerBundle\Controller\CompanyController
services:
acme.controller.company:
class: %acme.controller.company.class%
arguments: [#service_container]
When loading this controller, I get following error:
Catchable Fatal Error: Argument 1 passed to
Acme\CompanyBundle\Controller\CompanyController::__construct() must be
an instance of Symfony\Component\DependencyInjection\Container, none
given, called in C:\wamp\www\symfony\app\cache\dev\classes.php on line
2785 and defined in
C:\wamp\www\symfony\src\Acme\CompanyBundle\Controller\CompanyController.php
line ...
As you could see, this is a simple injection of Container object into a controller but throws nice errors. What is the problem here?
Similar issue is posted in another SO thread here.
You don't need to inject the container in controllers as long as they extend the base Controller class, which yours do.
Just do:
namespace Acme\CompanyBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
/**
* Company controller.
*
*/
class CompanyController extends Controller
{
public function getData()
{
$userObj = $this->get('security.context')->getToken()->getUser();
}
}
By default, routes look something like this:
cerad_player_wanabe_list:
pattern: /player-request/list
defaults:
_controller: CeradPlayerWanabeBundle:Player/PlayerList:list
The Symfony\Component\HttpKernel\HttpKernel::handle($request) method pulls the _controller attribute from the request object. If the attribute has two colons in it then it translates the attribute into a class name and creates an instance using the new operator. If the instance implements the ContainerAwareInterface then the container is injected into the controller instance. The controller service you defined is not used. Hence the error about no argument being passed to the constructor.
On the other hand, if _controller has only one colon then the controller is pulled as a service from the container. There is no checking for the ContainerAwareInterface. It's up to you to inject the dependencies via your service definition.
This is all documented in: http://symfony.com/doc/current/cookbook/controller/service.html
So for this particular question, your route should be something like:
cerad_player_wanabe_list:
pattern: /player-request/list
defaults:
_controller: acme.controller.company:action
This does raise the question of why you are trying to define the controller as a service. The default approach already does exactly what you want so you are not gaining anything.
The rationale for defining services as containers is that you can control exactly what dependencies the controller uses. Makes the controller easier to understand and test.
Injecting the complete container pretty much destroys the value of defining the controller as a service.
Never and never inject the container inside something (services, controller or whatever)
Instead try to inject the securityContext or access it through the helper method of symfony controller as suggested above.
The token it's not an object just because probably the route of the controller it's not under a firewall

Symfony2: How to access to service from repository

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']

Categories