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.
Related
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
I'm using the param converter to get object from url and body converter to get object form body.
This perfectly works for GET and POST methods, but I have to do it with a tricky way for an update (PUT):
/**
* #param PowerDNSDomain $domain
* #param PowerDNSRecord $record
* #param PowerDNSRecord $updatedRecord
* #param ConstraintViolationListInterface $validationErrors
*
* #ParamConverter("updatedRecordData", converter="fos_rest.request_body")
*
* #return View
*/
public function putAction(PowerDNSDomain $domain, PowerDNSRecord $record, PowerDNSRecord $updatedRecord, ConstraintViolationListInterface $validationErrors)
{
if ($validationErrors->count() > 0) {
return $this->handleBodyValidationErrorsView($validationErrors);
}
$record->setName($updatedRecord->getName().'.'.$domain->getName())
->setContent($updatedRecord->getContent())
->setTtl($updatedRecord->getTtl())
->setPrio($updatedRecord->getPrio());
$this->get('manager.dns')->saveRecord($record);
return $this->view($record);
}
In a nutshell, I have to retrieve two PowerDNSDomain object (one from the URL, one from the body) and update each fields manually.
This is overkill, isn't it?
The preferred way would be to have a method signature like this:
public function putAction(PowerDNSDomain $domain, ConstraintViolationListInterface $validationErrors)
Where the PowerDNSDomain $domain would be the result of the body converter object merged into the param converter one.
In this case, I will just have to get validation errors and save the object.
How can I make this possible ? :-)
Related configuration:
fos_rest:
routing_loader:
default_format: json
body_converter:
enabled: true
validate: true
serializer:
serialize_null: true
view:
formats:
xml: false
json: true
rss: false
yml: true
view_response_listener: force
param_fetcher_listener: force
format_listener:
rules:
- { path: '^/api/', priorities: ['json', 'yml'], fallback_format: json, prefer_extension: true }
- { path: '^/', stop: true } # FOSRest should not handle other routes than API
media_type:
enabled: true
Possibly related issue: https://github.com/FriendsOfSymfony/FOSRestBundle/issues/1238
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'm not really familiar with YAML so I open parameters.yml and config.yml files to see example how to use parameters or variable in YAML.
parameters.yml:
parameters:
database_driver: pdo_mysql
database_host: 127.0.0.1
database_port: 3306
database_name: homlist
config.yml:
doctrine:
dbal:
driver: "%database_driver%"
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
But when I tried it with doctrine mapping yaml file like this:
parameters:
table_name: test
Mockizart\Bundle\BlogBundle\Entity\MockblogTag:
type: entity
table: "%table_name%"
it's error like this:
An exception occurred while executing 'SELECT count(DISTINCT %0_.id) AS sclr0 FROM %table_name% %0_':
this is my mapping file Resources\Config\Entity\MockblogTag
Mockizart\Bundle\BlogBundle\Entity\MockblogTag:
type: entity
table: mockblog_tag
indexes:
user_id:
columns:
- user_id
name:
columns:
- name
slug:
columns:
- slug
id:
id:
type: integer
nullable: false
unsigned: false
comment: ''
id: true
generator:
strategy: IDENTITY
fields:
dateCreated:
type: integer
nullable: false
unsigned: false
comment: ''
column: date_created
name:
type: string
nullable: false
length: 60
fixed: false
comment: ''
slug:
type: string
nullable: false
length: 100
fixed: false
comment: ''
totalPost:
type: integer
nullable: false
unsigned: false
comment: ''
column: total_post
manyToOne:
user:
targetEntity: ORD\UserBundle\Entity\User
joinColumn:
referencedColumnName: id
type: integer
nullable: false
unsigned: false
lifecycleCallbacks:
How to define variable in yaml symfony2 ?
The way of defining parameters it's correct, however I see from comments that your purpose is to configure the class used for User object:
As Cerad said you can't do that. But if you want to configure the class you use for the User, you can have a manager service class.
<?php
namespace YourNamespace\UserBundle\Manager;
use Doctrine\Common\Persistence\ObjectManager;
class UserManager
{
/**
* #var ObjectManager
*/
protected $em;
/**
* Your user class
*
* #var string
*/
protected $className;
public function __construct(ObjectManager $em, $class)
{
$this->em = $em;
$this->className = $class;
}
public function createInstance()
{
return new $this->className;
}
public function getRepository()
{
return $this->em->getRepository($this->className);
}
}
And the services definitions will be like this:
services:
your_user.manager:
class: YourNamespace\UserBundle\Manager\UserManager
arguments: ['#doctrine.orm.entity_manager', 'YourNamespace\UserBundle\Entity\User']
In your controller you can use this manager class like this:
$userManager = $this->get('your_user.manager');
$user = $userManager->createInstance();
I think this is a good way to have a central point when dealing with user object. And if someday for whatever reason you decide to use a different class for user you just modify the argument 'YourNamespace\UserBundle\Entity\User'.
Also in this way you can use 'YourNamespace\UserBundle\Entity\User' argument as parameter, so the definition will change to:
services:
your_user.manager:
class: Moneytablet\UserBundle\Manager\UserManager
arguments: ['#doctrine.orm.entity_manager', '%user_class%']
and in you parameters.yml you can have:
parameters:
user_class: YouNamespace\UserBundle\Entity\User
I really like working this way, you can create save(), remove() methods on manager class and so on. Also later on when creating new services you can inject this manager like a regular service if it's a dependency.
And if you want a new manager for a different entity, you can create a new service definition with different construct arguments, but with the same service class.
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 }