Symfony 4 Dependency Injection: Inject into object while creating it in code - php

I'm creating a NavigationBuilder class for my backend. With it, I should be able to add navigation items and then get the html (similar to Symfony's FormBuilder). It is used in a Twig Extension Function.
I have an interface and abstract class for a navigation item (e.g. Nav Link, Divider, Heading, etc.) and I'm creating child classes for these specific items.
For some of these nav items (Link) I need the Symfony Router or RequestStack and I want to inject it into the abstract class so I don't have to pass it as an argument to the constructor of every child class I need it in.
I tried using the method of injecting it with setter methods, because I thought Symfony would do it automatically whenever I create a new object, but obviously that isn't the case.
The NavigationItem class:
namespace App\...\Navigation;
use App\...\NavigationItemInterface;
use Symfony\...\UrlGeneratorInterface;
use Symfony\...\Request;
abstract class NavigationItem implements NavigationItemInterface {
private $router;
private $request;
final public function setRouter(UrlGeneratorInterface $router): self {
$this->router = $router;
return $this;
}
final public function setRequest(Request $request): self {
$this->request = $request;
return $this;
}
final public function matchesCurrentRoute(String $route): Bool {
return $this->getRequest()->get('_route') == $route;
}
/** ... **/
}
My service.yaml file:
App\...\NavigationItem:
class: App\...\NavigationItem
calls:
- method: setRequest
arguments:
- '#request_stack'
- method: setRouter
arguments:
- '#router'
I imagine it to work like this:
$builder = new NavigationBuilder();
$builder
->addItem( new HeaderItem('A Heading') ) // No need for injection
->addItem( new LinkItem('Title', 'route') ) // NEED for injection
->build(); // Returns html
I get this error code:
An exception has been thrown during the rendering of a template ("Notice: Undefined property: App...\DashboardItem::$router").

if you declare a property as private only the class itself, but no child class can use it (directly, that is). declare it protected (talking about the router property in your abstract class) see: https://www.php.net/manual/en/language.oop5.visibility.php
apparently that's not the only problem, and I suppose you confuse dependency injection with auto-wiring. dependency injection only means, that an object doesn't create it's own dependencies, but instead they are from the outside/caller via parameters (in constructor, setters, or specific calls).
auto-wiring works in symfony by either fetching an object from the container (however, by default you always get the same object. but there are ways to define a factory or the option to always retrieve a new object, possibly, not quite sure).

Related

Symfony 4.1 - Can't inject dependencies in custom Form base class

I have a service with id manager_provider and I would like to create a base form class called ManagerAwareType with this service injected. Therefore all form classes extending this one would have access to manager_provider:
abstract class ManagerAwareType extends AbstractType
{
/** #var ManagerProvider */
private $managerProvider;
// getter and setter...
}
So my first try was to declare this using _instanceof in services.yaml:
_instanceof:
MyBundle\Form\ManagerAwareType:
tags: [form.type]
calls:
- [setManagerProvider, ["#manager_provider"]]
but when I create the form in Controller $managerProvider is null. Then I tried to do the same programatically in a DependencyInjection\Extension class and the code is being executed without any error (checked with debugger):
$managerAwareType = new Definition(ManagerAwareType::class);
$managerAwareType->addTag('form.type');
$managerAwareType->addMethodCall('setManagerProvider', [new Definition('manager_provider')]);
$container->addDefinitions([$managerAwareType]);
but again it didn't work, the dependency is not injected.
What am I doing wrong?
P.S: I know I could declare my dependency as form option and inject it each time I create the form through the Controller but I prefer this way if possible.
You have to actually tell your ManagerAwareType class about class you want to inject. Usual way is to put it in the __construct function:
abstract class ManagerAwareType extends AbstractType
{
/** #var ManagerProvider */
private $managerProvider;
public function __construct(ManagerProvider $mp)
{
$this->managerProvider = $mp
}
// getter and setter...
}

Symfony __construct usage

I am relatively new to Symfony (version 4) and trying to implement the __construct method for dependency injection.
Currently, I am "injecting" dependencies via my own implementation (before I was aware of the __construct method) like so:
routes.yaml
fetch:
path: /fetch/{req}
controller: App\Controller\Fetch::init
requirements:
req: ".+"
/fetch route calls the init() method, which serves as the constructor.
Controller Class
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use App\Services\Utilities; // a bunch of useful functions
class Fetch extends BaseController {
private $u;
public function init(Utilities $u) {
$this->u = $u; // set the $u member with an instance of $u
}
private function do_fetch(){
$this->u->prettyprint('hello service'); // use one of $u's methods
}
}
If you would indulge me, I came up with this ad-hoc scheme before reading the docs, which detail this almost exactly (I get a cookie).
The one difference is that the docs use __construct() in place of my init() method. The following is an example from the doc page linked above:
// src/Service/MessageGenerator.php
use Psr\Log\LoggerInterface;
class MessageGenerator
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function getHappyMessage()
{
$this->logger->info('About to find a happy message!');
// ...
}
}
But when I swap init() for __construct(), and update the routes.yaml, I get an error.
// .....
class Fetch extends BaseController {
private $u;
public function __construct(Utilities $u) {
$this->u = $u; // set the $u member with an instance of $u
}
// ....
fetch:
path: /fetch/{req}
controller: App\Controller\Fetch::__construct
requirements:
req: ".+"
Its asking me to provide an argument to __construct since that method takes one ($u) but this was not the case when init() was acting as the constructor.
Moreover, I feel like since the __construct() method is a built-in hook, Symfony should know to use it without my having to explicitly tell it to in routes.yaml. However, excluding it throws an error as well.
routes.yaml (__construct not explicitly indicated)
fetch:
path: /fetch/{req}
controller: App\Controller\Fetch
requirements:
req: ".+"
What am I missing here?
__construct is a magic method in PHP. The problem with your init method is that it does not enforce that the object must have an instance of the object you need in order to be built. Sometimes an object property will not be needed. In this case, I recommend creating a setter as a way to optional set that property.Try to make your class properties private, and only allow them to be mutated or retrieved through setters and getters...this will provide a standard API to your obejct, and avoid random state manipulation.
You can use the DIC in Symfony's router to construct your controller instead of extending the base controller class by registering your controllers as services. This greatly decouples you code and allows all kinds of additional flexibility. You should always favor composition over inheritance.

zf2 controller factory serviceLocator

I'm trying to inject the service manager into a controller.
Actual Error:
\vendor\zendframework\zend-servicemanager\src\Exception\ServiceLocatorUsageException.php:34
Service "Project\Service\ProjectServiceInterface" has been requested to plugin manager of type "Zend\Mvc\Controller\ControllerManager", but couldn't be retrieved.
A previous exception of type "Zend\ServiceManager\Exception\ServiceNotFoundException" has been raised in the process.
By the way, a service with the name "Project\Service\ProjectServiceInterface" has been found in the parent service locator "Zend\ServiceManager\ServiceManager": did you forget to use $parentLocator = $serviceLocator->getServiceLocator() in your factory code?
The process goes:
class BaseController extends AbstractActionController implements ServiceLocatorAwareInterface
{
public function __construct(\Zend\ServiceManager\ServiceLocatorInterface $sl)
{
$this->serviceLocator = $sl;
}
}
Create controller and use constructor method
Extend this BaseController to AdminController
Setup Routes to AdminController => /admin
use Module.php
public function getControllerConfig()
Use closer as factory to create controller object injecting the serviceLocator
'Project\Controller\Project' => function($sm) {
$serviceLocator = $sm->getServiceLocator();
return new \Project\Controller\ProjectController($serviceLocator);
},
try to use $this->getServiceLocator()->get('service_name')
Exception found for missing service.....
Now the problem is this:
/**
*
* #param ServiceLocatorInterface $sl
*/
public function __construct(\Zend\ServiceManager\ServiceLocatorInterface $sl)
{
$rtn = $sl->has('Project\Service\ProjectServiceInterface');
echo '<br />in Constructor: '.__FILE__;var_dump($rtn);
$this->serviceLocator = $sl;
}
public function getServiceLocator()
{
$rtn = $this->serviceLocator->has('Project\Service\ProjectServiceInterface');
echo '<br />in getServiceLocator: '.__FILE__;var_dump($rtn);
return $this->serviceLocator;
}
Within the __constructor() the service IS FOUND. Within the getServiceLocator() method the service with the same name IS NOT FOUND....
in Constructor: Project\Controller\BaseController.php
bool(true)
in getServiceLocator: Project\Controller\BaseController.php
bool(false)
Am I missing something? Is the SharedServiceManager doing something here?
The entire purpose of this exercise was due to this message:
Deprecated: ServiceLocatorAwareInterface is deprecated and will be removed in version 3.0, along with the ServiceLocatorAwareInitializer. ...
If you really need the ServiceLocator, you have to inject it with a factory
Something like this
Controller:
<?php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\ServiceManager\ServiceLocatorInterface;
class BaseController extends AbstractActionController
{
protected $serviceLocator = null;
public function __construct(ServiceLocatorInterface $serviceLocator)
{
$this->setServiceLocator($serviceLocator);
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
}
Factory:
<?php
namespace Application\Controller\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Application\Controller\BaseController;
class BaseControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator);
{
$controller = new BaseController($serviceLocator->getServicelocator());
return $controller;
}
}
?>
in module.config.php
<?php
// ...
'controllers' => [
'factories' => [
'Application\Controller\BaseController' => 'Application\Controller\Factory\BaseControllerFactory',
// ...
],
// ...
In Zend Framework 2 there are multiple service locators (docs here), one general (mainly used for your own services), one for controllers, one for view helpers, one for validators, ... The specific ones are also called plugin managers.
The error message you are receiving is just telling you that you are using the wrong service locator, the ones that retrieves controllers and not the general one. It is also suggesting you how to solve your problem:
did you forget to use $parentLocator = $serviceLocator->getServiceLocator() in your factory code
What is probably happening (not 100% sure about this) is that in the constructor you are passing in an instance of the general service manager, and everything works fine with it. Then, since the controller implements the ServiceLocatorAwareInterface, the controller service locator is injected into your controller, overriding the one that you defided before.
Moreover, I think that the idea beyound the decision of removing ServiceLocatorAwareInterface in version 3 is that you don't inject the service locator inside your controller, but instead you inject directly the controller dependencies.
You should try to prevent injecting the service manager or service locator in the controller. It would be much better to inject the actual dependencies (in your case 'Project\Service\ProjectServiceInterface') directly into the __construct method of your class. Constructor injection (the dependencies are provided through a class constructor) is considered best practice in ZF2.
This pattern prevents the controller from ever being instantiated without your dependencies (it will throw an error).
If you inject a ServiceLocator or ServiceManager from which you will resolve the actual dependencies in the class, then it is not clear what the class actually needs. You can end up in a class instance with missing dependencies that should never have been created in the first place. You need to do custom checking inside the class to see if the actual dependency is available and throw an error if it is missing. You can prevent writing all this custom code by using the constructor dependency pattern.
Another issue is that it is harder to unit-test your class since you cannot set mocks for your individual dependencies so easily.
Read more on how to inject your dependencies in my answer to a similar question.
UPDATE
About the issue you encountered. Controller classes implement a ServiceLocatorAwareInterface and during construction of your controller classes the ControllerManager injects a ServiceLocator inside the class. This happens here in the injectServiceLocator method at line 208 in ControllerManager.php. Like #marcosh already mentioned in his answer, this might be a different service locator then you injected. In this injectServiceLocator method you also find the deprecation notice you mentioned in your question.
Yours is available in the __construct method because at that time (just after constructing the class) the variable is not yet overwritten. Later when you try to access it in your getServiceLocator method it is overwritten.

Syfmony - load service on boot

I posted another question trying to find a way to statically access a repository class outside of a controller in a custom "helper" class.
So far the only way I have figured out how to achieve this is using the code below. If anyone wants to chime into the other question about "best practice" or "design patterns" please do.
I opened this question to seek the best method on having a singleton service (?) loaded when symfony boots so other classes can access it statically without any dependency injection. I haven't had much luck on finding any official docs or common practices. I know singleton is anti practice, but is the method below the best way, or is there a more ideal solution?
services.yml
parameters:
entity.device: Asterisk\DbBundle\Entity\Device
services:
asterisk.repository.device:
class: Asterisk\DbBundle\Entity\Repositories\DeviceRepository
factory: ["#doctrine.orm.asterisk_entity_manager", getRepository]
arguments:
- %entity.device%
tags:
- {name: kernel.event_listener, event: kernel.request, method: onKernelRequest}
DeviceRepository
class DeviceRepository extends \Doctrine\ORM\EntityRepository
{
/** #var ExtendedEntityRepository */
protected static $instance;
public function __construct(EntityManager $entityManager, ClassMetadata $class)
{
parent::__construct($entityManager, $class);
if(static::$instance instanceof static == false)
static::$instance = $this;
}
public static function getInstance()
{
return static::$instance;
}
public function onKernelRequest($event)
{
return;
}
}
Glad to see you are not running around anymore.
Your approach is not going to work unless someone grabs the repository out of the container first so self::$instance is initialized. But you really don't want to do this anyways. Super hacky.
You want to inject the repository service into your kernel listener. Trying to make the repository act as a kernel listener is just not a good design. So just make a service for your repository and then a second one for the listener. It may seem a bit strange at first but it really does work well in practice and it's the way S2 is designed.
If for some reason you are stuck with the notion that you have to be able to access the container globally then be aware that your kernel is defined globally(take a look at app.php) and it has a getContainer method in it.
$repo = $_GLOBAL['kernel']->getContainer()->get('asterisk.repository.device');
But again, there should be no need to do this.
==============================
Update - It looks like you are trying to use the listener functionality just to setup singletons. You should try to avoid singletons but if you really think you need them then the global access to the kernel can be used:
class DeviceRepository extends \Doctrine\ORM\EntityRepository
{
/** #var ExtendedEntityRepository */
protected static $instance;
public static function getInstance()
{
if (!static::$instance) {
static::$instance = $_GLOBAL['kernel']->getContainer()->get('asterisk.repository.device');
}
return static::$instance;
}
Poor design but at least it get's rid of the listener hack and it avoids creating the repository until it's actually needed. It aslo means you can access the repository from commands (listeners are not setup when commands are called).
I do not understand what the profit will be about this method. The idea of the servicecontainer is to make just one instance of each class and give a reference (or pointer if you like) to any method who asks to use this same instance. Let me proof it:
Service definition:
// app/config.yml
services:
app.test:
class: Vendor\AppBundle\Service\Test
and a custom class:
// src/AppBundle/Service/Test.php
namespace AppBundle/Service;
class Test {
public $test = 0;
}
and a controller:
// src/AppBundle/Controller/DefaultController
namespace AppBundle/Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/", name="homepage")
*/
public function indexAction()
{
$instance1 = $this->get('app.test');
$instance2 = $this->get('app.test');
$instance1->test = 1;
echo $instance2->test; // RETURNS 1 !!!
exit;
}

Inject filter into Zend_View

I wish to set some properties in MyFilter with constructor injection but it seems impossible with Zend_View::addFilter(string $filter_class_name) since it loads a new instance upon usage. MyFilter implements Zend_Filter_Interface.
Can I somehow inject an instance of a filter to an instance of Zend_View?
Closing since it (hopefully) will be pushed into 2.0, see ticket on JIRA.
You may pass object:
$filter = new Your_Filter($params); // implements Zend_Filter_Interface
$view->addFilter($filter);
You may get view instance from viewRenderer, e.g. using staticHelper.
Edit:
The other method may be:
class MyFilterSetup extends MyFilter implements Zend_Filter_Interface
{
public function __construct($params)
{
$this->_params = $params;
parent::__construct();
}
public function filter($string)
{
// .... $this->_params;
}
}
I'm not certain, but I don't think it's possible. Looking at the sourcecode setFilter() and addFilter() only accept the Filter Classname as a string. You cannot set any options, like you can in Zend_Form for instance. What you could do though is:
class MyFilter implements Zend_Filter_Interface
{
protected static $_config;
public static setConfig(array $options)
{
self::_config = $options;
}
// ... do something with the options
}
and then you set the options where needed with MyFilter::setOptions(), so when Zend_View instantiates the Filter instance, it got what it needs to properly run the filter.
You can't in the 1.x branch, ticket is filed:
http://framework.zend.com/issues/browse/ZF-9718
Can't we create a custom view object extending Zend_View that overrides the addFilter() method to accept either a class or an instance. Then override the _filter() method to deal with both types of filters - string and instance - that we have stored.
Why not assign the filter properties to the view, and then either set the properties when the view is set, or access the view directly in your filtering function? e.g.
$view->assign('MyFilterProperty', 'fubar');
and then in your filter class:
public function setView($aView)
{
$this->_property = $aView->MyFilterPropery;
}
It's kludgy, but it should get the job done.

Categories