I'm trying to inject ServiceA(used to fetch entities based on is_granted) into ServiceB (a voter) and getting a circular reference.
I believe because when ServiceA is loaded its authentication_checker dependency tries to load all the voters, which includes ServiceB, which requires ServiceA... etc.
Anyway to work around this?
YAML service mapping:
services:
app.security_service:
class: AppBundle\Service\appSecurityService
arguments: [ "#logger", "#doctrine", "#security.authorization_checker", "#security.token_storage" ]
app.entity_voter:
class: AppBundle\Security\ChargebackNotificationVoter
public: false
tags:
- { name: security.voter }
arguments: [ "#logger", "#doctrine", "#app.security_service" ]
An example of what I'm doing in ServiceA
public function getEntitiesForUser(UserInterface $user)
{
$user = $this->tokenStorage->getToken()->getUser();
if($this->authorizationChecker->isGranted('ROLE_SYSTEM_ADMIN')){
//If the user has ROLE_SYSTEM_ADMIN get all the entities
$entitiess = $this->managerRegistry->getRepository('AppBundle:Entities')->findAll();
}elseif($this->authorizationChecker->isGranted('ROLE_ORGANIZATION_ADMIN')){
//ElseIf the user has ROLE_ORGANIZATION_ADMIN get all the entitiess that belong to the organization
$entitiess = $user->getOrganization()->getEntities();
} elseif($this->authorizationChecker->isGranted('ROLE_USER')) {
$entitiess = $this->managerRegistry->getRepository('AppBundle:Entities')->findByUser($user);
} else {
//if ROLE_USER is missing return null
$entitiess = null;
}
return $entities;
}
..and the error I get
Circular reference detected for service "security.authorization_checker", path: "twig.controller.exception -> twig -> security.authorization_checker -> security.access.decision_manager -> ccp.chargebacknotification_voter -> ccp.security_service".
You can try to inject security.authorization_checker (resp. app.security_service) into app.security_service (resp. app.entity_voter) using setter method:
services:
app.security_service:
class: AppBundle\Service\appSecurityService
arguments: [ "#logger", "#doctrine", "#security.token_storage" ]
calls:
- [setAuthorizationChecker, ['#security.authorization_checker']]
app.entity_voter:
class: AppBundle\Security\ChargebackNotificationVoter
public: false
tags:
- { name: security.voter }
arguments: [ "#logger", "#doctrine" ]
calls:
- [setSecurityService, ['#app.security_service']]
I use Symfony3
Related
I'm trying to inject multiple monolog handler into a service. Right now my parent class injects a logger and the children class injects another logger. My goal is it to be able to log specific actions to specific log files.
My service.yaml:
App\Services\PrinterManager:
arguments: ['#doctrine.orm.entity_manager','#logger', '', '', '', '','']
tags:
- { name: monolog.logger, channel: printerface}
App\Services\Printer\Printer:
autowire: true
autoconfigure: false
public: false
parent: App\Services\PrinterManager
arguments:
index_2: '#logger'
index_3: '#oneup_flysystem.printer_invoice_filesystem'
index_4: '#oneup_flysystem.printerface_content_filesystem'
index_5: '#oneup_flysystem.sftp_filesystem'
index_6: '#App\Services\PrinterApiService'
tags:
- { name: monolog.logger, channel: printerlog}
My monolog.yaml:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event, !printerface", "!printerlog"]
printerface:
type: stream
level: debug
channels: ["printerface"]
path: "%kernel.logs_dir%/printerface.log"
printerlog:
type: stream
level: debug
channels: ["printerlog"]
path: "%kernel.logs_dir%/printerlog.log"
But it seems that the current service configuration breaks the constructor and I get the following error:
The argument must be an existing index or the name of a constructor's parameter.
Is there any way to use two log files in a service?
I've not done it with a parent/child class, but with something a little simpler I'm using named parameters, this is what I have (with three different loggers):
# App/Subscribers/WebhookLoggingListener.php file
public function __construct(
LoggerInterface $logger,
LoggerInterface $mailgunLog,
LoggerInterface $dripLog) {
}
# services.yml
App\Subscribers\WebhookLoggingListener:
arguments:
$logger: "#logger"
$mailgunLog: "#monolog.logger.mailgun"
$dripLog: "#monolog.logger.drip"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
If I was using the other loggers elsewhere I could also bind them to specific variable names:
services:
_defaults:
# ... other config
bind:
$dripLog: "#?monolog.logger.drip"
This is the method that Symfony is using to replace parent's arguments in a child service:
/**
* You should always use this method when overwriting existing arguments
* of the parent definition.
*
* If you directly call setArguments() keep in mind that you must follow
* certain conventions when you want to overwrite the arguments of the
* parent definition, otherwise your arguments will only be appended.
*
* #param int|string $index
* #param mixed $value
*
* #return self the current instance
*
* #throws InvalidArgumentException when $index isn't an integer
*/
public function replaceArgument($index, $value)
{
if (\is_int($index)) {
$this->arguments['index_'.$index] = $value;
} elseif (0 === strpos($index, '$')) {
$this->arguments[$index] = $value;
} else {
throw new InvalidArgumentException('The argument must be an existing index or the name of a constructor\'s parameter.');
}
return $this;
}
As you can see, the indexes must be eighter same the argument variable names in the parent's constructor with a prefixed $ or an integer indicating the related argument.
So I think you must define your child service as below:
App\Services\Printer\Printer:
autowire: true
autoconfigure: false
public: false
parent: App\Services\PrinterManager
arguments:
2: '#logger'
3: '#oneup_flysystem.printer_invoice_filesystem'
4: '#oneup_flysystem.printerface_content_filesystem'
5: '#oneup_flysystem.sftp_filesystem'
6: '#App\Services\PrinterApiService'
tags:
- { name: monolog.logger, channel: printerlog}
Update:
After I reproduced your problem, I figured out that the solution is as below. With this solution, the Symfony autowiring will work for the child service.
App\Services\Printer\Printer:
autowire: true
autoconfigure: false
public: false
parent: App\Services\PrinterManager
arguments:
$arg2: '#logger'
$arg3: '#oneup_flysystem.printer_invoice_filesystem'
$arg4: '#oneup_flysystem.printerface_content_filesystem'
$arg5: '#oneup_flysystem.sftp_filesystem'
$arg6: '#App\Services\PrinterApiService'
tags:
- { name: monolog.logger, channel: printerlog}
$arg2, $arg3, $arg4, $arg5 and $arg6 must be replaced by your class constructor's argument names.
Here is the scheme of the required URLs:
/service/getBalance should map to CustomerController::getBalance
/service/addBalance should map to CustomerController::addBalance
/customer/getBalance should map to CustomerController::getBalance
/customer/addBalance should map to CustomerController::addBalance
Here is a simple controller
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class CustomerController extends Controller {
/**
* #Route("/getBalance")
*/
public function getBalanceAction(Request $request) {
}
/**
* #Route("/addBalance")
*/
public function addBalanceAction(Request $request) {
}
} // class CustomerController
Have tried the following ways. None of them worked.
# rounting.yml
v1:
resource: "#AppBundle/Controller/CustomerController.php"
prefix: /service
type: annotation
v2:
resource: "#AppBundle/Controller/CustomerController.php"
prefix: /customer
type: annotation
loading the same resource with different prefix always overrides the previous occurrence (the last one works). The following also doesn't work for the same reason and have the same behavior.
# rounting.yml
v1:
resource: "#AppBundle/Resources/config/routing_customer.yml"
prefix: /service
v2:
resource: "#AppBundle/Resources/config/routing_customer.yml"
prefix: /customer
# routing_customer.yml
getBalance:
path: /getBalance
defaults : { _controller: "AppBundle:Customer:getBalance" }
addBalance:
path: /addBalance
defaults : { _controller: "AppBundle:Customer:addBalance" }
Third not working option:
# rounting.yml
v1:
resource: "#AppBundle/Resources/config/routing_v1.yml"
prefix: / # evenr putting /service here instead of inside
v2:
resource: "#AppBundle/Resources/config/routing_v2.yml"
prefix: / # evenr putting /customer here instead of inside
# routing_v1.yml
getBalance:
path: /service/getBalance
defaults : { _controller: "AppBundle:Customer:getBalance" }
addBalance:
path: /service/addBalance
defaults : { _controller: "AppBundle:Customer:addBalance" }
# routing_v2.yml
getBalance:
path: /customer/getBalance
defaults : { _controller: "AppBundle:Customer:getBalance" }
addBalance:
path: /customer/addBalance
defaults : { _controller: "AppBundle:Customer:addBalance" }
I actually need to route / and /customer to the same controller:
/getBalance and /customer/getBalance.
I want to give two prefixes for a group of methods. How to do that in Symfony?
Conclusion from my trials, #goto's answer and #Cerad's comments:
The last Example above could have worked if I used different route names. Route names are unique though out the whole project (Not just unique in file). v1_getBalance and v2_getBalance.
The other solution is to use a custom loader as #goto described.
You could do routes this way:
#Route(
"/{subject}/getBalance",
requirements={
"subject": "customer|service"
}
)
in yml:
subject_getbalance:
path: /{subject}/getBalance
requirements:
subject: customer|service
The requirement is safer than nothing: it allows you to route another route like foo/getBalance on another controller (as it does not match the requirement)
EDIT: for your special case, as you need /getBalance to map to your route too you could do:
subject_getbalance:
path: /{subject}/getBalance
default: { _controller: YourBundle:Controller }
requirements:
subject: customer|service
default_getbalance:
path: /getBalance
default: { _controller: YourBundle:Controller, subject: customer }
Edit: the last idea is to a custom route loader (but I've never tried):
class ExtraLoader extends Loader
{
public function load($resource, $type = null)
{
/* ... */
$prefixes = [ 'default_' =>'','customer_' =>'/customer','service_' =>'/service']
// prepare a new route
$path = '/getbalance/{parameter}';
$defaults = array(
'_controller' => 'YourBundle:Actiona',
);
$requirements = array(
'parameter' => '\d+',
);
foreach($prefixes as $prefixName => $prefixRoute) {
$route = new Route($prefixRoute . $path, $defaults, $requirements);
$routes->add($prefixName . 'getBalance', $route);
}
This will allows you to have 3 routes generated:
default_getBalance: /getBalance
customer_getBalance: /customer/getBalance
service_getBalance: /service/getBalance
I want to disable Gedmo\SoftDeleatable behaviour for some of my phpunit tests in Symfony2.
I wrote these lines to remove SoftDeleatableListener:
foreach ($em->getEventManager()->getListeners() as $eventName => $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof \Gedmo\SoftDeleteable\SoftDeleteableListener) {
$em->getEventManager()->removeEventListener($eventName, $listener);
}
}
}
But none of the listeners was identified as being an instance of SoftDeleteableListener.
So I added these lines in app/config.yml:
doctrine:
..
orm:
..
filters:
softdeleteable:
class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter
enabled: false
and these lines in Acme/MyBundle/Resources/config/services.yml:
services:
..
gedmo.listener.softdeleteable:
class: Gedmo\SoftDeleteable\SoftDeleteableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ #annotation_reader ] ]
But when I run php app/console doctrine:schema:update --dump-sql,
I get the following error message:
No mapping found for field 'deletedAt' on class 'Acme\MyBundle\Entity\Account'.
Any idea ?
Looks like you still need to add the property to the class.
/**
* #var \DateTime
* #ORM\Column(type="datetime")
*/
protected $deletedAt;
I have used the Trait. Only this Trait does not define the deletedAt. Where the trait TimestampableEntity defines the columns.
use SoftDeleteable;
I hope this helps
I've decided to install "gedmo/doctrine-extensions" on Symfony to use Translatable.
It works fine, except that listener is ignoring default locale I've specified, always falling back to en_US.
Translatable is plugged in as service:
#config.yml
services:
gedmo.listener.translatable:
class: Gedmo\Translatable\TranslatableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ #annotation_reader ] ]
- [ setDefaultLocale, [ ru ] ]
- [ setTranslationFallback, [ true ] ]
And when I try to find() object in database it always fetches en_US instead of ru:
$test = $em->find('Vendor\Entity\Test', 1);
//outputs row with 'locale' = "en_US" from `ext_translations_test` table
Only if I specify language directly, like:
$test->setTranslatableLocale('ru');
$em->refresh($test);
It gives desired translation.
Is there any way to set default locale that will work?
UPDATE
Adding another call function in config.yml fixed the problem, altough now I'm not quite sure what setDefaultLocale() function actually does, as Gedmo\Translatable\TranslatableListener::$defaultLocale property provided with a most horrid commentary I've ever seen. Will try to find more...
services:
gedmo.listener.translatable:
class: Gedmo\Translatable\TranslatableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ #annotation_reader ] ]
- [ setDefaultLocale, [ ru ] ]
- [ setTranslatableLocale, [ ru ] ]
- [ setTranslationFallback, [ true ] ]
According to: https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/symfony2.md
Note: if you noticed, there's Acme\DemoBundle\Listener\DoctrineExtensionListener you will need to create this listener class if you use loggable or translatable behaviors. This listener will set the locale used from request and username to loggable. So, to finish the setup create Acme\DemoBundle\Listener\DoctrineExtensionListener
Make sure you have setup the kernel listener as well.
namespace Acme\DemoBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DoctrineExtensionListener implements ContainerAwareInterface
{
/**
* #var ContainerInterface
*/
protected $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function onLateKernelRequest(GetResponseEvent $event)
{
$translatable = $this->container->get('gedmo.listener.translatable');
$translatable->setTranslatableLocale($event->getRequest()->getLocale());
}
public function onKernelRequest(GetResponseEvent $event)
{
$securityContext = $this->container->get('security.context', ContainerInterface::NULL_ON_INVALID_REFERENCE);
if (null !== $securityContext && null !== $securityContext->getToken() && $securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
$loggable = $this->container->get('gedmo.listener.loggable');
$loggable->setUsername($securityContext->getToken()->getUsername());
}
}
}
And add the following to your config file:
services:
extension.listener:
class: Acme\DemoBundle\Listener\DoctrineExtensionListener
calls:
- [ setContainer, [ #service_container ] ]
tags:
# translatable sets locale after router processing
- { name: kernel.event_listener, event: kernel.request, method: onLateKernelRequest, priority: -10 }
# loggable hooks user username if one is in security context
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
I defined the following service:
my_project.widget_listing_content_resolver:
class: MyProject\Widget\ListingBundle\Resolver\WidgetListingContentResolver
arguments:
- "#router"
tags:
- { name: my_project.widget_content_resolver, alias: Listing }
And I want to declare an alias of this service, with a different tag:
my_project.widget_domain_operations_content_resolver:
alias: my_project.widget_listing_content_resolver
tags:
- { name: my_project.widget_content_resolver, alias: DomainOperations }
But in my ContentResolverChain, the service aliased "DomainOperations" is not present. Is there a way to solve this ?
EDIT:
I tried the following configuration:
my_project.widget_listing_content_resolver:
class: MyProject\Widget\ListingBundle\Resolver\WidgetListingContentResolver
arguments:
- "#router"
tags:
- { name: my_project.widget_content_resolver, alias: Listing }
- { name: my_project.widget_content_resolver, alias: DomainOperations }
It results that the "my_project.widget_listing_content_resolver" service is only tagged as "Listing". My problem now is: "How to tag a service with multiple tag aliases"
I found the solution to that problem. The service alias was tagged as espected, but the additionnal tag was not read by my CompilerPass:
This was the errored CompilerPass:
$taggedServices = $container->findTaggedServiceIds(
'my_project.widget_content_resolver'
);
foreach ($taggedServices as $id => $attributes) {
$definition->addMethodCall(
'addResolver',
array($attributes[0]['alias'], new Reference($id))
);
}
As you see, it took only the first alias found ($attributes[0])
I had to change it to:
$taggedServices = $container->findTaggedServiceIds(
'my_project.widget_content_resolver'
);
foreach ($taggedServices as $id => $attributes) {
foreach ($attributes as $attribute) {
$definition->addMethodCall(
'addResolver',
array($attribute['alias'], new Reference($id))
);
}
}