How to override bundle templates and controllers (FOSUserBundle) without using bundle inheritance - symfony

In my Symfony 2.8 based project I use FOSUserBundle and extended it with a custom MyUserBundle using bundle inheritance.
When trying to update FOSUserBundle from 2.0.2 to 2.1.2 I came across several problems.
Since Bundle inheritance is deprecated in Symfony 3.4 and completely dropped in Symfony 4.0 I am now trying to achieve the same result without using bundle inheritance.
I found many information that shows, that bundle resources can be overridden by simply placing the files in the app/Resources/<BundleName>/.... Thus the FOSUserBundle templates could be overridden by placing them in app/Resources/FOSUserBundle/views/<template.html.twig>
While this will work, it does not deliver the same result as bundle inheritance. I can use my inherited bundle in different projects and thus reuse the same code over and over again. Using the app/Resources/<BundleName>/... solution would only work for the current project.
Question 1: How to override templates (and other resources) within a custom bundle which can be used in different projects rather than within a project specific app/Resources folder?
Question 2: How to override the behavior of controllers which to not offer event to to do so?
Currently I am using bundle inheritance to override the confirmedAction to send the user to a custom setup page rather than to the default page:
// Original FOSUserBundle code
public function confirmedAction(Request $request) {
$user = $this->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw new AccessDeniedException('This user does not have access to this section.');
}
return $this->render('#FOSUser/Registration/confirmed.html.twig', array(
'user' => $user,
'targetUrl' => $this->getTargetUrlFromSession($request->getSession()),
));
}
// MyUserBundle codes which should override the default behavior
public function confirmedAction(Request $request) {
$user = $this->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw new AccessDeniedException('This user does not have access to this section.');
}
// Send user to setup-page
return $this->redirectToRoute('app_user_setup');
}
How can I control the behavior in the same way and send the user to a setup page instead of to the default confirmation page without using bundle inheritance?
As far as I know FOSUserBundle does not provide an option to specify the confirmation target. Of course I could intercept the request to the confirmed-route using some request event listener and re-route it. But this would be quite hacky. Would this be the right solution to do this or is there a cleaner way to achieve the same?

Overriding resources within a bundle since Symfony 4.0 without bundle inheritance
About Controllers
Routes! is the way to achieve it now, "the first route found wins" so make sure to define the same route before the original one, with your custom controller. e.g.:
<route id="fos_user_registration_confirmed" path="/confirmed" methods="GET">
<default key="_controller">MyUserBundle\Controller\RegistrationController::confirmedAction</default>
</route>
Then, your routing file MyUserBundle/Resources/config/routing/registration.xml must be imported before the FOSUserBundle routing file. This way, your custom action will be executed instead of the original fos_user.registration.controller:confirmedAction.
About Templates
Again, "the first template found wins" so make sure your bundle views path is defined before the one in FOSUser Twig namespace. You can achieve it by creating a compiler pass:
// MyUserBundle/DependencyInjection/Compiler/TwigPass.php
$path = dirname(__DIR__, 2).'/Resources/views';
$twigFilesystemLoaderDefinition = $container->findDefinition('twig.loader.native_filesystem');
$twigFilesystemLoaderDefinition->addMethodCall('prependPath', array($path, 'FOSUser'));
Check the Twig loader paths by running bin/console debug:twig, it should look like this:
#FOSUser - vendor/acme/my-user-bundle/Resources/views
- vendor/friendsofsymfony/user-bundle/Resources/views
From here, you're able to override any template of FOSUserBundle like Registration/confirmed.html.twig when #FOSUser/Registration/confirmed.html.twig is called.

Related

Twig is_granted fails in Behat scenario

I have this Behat setup:
default:
extensions:
Behat\Symfony2Extension: ~
Behat\MinkExtension:
sessions:
default:
symfony2: ~
And this scenarion:
Scenario: Event list for authenticated user
Given I am authenticated
Then I should see pagination control
And I should be able to change list page
I check if the user is authenticated and if so show him pagination control in Twig:
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
...
Related Behat context:
/**
* #Given I am authenticated
*/
public function iAmAuthenticated()
{
$user = new User('test', null, ['ROLE_USER']);
$token = new UsernamePasswordToken($user, null, 'test', $user->getRoles());
$this->getTokenStorage()->setToken($token);
}
/**
* #Then I should see pagination control
*/
public function iShouldSeePaginationControl()
{
$this->assertSession()->elementExists('css', 'ul.pagination');
}
I get true for
$this->kernel
->geContainer()
->get('security.authorization_checker')
->isGranted('IS_AUTHENTICATED_FULLY')
in my iShouldSeePaginationControl() but it is false in rendered content.
What am I missing?
My guess is that you're using a different instance of the container in your behat step and in your template.
AFAIR, the symfony2 driver uses BrowserKit under the hood to navigate through your website. The container which will be used in your web page will then be instanciated by the PHP Engine of your Web server (and not by Behat). If so, it is absolutely impossible to operate modifications in the container at runtime in a step and expect that the web server will be aware of them.
Easy solution would be to actually log in in the behat step (through the web interface) instead of setting the token manually.
Another harder way, if you absolutely want to login programatically, would be to serialize the created token on HDD and register some kind of logic (a kernel.request listener for example) that will check if this file is available and inject the unserialized token in the security context. If you do so, MAKE SURE that you enable this logic in TEST environment only, as it potentially is a security breach.
The problem is you have running 2 instances of Symfony:
One core for Behat, that was initialized.
Second, initialized by apache/nginx that was triggered by Mink connection to the server.
Solution
For that, we had a solution in another project (with Zend).
We created service, that created an additional configuration to authorization:
if a file exists and the project was in DEV mode, then it was loaded in the initialization step.
Then in hook/step we could call service that generates a file like that and after scenario, delete it. This way, you could have any logged user in your project.
Another way is to call steps that will log you into your project via a standard form.

Does symfony 2.8 load bundles and services in correct order during warmup?

I use Propel to connect and query database and after upgrading from symfony 2.7 to symfony 2.8 I run into strange issue. When I completely remove content of cache directory and then run app/console command I got this exception:
[PropelException]
No connection information in your runtime configuration file for datasource
[default]
This also happens everytime when I change configuration or translation files and then I refreshe page. When I do this once again it works fine though.
I figured out that this is caused by one of my service that is passed to twig as global variable like this in app/config/config.yml:
# Twig Configuration
twig:
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"
globals:
settings: #app.settings
The service try to loads data from database in constructor:
<?php
namespace AppBundle\Services;
use AppBundle\Propel\SettingsQuery;
class PropelSettings {
private $settings = array();
public function __construct() {
$this->initialize();
}
public function initialize() {
$settings = SettingsQuery::create()
->select(array('key', 'value'))
->find();
foreach ( $settings as $s ) {
$this->settigns[$s['key']] = $s['value'];
}
}
public function get($key, $default = null) {
return isset($this->settings[$key]) ? $this->settings[$key] : $default;
}
}
but this happens before propel load configuration by calling boot() method in vendor/propel/propel-bundle/Propel/PropelBundle/PropelBundle.php, which means that symfony try to create instance of service before it actually loads all bundles during warmup. This is not an issue in symfony 2.7 and erlier versions. This can be easily avoided by not querying database during construction of service, but is this correct behavior of symfony 2.8? Is there any way to force propel to be booted before symfony create any instance of service?
Just try moving your initialization code out of the constructor (I know you already know that). Symfony does more during the cache warmup process than it did in 2.7. This service is instantiated likely due the Twig templates being warmed up. The probably issue is that Propel itself probably needs some cache to exist in order for it to be booted... or perhaps there's some code that helps boot Propel correctly during the request-response cycle.
In short: this looks like a quirk of Propel to me, but someone who knows more about Propel might know a way to forcefully "boot" it. Propel is special because of the static context it uses.
I hope that clarifies a little!

Conflicts in routing in symfony between bundles

Playing around with symfony. I have two bundles and each bundle has a controller within it. Just to see how routing works I gave the same path to functions within both controllers. Bundle B was the newly created bundle and when the URL app/simple was hit I got a response from bundle B always. Just curious as to whether there is any logic behind this.
PS: I know this is bad practice but just wanted to see how the guts of routing in works.
/**
* #Route("/app/simple", name="homepage")
*/
public function indexAction()
{
return new Response('Hello From bundle A!');
}
In bundle B
/**
* #Route("/app/simple", name="homepage")
*/
public function indexAction()
{
return new Response('Hello From bundle B!');
}
Your app has a single routing configuration which can include other configurations. Probably app/config/routing.yml.
That configuration file will include the routes for your bundles by using the resource key that can import routes from another routing.yml file or from annotations in a PHP controller.
The order of those will determine which route gets chosen since Symfony2 always uses the first matching route.

Access to Doctrine during Bundle Initialization

I have a Symfony2 bundle which I want to use database table which stores key value configuration parameters. I want to be able to load a query and cache it for a long time and be able to inject the configuration parameters into symfony2 service container.
Right now I am injecting a service which loads the configuration from doctrine, and calling a get($key) method to retrieve the value for the key I want.
I basically want these configuration options to be available from the symfony2 service container parameter bag.
Is there maybe an event I could tie into or some sort of compiler pass I can use with my bundle to achieve this?
I'll do something like that in your service listener
public function onLateKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$mydata= $this->manager->getRepository('YourBundle:YourTable')->getAll();
$parameters['mydata'] = $mydata;
$request->attributes->add($parameters);
}
In your Controller, you can get your parameters :
$this->container->get('request')->attributes->get('mydata');

Symfony Dev Toolbar not loading (wrong route)

I'm building a CMS style application with dynamic routing. So far it seems to work fine but the output doesn't honor the environment for WDT, assets and links... it always links to /whatever instead of /app_dev.php/whatever.
Dynamic routing is implemented via kernel.request listener. The relevant code is on gist. Do I need to pass the current environment to Twig at some moment?
Edit:
The problem appears when in DEV mode... no problem when in production mode.
Thanks to #AdrienBrault I finally solved the issue by using a Controller to do the rendering.
In the RequestListener I added a new route to the RouteCollection of the router indicating the controller which will handle the request.
$route = new Route($path, array(
'_controller' => 'CmsBundle:Routing:routing',
));
$this->router->getRouteCollection()->add('cms', $route);
In the controller it was a bit harder to get the original URL, but finally I solved it by querying the routers RouteCollection with the name assigned in the previous add() method.

Resources