I try to override Exception Controller with Symfony 3
I followed their example here http://symfony.com/doc/current/controller/error_pages.html but it doesn't work
So first I have created my error page in app/Ressources/TwigBundle.../error404.html.twig
And it works without the controller overriding.
Second, I have added in services.yml, under services:
frontBundle\Controller\CustomExceptionController:
public: true
arguments:
$debug: '%kernel.debug%'
Third in my Controller folder, I have created a CustomExceptionController.php
Inside, I have put (I want to override findtemple() for example)
namespace frontBundle\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
class CustomExceptionController extends ExceptionController
{
/**
* #param Request $request
* #param string $format
* #param int $code An HTTP response status code
* #param bool $showException
*
* #return TemplateReferenceInterface
*/
protected function findTemplate(Request $request, $format, $code,
$showException)
{
}
}
However it doesn't work. This new controller is not taken into consideration and the findTemplate() is not overrided.
Did I miss something?
There are really not a lot of help about that with Symfony 3...
Thank you so much
If you need override the controller try this:
# app/config/services.yml
services:
_defaults:
# ... be sure autowiring is enabled
autowire: true
# ...
AppBundle\Controller\CustomExceptionController:
public: true
arguments:
$debug: '%kernel.debug%'
and:
# app/config/config.yml
twig:
exception_controller: AppBundle:Exception:showException
Link
Regards
Related
I just made the migration from symfony 4.1 to 4.4
I have this error:
Argument 1 passed to App\EventListener\KernelRequestListener::__construct() must be an instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage, instance of Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage given, called in C:\xampp\htdocs\chat-project-symfony\var\cache\dev\Container06Mjwya\srcApp_KernelDevDebugContainer.php on line 1130
While if you look at my KernelRequestListener :
<?php
namespace App\EventListener;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
//..
class KernelRequestListener
{
private $tokenStorage;
/**
* KernelRequestListener constructor.
* #param TokenStorage $tokenStorage
* ...
*/
public function __construct(TokenStorage $tokenStorage/*...*/)
{
$this->tokenStorage = $tokenStorage;
//..
}
}
Here is my config/services.yaml file:
#...
services:
#..
App\EventListener\KernelRequestListener:
arguments: [ '#security.token_storage' ]
tags:
- { name: kernel.event_listener, event: kernel.request }
- { name: kernel.event_listener, event: kernel.response }
I don't know why symfony tell me that I'm using Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage while it's clearing written Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage
I already tried to clear the cache folder and also delete the cache folder and it didn't change.
How can I fix this ?
Thank you
I don't know why symfony tell me that I'm using Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage while it's clearing written Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage
It's not symfony but PHP's type checking feature. You are stating that your Listener wants a TokenStorage but symfony is passing to it different class, thus the error.
So, as #JaredFarrish pointed, you should be using TokenStorageInterface in your constructor, like this:
namespace App\EventListener;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
//..
class KernelRequestListener
{
private $tokenStorage;
/**
* KernelRequestListener constructor.
* #param TokenStorageInterface $tokenStorage
* ...
*/
public function __construct(TokenStorageInterface $tokenStorage/*...*/)
{
$this->tokenStorage = $tokenStorage;
//..
}
}
It's a common practice to use interfaces where they exists, because this way you will loose coupling with other classes and provide a way to unit test your classes.
Take a look: https://github.com/symfony/security-bundle/blob/master/Resources/config/security.xml#L22 they switched class for #security.token_storage service, because of deprecation. But when you use an interface you don't care of anything underlying, you just know that you will have your methods because of interface contract.
I fixed it changing this line:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
With this one:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface as TokenStorage;
Considering this following service class:
namespace AppBundle\Listener\Entity;
use AppBundle\Entity\Payment;
use AppBundle\Event\PaymentEvent;
use AppBundle\Event\PaymentEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class PaymentEntityListener
{
/**
* #var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* #required
*
* #param EventDispatcherInterface $eventDispatcher
*/
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher)
{
dump($eventDispatcher);
$this->eventDispatcher = $eventDispatcher;
}
/**
* #param Payment $payment
*/
public function postPersist(Payment $payment)
{
$this->eventDispatcher->dispatch(PaymentEvents::ADD, new PaymentEvent($payment));
}
}
The method PaymentEntityListener::setEventDispatcher should be call with the EventDispatcherInterface matched service.
This method is correctly configured according to the debug command:
$ ./bin/console debug:container --show-private AppBundle\\Listener\\Entity\\PaymentEntityListener
Information for Service "AppBundle\Listener\Entity\PaymentEntityListener"
=========================================================================
---------------- -------------------------------------------------
Option Value
---------------- -------------------------------------------------
Service ID AppBundle\Listener\Entity\PaymentEntityListener
Class AppBundle\Listener\Entity\PaymentEntityListener
Tags -
Calls setEventDispatcher
Public no
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
---------------- -------------------------------------------------
But this method is never called.
Here is my services.yml:
imports:
- { resource: legacy_aliases.yml }
services:
_defaults:
autowire: true
autoconfigure: true
public: false
_instanceof:
Doctrine\ORM\Decorator\EntityManagerDecorator:
public: true
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository}'
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
tags: ['controller.service_arguments']
PowerDNSBundle\:
resource: '../../src/PowerDNSBundle/*'
exclude: '../../src/PowerDNSBundle/{Entity,Repository}'
PowerDNSBundle\Doctrine\ORM\PowerDNSEntityManager:
arguments:
$wrapped: '#doctrine.orm.powerdns_entity_manager'
I don't know why the method is not called and why Symfony does not throw any exception.
FYI, the following command:
./bin/console debug:container --show-private Symfony\\Component\\EventDispatcher\\EventDispatcherInterface
Does return a service match.
Where can be the issue?
Thanks for your help.
I finally found why: This listener didn't have the correct tag:
AppBundle\Listener\Entity\:
resource: '../../src/AppBundle/Listener/Entity'
tags: ['doctrine.orm.entity_listener']
Because of that, I guess the service is not recognized by Doctrine and this one try to create the listener itself. But in this case, the method will never be called.
Following this documentation, I can create many channels which will create services with the following name monolog.logger.<channel_name>
How can I inject these services into my service with DI injection and autowiring ?
class FooService
{
public function __construct(LoggerInterface $loggerInterface) { }
}
Yaml
#existing
foo_service:
class: AppBundle\Services\FooService
arguments: ["#monolog.logger.barchannel"]
# what I want to do
foo_service:
autowire: true # how to inject #monolog.logger.barchannel ?
Starting from MonologBundle 3.5 you can autowire different Monolog
channels by type-hinting your service arguments with the following
syntax: Psr\Log\LoggerInterface $<channel>Logger. For example, to
inject the service related to the app logger channel use this:
public function __construct(LoggerInterface $appLogger)
{
$this->logger = $appLogger;
}
https://symfony.com/doc/current/logging/channels_handlers.html#monolog-autowire-channels
I wrote (maybe more complicated) method. I don't want to tag my autowired services to tell symfony which channel to use.
Using symfony 4 with php 7.1.
I built LoggerFactory with all additional channels defined in monolog.channels.
My factory is in bundle, so in Bundle.php add
$container->addCompilerPass(
new LoggerFactoryPass(),
PassConfig::TYPE_BEFORE_OPTIMIZATION,
1
); // -1 call before monolog
This is important to call this compiler pass before monolog.bundle because monolog after pass removes parameters from container.
Now, LoggerFactoryPass
namespace Bundle\DependencyInjection\Compiler;
use Bundle\Service\LoggerFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class LoggerFactoryPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
* #param ContainerBuilder $container
* #throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* #throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
*/
public function process(ContainerBuilder $container): void
{
if (!$container->has(LoggerFactory::class) || !$container->hasDefinition('monolog.logger')) {
return;
}
$definition = $container->findDefinition(LoggerFactory::class);
foreach ($container->getParameter('monolog.additional_channels') as $channel) {
$loggerId = sprintf('monolog.logger.%s', $channel);
$definition->addMethodCall('addChannel', [
$channel,
new Reference($loggerId)
]);
}
}
}
and LoggerFactory
namespace Bundle\Service;
use Psr\Log\LoggerInterface;
class LoggerFactory
{
protected $channels = [];
public function addChannel($name, $loggerObject): void
{
$this->channels[$name] = $loggerObject;
}
/**
* #param string $channel
* #return LoggerInterface
* #throws \InvalidArgumentException
*/
public function getLogger(string $channel): LoggerInterface
{
if (!array_key_exists($channel, $this->channels)) {
throw new \InvalidArgumentException('You are trying to reach not defined logger channel');
}
return $this->channels[$channel];
}
}
So, now you can inject LoggerFactory, and choose your channel
public function acmeAction(LoggerFactory $factory)
{
$logger = $factory->getLogger('my_channel');
$logger->log('this is awesome!');
}
After some searching I have found some kind of workaround using tags and manually injecting several parameters to autowired service.
My answer looks similar to #Thomas-Landauer. The difference is, I do not have to manually create logger service, as the compiler pass from monolog bundle does this for me.
services:
_defaults:
autowire: true
autoconfigure: true
AppBundle\Services\FooService:
arguments:
$loggerInterface: '#logger'
tags:
- { name: monolog.logger, channel: barchannel }
You can use the bind parameter:
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: true
bind:
$loggerMyApi: '#monolog.logger.my_api'
Then you can use it in your service's constructor:
use Psr\Log\LoggerInterface;
...
public function __construct(LoggerInterface $loggerMyApi)
{
...
}
I didn't find a way to autowire the very logger channel. However, I found a way to use autowire in principle, and inject just the logger manually. With your class FooService, this is how services.yml could look like (Symfony 3.3):
# services.yml
services:
_defaults:
autowire: true
autoconfigure: true
AppBundle\Services\FooService:
arguments:
$loggerInterface: '#monolog.logger.barchannel'
So the "trick" is to inject the logger channel explicitly, while still having all other dependencies of this service injected through autowiring.
From the documentation it is now possible to autowire based on the type hinting of the argument name.
// autowires monolog with "foo" channel
public function __construct(\Psr\Log\LoggerInterface $fooLogger);
Essentially, you've got two options:
First, service tagging:
services:
App\Log\FooLogger:
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: foo }
Then you can use your CustomLogger as a dependency elsewhere
Second, you can rely on Monolog to auto-register loggers for each custom channel within the configuration:
# config/packages/prod/monolog.yaml
monolog:
channels: ['foo', 'bar']
You will then have these services available: monolog.logger.foo, 'monolog.logger.bar'
You can then retrieve them from the service container, or wire them in manually, e.g:
services:
App\Lib\MyService:
$fooLogger: ['#monolog.logger.foo']
You can read more here and here.
Recently I was implement single point access to the all registered loggers by MonologBundle.
And also I tried to do some better solution - and did auto-generated logger decorators. Each class decorates one object of one of the registered monolog channel.
Link to the bundle adrenalinkin/monolog-autowire-bundle
For those still struggling with this one.
In Symfony 4.3, I had, on top of that, add an alias for the specific channel, because without that, it was working only on the dev environment : when building, the Unit Tests were all failing because the custom logger was an undefined service.
monolog.logger.my_custom_logger:
alias: Psr\Log\LoggerInterface
public: true
App\Logger\MyLogger:
arguments:
$logger: '#monolog.logger.my_custom_logger'
I have such configuration file:
src/AppBundle/services.yml
#imports:
# - { resource: '../../app/config/config.yml' }
parameters:
#laikinas, tikras yra config.yml
app_url: http://app.guru
services:
UserManagement:
class: Tests\AppBundle\SharedCode\UserManagement\UserManagement
arguments: [%app_url%]
UserRegistrationContext:
class: Tests\AppBundle\features\user_registration\bootstrap\UserRegistrationContext
arguments: ['#UserManagement']
tests/AppBundle/features/user_registration/bootstrap/UserRegistrationContext.php
<?php
namespace Tests\AppBundle\features\user_registration\bootstrap;
use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Tests\AppBundle\SharedCode\UserManagement\UserManagement;
use AppBundle\Controller\UserController;
use Tests\AppBundle\features\BaseContext;
/**
* Defines application features from the specific context.
*
* To run:
* sudo vendor/behat/behat/bin/behat
* tests/AppBundle/features/user_registration/user_registration.feature
* --stop-on-failure
*/
class UserRegistrationContext extends BaseContext implements Context, SnippetAcceptingContext
{
private $userManagement;
/**
* UserRegistrationContext constructor.
*/
public function __construct(UserManagement $userManagement)
{
//$this->userManagement = new UserManagement();
$this->userManagement = $userManagement;
parent::__construct();
}
}
I run behat tests and get an error:
vagrant#php7dev:/shared$ sudo vendor/behat/behat/bin/behat tests/AppBundle/features/user_registration/user_registration.feature
Fatal error: Uncaught Symfony\Component\Debug\Exception\FatalThrowableError: Type error: Argument 1 passed to Tests\AppBundle\features\user_registration\bootstrap\UserRegistrationContext::__construct() must be an instance of Tests\AppBundle\SharedCode\UserManagement\UserManagement, none given in /shared/tests/AppBundle/features/user_registration/bootstrap/UserRegistrationContext.php:30
Stack trace:
#0 [internal function]: Tests\AppBundle\features\user_registration\bootstrap\UserRegistrationContext->__construct()
#1 /shared/vendor/behat/behat/src/Behat/Behat/Context/ContextFactory.php(123): ReflectionClass->newInstance()
#2 /shared/vendor/behat/behat/src/Behat/Behat/Context/ContextFactory.php(80): Behat\Behat\Context\ContextFactory->createInstance(Object(ReflectionClass), Array)
#3 /shared/vendor/behat/behat/src/Behat/Behat/Context/Environment/Handler/ContextEnvironmentHandler.php(104): Behat\Behat\Context\ContextFactory->createContext('Tests\\AppBundle...', Array)
#4 /shared/vendor/behat/behat/src/Behat/Testwork/Environme in /shared/tests/AppBundle/features/user_registration/bootstrap/UserRegistrationContext.php on line 30
We can see that in services.yml I have given the parameter. What is wrong?
For services.yml to be read, as I understand I need exctension class, here it is:
src/AppBundle/DependencyInjection/AppExtension.php
<?php
namespace AppBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
class AppExtension extends Extension
{
/**
* #param array $configs configs
* #param ContainerBuilder $container container
* #return null
*/
public function load(array $configs, ContainerBuilder $container)
{
// ... you'll load the files here later
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__ . '/../')
);
$loader->load('services.yml');
}
}
Thank you so much Matteo, so awesome, from his comment alone was able to fix the problem.
It turns out that I had to use Symfony2 Extension and configure dependencies in behat.yml instead of my config. Here is how behat.yml looks now:
default:
autoload:
'': %paths.base%/tests/AppBundle/features/user_registration/bootstrap
formatters:
progress: ~
suites:
app_features:
paths: [ %paths.base%//tests/AppBundle/features ]
contexts:
- Tests\AppBundle\features\user_registration\bootstrap\UserRegistrationContext:
userManagement: '#UserManagement'
- Tests\AppBundle\features\user_login\bootstrap\UserLoginContext:
userManagement: '#UserManagement'
- Tests\AppBundle\features\password_reset\bootstrap\PasswordResetContext:
userManagement: '#UserManagement'
extensions:
Behat\Symfony2Extension: ~
And I even commented out UserManagement from src/AppBundle/services.yml and it finds it somehow, I do not understand how actually.
And here is something written about this, I googled again for symfony3 behat dependency injection after I solved the problem:
http://docs.behat.org/en/v3.0/cookbooks/1.symfony2_integration.html
I remember I saw this page before, but this was not in my head when solving this problem. Maybe because in the example there was Session being injected which is symfony component, while UserManagement class was my created component.
Update:
Done from scratch and will give minimal versions of files how they look:
behat.yml has to be in the root of the project. http://docs.behat.org/en/v3.0/cookbooks/1.symfony2_integration.html
default:
suites:
default:
contexts:
- FeatureContext:
userRepository: "#user_repository"
extensions:
Behat\Symfony2Extension: ~
features/bootstrap/FeatureContext.php
use AppBundle\Repository\UserRepository;
use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
/**
* Defines application features from the specific context.
*/
class FeatureContext implements Context, SnippetAcceptingContext
{
private $userRepository;
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
//other methods
}
Pay attention that there has to be matches of keys - if there is such key:
userRepository: "#user_repository"
then in constructor the variable has to be named
$userRepository
I want to make
behat.yml -
default:
extensions:
Behat\MinkExtension\Extension:
base_url: 'my-url'
a parameter pulled from parameters.yml... Is this possible? I made a mink_base_url parameter in parameters.yml and then added
imports:
- { resource: parameters.yml }
to behat.yml. No matter what I do, I get this
[Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException]
The service "behat.mink.context.initializer" has a dependency on a non-existent parameter "mink_base_url"
Behat configuration is in no way related to Symfony's. It's true that Behat uses Symfony's DI container, but it's a separate instance.
If wanted to implement it, you'd probably need to create your own Behat extension to support the imports section.
This worked for me with Symfony 3. Just omit base_url from behat.yml, and set it from the container parameters. Thanks to #DanielM for providing the hint.
<?php
use Behat\MinkExtension\Context\MinkContext;
use Symfony\Component\DependencyInjection\ContainerInterface;
class FeatureContext extends MinkContext {
/**
* FeatureContext constructor.
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* #BeforeScenario
*/
public function setUpTestEnvironment()
{
$this->setMinkParameter('base_url', $this->container->getParameter('my_url'));
}
}
It is possible to access the symfony parameters within behat yaml as using
- '%%name_of_the_parameter%%'
Double percentage sign (%%) does the trick.
If you just want to access base_url, you can get it once mink has been started.
$this->getMinkParameter('base_url');
Here's an example :
class AbstractBehatContext extends MinkContext {
/**
* The base url as set behat.yml
* #var bool
*/
protected $baseUrl;
/**
* #BeforeScenario
*/
public function getBaseUrl() {
$this->baseUrl = $this->getMinkParameter('base_url');
}
}
Note, this needs to be able to access Mink, so it won't work in __construct or in #BeforeSuite. Additionally #BeforeScenario will be called at the start of every scenario which is going to set it pointlessly a lot.