This is a question about Symfony 4 autowiring
using only 'array' as a constructor argument type-hint. I give a specific case, but this may be helpful to others because this situation happens in several Symfony bundles.
This Symfony command asks for a password and encodes it:
php bin/console security:encode-password
The code for this command is in vendor/symfony/security-bundle/Command/UserPasswordEncoderCommand.php
In Symfony 4.2.3, this is the constructor for UserPasswordEncoderCommand:
class UserPasswordEncoderCommand extends Command
{
protected static $defaultName = 'security:encode-password';
private $encoderFactory;
private $userClasses;
public function __construct(EncoderFactoryInterface $encoderFactory, array $userClasses = [])
{
$this->encoderFactory = $encoderFactory;
$this->userClasses = $userClasses;
parent::__construct();
}
Symfony uses dependency injection to call the constructor, passing in automatically determined arguments. The above constructor first argument $encoderFactory is autowired using the type-hint EncoderFactoryInterface.
My question is: How is the second argument $userClasses autowired?
How does Symfony know what the array should contain? Debug print statements show the array contains a single value "App\Entity\User".
Here is my config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: argon2i
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
guard:
authenticators:
- App\Security\LoginFormAuthenticator
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
logout:
path: app_logout
# Where to redirect after logout
target: app_login
I think the following three files are important to answering this question.
vendor/symfony/security-bundle/Resources/config/console.xml contains:
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="false" />
<service id="security.command.user_password_encoder" class="Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand">
<argument type="service" id="security.encoder_factory"/>
<argument type="collection" /> <!-- encoders' user classes -->
<tag name="console.command" command="security:encode-password" />
</service>
</services>
</container>
Part of vendor/symfony/security-bundle/Resources/config/security.xml contains:
<service id="security.encoder_factory" alias="security.encoder_factory.generic" />
<service id="Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface" alias="security.encoder_factory" />
<service id="security.user_password_encoder.generic" class="Symfony\Component\Security\Core\Encoder\UserPasswordEncoder">
<argument type="service" id="security.encoder_factory"></argument>
</service>
<service id="security.password_encoder" alias="security.user_password_encoder.generic" public="true" />
<service id="Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface" alias="security.password_encoder" />
Part of vendor/symfony/security-bundle/DependencyInjection/SecurityExtension.php contains:
function load(array $configs, ContainerBuilder $container)
...
if (class_exists(Application::class)) {
$loader->load('console.xml');
$container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders']));
}
I ask this question because I would like to use UserPasswordEncoderCommand.php as a starting point for a command to create an admin user in my database. This will give the first admin user permission to login using the web browser to
create other users.
I tried copying vendor/symfony/security-bundle/Command/UserPasswordEncoderCommand.php to src/Command/AddUserCommand.php and changing these lines:
< namespace Symfony\Bundle\SecurityBundle\Command;
---
> namespace App\Command;
< class UserPasswordEncoderCommand extends Command
---
> class AddUserCommand extends Command
< protected static $defaultName = 'security:encode-password';
---
> protected static $defaultName = 'app:add-user';
When I ran this command: php bin/console app:add-user
this error appeared:
There are no configured encoders for the "security" extension.
This happened because the constructor __construct(EncoderFactoryInterface $encoderFactory, array $userClasses = [])
was called with only one argument, so the second argument defaulted to an empty array.
It seems these files also need to be copied from vendor/symfony/security-bundle to somewhere under src/
DependencyInjection/SecurityExtension.php
Resources/config/console.xml
and be somehow modified. Which other files need to be copied to src/ ? The Symfony documentation under https://symfony.com is excellent, but I can not find where this situation is described.
Note that this command works:
php bin/console app:add-user mypassword "App\Entity\User"
but I do not want the user to have to type this. And this may change if security.yaml changes.
In this particular case the data is being assembled by the SecurityBundle as part of the configuration process. When the kernel is booted all bundles are collected and their respective Extensions, usually inside the DependencyInjection subfolder, will be registered. When the container is built it is passed through all these extension so they can modify the services or add their own. This is how Symfony collects the configs from inside the bundle, when the main application itself does not know where they are stored, but the bundle does.
Getting the user classes is done by Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension when processing the encoders-section of the security.yml. If you look at the encoders section of your security.yaml you notice that each Encoder is assigned to a class name. The method will then use this array to create the appropriate encoder for the value and keep the class name as key. This array is passed to the encoder factory in createEncoders. Afterwards it will take the same array from the configuration, get the names of each user class using array_keys() and replace the argument with offset 1, i.e. the $userClasses, for the command.
This concept is called semantic configuration and inside your own application this would probably be overkill as you have to define the Configuration and then write the logic by hand. Instead you would probably just define an array with the class names in the parameters-section of your services.yaml.
If you want to pass in other services, which can come from other bundles your bundle does not know about, there is another approach called CompilerPasses. In a CompilerPass you can collect all classes, e.g. with a specific tag, and then pass their references to a method or the constructor of your service. This is for example used inside Symfony to register EventListeners or make Commands available in the Console application.
Related
I try to change the access control decision strategy in a Symfony project but it doesn't seem to work.
I have the following in my security.yaml:
security:
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false
access_control:
- { path: ^/dev/test, roles: [COMPLEX_TEST, ROLE_OTHER] }
I need both COMPLEX_TEST and ROLE_OTHER to be granted for the route to be accessible (COMPLEX_TEST is tested by a custom role voter).
But when I try to access the route, only the COMPLEXT_TEST voter is called, and that's because it allows the access and the strategy is still affirmative.
I can say this because when debugging the code, I can see that the value in Symfony\Component\Security\Core\Authorization\AccessDecisionManager is always affirmative, no matter what I set in the security.yaml.
For now I've created a compiler pass that forces the strategy to unanimous, but it's kind of a hack:
class FixAccessDecisionManagerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->hasDefinition('security.access.decision_manager')) {
$definition = $container->getDefinition('security.access.decision_manager');
$definition->setArgument('$strategy', AccessDecisionManager::STRATEGY_UNANIMOUS);
}
}
}
With the compiler pass I can see that the value in Symfony\Component\Security\Core\Authorization\AccessDecisionManager is correcly set to unanimous and the access control works as expected.
Do you see what I am missing? Or is it a bug in Symfony? My Symfony version is 4.4.7.
Thank you for your time.
I'm trying to refactor some Symfony 3 code to Symfony 4.
I am getting the following error when attempting to log:
The "monolog.logger.db" service or alias has been removed or inlined
when the container was compiled. You should either make it public, or
stop using the conta iner directly and use dependency injection
instead.
My logging code:
$logger = $container->get('monolog.logger.db');
$logger->info('Import command triggered');
Monolog config:
monolog:
channels: ['db']
handlers:
db:
channels: ['db']
type: service
id: app.monolog.db_handler
app.monolog.db_handler config (Note, I tried public: true here and it had no affect:
app.monolog.db_handler:
class: App\Util\MonologDBHandler
arguments: ['#doctrine.orm.entity_manager']
How can I get this wired up correctly in Symfony 4?
By default all services in Symfony 4 are private (and is the recommended pratice) so you need to "inject" in each Controller each needed service (personally I use a custom CommonControllerServiceClass).
You can also create a public service "alias" to continue accessing the service as you did, but it's not the best pratice to follow (also because I guess you will have many other services to fix).
mylogger.db:
alias: monolog.logger.db
public: true
then you can get the service from the container:
$logger = $container->get('mylogger.db');
Alister's answer is a good start, but you can utilise service arguments binding instead of creating a new service for each logger:
services:
_defaults:
autowire: true
bind:
$databaseLogger: '#monolog.logger.db'
Then just change the argument name in your class:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $databaseLogger = null) {...}
It appears that App\Util\MonologDBHandler may be the only thing that is actively using monolog.logger.db - via a container->get('...') call. (If not, you will want to use this technique to tag the specific sort of logger into more services).
You would be better to allow the framework to build the app.monolog.db_handler service itself, and use the container to help to build it. Normally, to inject a logger service, you will just need to type-hint it:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $logger = null) {...}
However, that will, by default, setup with the default #logger, so you need to add an extra hint in the service definition of the handler that you want a different type of logger:
services:
App\Log\CustomLogger:
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: db }
Now, the logger in CustomLogger should be what you had previously known as monolog.logger.db.
You can also alias a different interface (similar to how the LoggerInterface is aliased to inject '#logger') to the allow for the tagging.
I have a service which extends UserManager, so when I do:
$message = \Swift_Message::newInstance()
->setSubject('~')
->setFrom('~')
->setTo('~')
->setBody('~', 'text/html');
$this->get('mailer')->send($message);
I get the error:
Fatal error: Call to undefined method My\MyBundle\Service\ServiceClass::get()
I know this is because I need to inject the swiftmailer into here, but how?
(usually the service class extends 'Generic' so the swift mailer is included.)
Depending on what kind of service file you are using you need to inject it into your service directly like you said.
XML:
<services>
<service id="sample.service" class="%sample.service.class%">
<argument type="service" id="mailer" />
</service>
</services>
YAML:
services:
sample.service:
class: %sample.service.class%
arguments: [#mailer]
You can simply grab the service in your constructor like this.
Or if you really want, you can inject the service_container. But that's really dirty, since you can just inject the services you need.
Injection the service_container is only needed if you need a dynamic service call.
In services.yml (symfony 4 example)
mailer:
class: \Swift_Mailer
myClass:
class: x\x
arguments:
- "#mailer"
I would like to add some code inside the function authenticate() of the Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager.
I tried to create a child bundle of the security bundle.
And i redefined the service for security.authentication.manager in this bundle like that
<!-- Authentication related services -->
<service id="security.authentication.manager" class="%security.authentication.manager.class%" public="false">
<argument type="collection" />
</service>
But when i relad the page, the framework throw an exception: InvalidArgumentException: You must at least add one authentication provider.
I suppose it's becaue the dependecies are created inside the parent bundle configuation.
What i must do to get it work without redefine the whole security bundle ?
Thank you.
I suppose that better would be created your own handlers.
You need to create service
Register it into service container.
Simply set handlers in your security.yml:
form_login:
success_handler: success_login_handler
failure_handler: failure_login_handler
logout:
success_handler: success_logout_handler
Ok, I found that the best way is to create my own a factory, that extends the formFactory of symfony2
Then i must create my own AuthentificationProvider that extend the DaoAuthenticationProvider and declare it as abstract service.
Then create the service inside the factory via the method createAuthProvider() and replace the parameters we need.
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'acme.authentication.provider.dao.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('acme.authentication.provider.dao'))
->replaceArgument(0, new Reference($userProviderId))//replace args on the service constructor
->replaceArgument(2, $id)
->addArgument(new Reference('acme.api'))//add some args to the service constructor
;
return $provider;
}
What is the best way to have an event that fires after a record is inserted in Symfony2 / Doctrine?
First, register a service as a Doctrine event listener:
app/config.yml:
services:
foo.listener:
class: Vendor\FooBundle\BarClass
tags:
- { name: doctrine.event_listener, event: postPersist, method: onPostPersist }
Then in your listener class, define an onPostPersist method (or whatever you named the method in the config) that takes a Doctrine\ORM\Event\LifecycleEventArgs argument:
public function onPostPersist(LifecycleEventArgs $eventArgs)
{
// do stuff with the entity here
}
Note that you can't pass an instance of EntityManager to the listener class, because $eventArgs contains a reference to it, and doing so will throw a CircularReferenceException.
Doctrine Project documentation here. Symfony Project documentation here (out of date, but included for reference)/
Try injecting the container itself instead of the security context. with FOS_USER, security.context depends on your listener (EM) and your listener requires security.context.
<service id="foo.listener" class="%foo.listener.class%">
<argument type="service" id="service_container"/>
<tag name="doctrine.event_listener" event="postPersist" method="fooMethod" />
</service>
By the way, at least in XML, the method name does not seem to be working, by default it call the method 'postPersist' instead and ignore whatever method name you give (fooMethod); Please let me know if that's the case with YAML config, too, or I am just wrong.