How to serve same functionality with different layouts in a Symfony2 app? - symfony

I have in a Symfony2 application the following bundle architecture:
CommonBundle
FirstBundle
SecondBundle
Several features are implemented in the CommonBundle.
These features have to be available in the 2 other bundles.
The FirstBundle and SecondBundle have therefore their own features + the ones of the CommonBundle. These bundles each have their own host defined in the main application routing.yml file.
What I'm trying to do:
Features of the CommonBundle should be displayed with the layout of the current bundle.
For instance, if I hit http://firstbundle.myapp.com/common/feature1, I should see the layout of the FirstBundle bundle.
And if I hit http://secondbundle.myapp.com/common/feature1, the layout of the SecondBundle bundle should be used.
How can I do that?
I can't use bundle inheritance as the same bundle can't be extended twice.
In my current implementation, each bundle imports the routes of the CommonBundle in its own host.

You should create a controller response listener and change the template name depending on the request hostname in there.
A good read is the How to setup before/after filters chapter of the documentation.
You could aswell use a twig extension registering a global variable and decide which template to extend inside your base template:
config.yml
services:
twig.extension.your_extension:
class: Vendor\YourBundle\Twig\Extension\YourExtension
arguments: [ #request ]
tags:
- { name: twig.extension, alias: your_extension }
YourExtension.php
use Symfony\Component\HttpFoundation\Request;
class YourExtension extends \Twig_Extension
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function getGlobals()
{
// some logic involving $this->request
$baseTemplate = ($this->request->getHost() === 'first.host.tld') ? 'FirstBundle::base.html.twig' : 'SecondBundle::base.html.twig';
return array(
'base_template' => $baseTemplate,
);
}
public function getName()
{
return 'your_extension';
}
base.html.twig
{% extends base_template %}

Related

Drupal 8 Custom Twig Extension service causes drush abnormal termination

I'm adding a custom twig extension service to a Drupal 8 module. My services file looks like this:
services:
analytics.my_twig_extension:
class: Drupal\analytics\TwigExtension\MyTwigExtension
tags:
- { name: twig.extension }
I get this error when running drush cr:
[warning] Drush command terminated abnormally. Check for an exit()
in your Drupal site.
When I remove the tags property in services file, like this:
services:
analytics.my_twig_extension:
class: Drupal\analytics\TwigExtension\MyTwigExtension
then drush cr works correctly, but my Twig extension functions are not not running at all.
The MyTwigExtension class:
<?php
namespace Drupal\analytics\TwigExtension;
use Twig_Extension;
use Twig_SimpleFilter;
class MyTwigExtension extends \TwigExtension {
public function __construct() {
}
public function getFunctions() {
return [
new \Twig_SimpleFunction('get_type', array($this, 'getType'))
];
}
public function getType($var) {
return gettype($var);
}
}
?>
Does anyone have any idea why this is happening?
Jacob, you're a dummy.
I fixed it by simply using extends \Twig_Extension instead of extends \TwigExtension. A coworker found the answer. Unfortunately, there was no indication in the logs that this was the problem.

Symfony Custom Error Page By Overriding ExceptionController

what I am trying to do is to have custom error page, not only will they be extending the base layout but also I want extra up selling content in those pages too so changing templates only is not an option
regardless of the reason (404 Not Found or just missing variable) I would like to show my template and my content instead
I have spent hours trying to get this going with no luck
app/console --version
Symfony version 2.5.6 - app/dev/debug
I tried some resources, but couldn't get it working. The name a few:
http://symfony.com/doc/current/reference/configuration/twig.html
http://symfony.com/doc/current/cookbook/controller/error_pages.html
I'm running in dev with no debug, see app_dev.php below:
$kernel = new AppKernel('dev', false);
following the tutorials i got these extra bits
app/config/config.yml
twig:
exception_controller: SomethingAppBundle:Exception:show
in my bundle
<?php
namespace Something\AppBundle\Controller;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpKernel\Exception\FlattenException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class ExceptionController extends Controller
{
public function showAction( FlattenException $error, DebugLoggerInterface $debug)
{
print_r($error);
}
}
but my error controller does not get executed,
I am on purpose causing error by trying to echo undefined variable in different controller, since it should handle error from entire application
At the beginning you need to create action in the controller:
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class ErrorController extends Controller
{
public function notFoundAction()
{
return $this->render('error/404.html.twig');
}
}
Then you need to create a Listener:
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class NotFoundHttpExceptionListener
{
private $controller_resolver;
private $request_stack;
private $http_kernel;
public function __construct($controller_resolver, $request_stack, $http_kernel)
{
$this->controller_resolver = $controller_resolver;
$this->request_stack = $request_stack;
$this->http_kernel = $http_kernel;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
if ($event->getException() instanceof NotFoundHttpException) {
$request = new \Symfony\Component\HttpFoundation\Request();
$request->attributes->set('_controller', 'AppBundle:Error:notFound');
$controller = $this->controller_resolver->getController($request);
$path['_controller'] = $controller;
$subRequest = $this->request_stack->getCurrentRequest()->duplicate(array(), null, $path);
$event->setResponse($this->http_kernel->handle($subRequest, HttpKernelInterface::MASTER_REQUEST)); // Simulating "forward" in order to preserve the "Not Found URL"
}
}
}
Now register the service:
#AppBundle/Resources/config/services.yml
services:
kernel.listener.notFoundHttpException:
class: AppBundle\EventListener\NotFoundHttpExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: -10 }
arguments: [ #controller_resolver, #request_stack, #http_kernel ]
Not tested this, but rather it should work;)
EDIT:
Tested, it works. On the rendered page, you have a session, so you have access to app.user, his cart, and other matters related to the session.

Inject service in symfony2 Controller

How can I inject a service (the service that I created) into my Controller?
A setter injection would do.
<?php
namespace MyNamespace;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
public function setMyService(MyService $myService)
{
$this->myService = $myService;
}
public function indexAction()
{
//Here I cannot access $this->myService;
//Because the setter is not called magically!
}
}
And my route settings :
// Resources/routing.yml
myController_index:
pattern: /test
defaults: { _controller: "FooBarBundle:MyController:index" }
I'm setting the service in another bundle :
// Resources/services.yml
parameters:
my.service.class: Path\To\My\Service
services:
my_service:
class: %my.service.class%
When the route is resolved, the service is not injected ( I know it shouldn't ).
I suppose somewhere in a yml file, I have to set:
calls:
- [setMyService, [#my_service]]
I am not using this Controller as a service, it's a regular Controller that serves a Request.
Edit: At this point in time, I am getting the service with $this->container->get('my_service'); But I need to inject it.
If you want to inject services into your controllers, you have to define controllers as services.
You could also take a look at JMSDiExtraBundle's special handling of controllers — if that solves your problem. But since I define my controllers as services, I haven't tried that.
When using the JMSDiExtraBundle you DON'T have to define your controller as a service (unlike #elnur said) and the code would be:
<?php
namespace MyNamespace;
use JMS\DiExtraBundle\Annotation as DI;
use Path\To\My\Service;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
/**
* #var $myService Service
*
* #DI\Inject("my_service")
*/
protected $myService;
public function indexAction()
{
// $this->myService->method();
}
}
I find this approach very nice because you avoid writing a __construct() method.
Since it's 2017 ending now and there is no tag for Symfony 3 or upcoming Symfony 4 (and I think there should not be), this problem is solvable in a native much better way.
If you are still struggling and somehow ended up on this page and not in Symfony docs, then you should know, that you do not need to declare controller as service, as it is already registered as one.
What you need to do, is check you services.yml:
# app/config/services.yml
services:
# default configuration for services in *this* file
_defaults:
# ...
public: false
Change public: false to public:true if you want all services to be public.
Or explicitly add a service and declare it public:
# app/config/services.yml
services:
# ... same code as before
# explicitly configure the service
AppBundle\Service\MessageGenerator:
public: true
And then in your controller you can get the service:
use AppBundle\Service\MessageGenerator;
// accessing services like this only works if you extend Controller
class ProductController extends Controller
{
public function newAction()
{
// only works if your service is public
$messageGenerator = $this->get(MessageGenerator::class);
}
}
Read more:
Public vs Private services
Services in Container
If you don't want to define your controller as a service, you can add a listener to the kernel.controller event to configure it just before it is executed. This way, you can inject the services you need inside your controller using setters.
http://symfony.com/doc/current/components/http_kernel/introduction.html#component-http-kernel-kernel-controller

Symfony2, autoload service in service

Question is simple but...
So we have main service:
class ManagerOne {}
and have several another services we want to use in main service:
class ServiceOne{}
class ServiceTwo{}
class ServiceThree{}
class ServiceFour{}
...
Each named as (in services.yml)
service.one
service.two
service.three
service.four
...
Locations of services is different, not in one folder (but I don't think it's a huge trouble for custom autoloader).
Regarding manual we can inject them via __construct() in main service (ManagerOne) but what if we got 20 such services need to be injected? Or use only that we need. Describe them in services as simple inject? O.o I think it's not good idea so.... Also we can inject container and that's it. BUT! Everywhere people saying that inject container worst solution.
What I want. I need method for ManagerOne service which will load service i need by 'service.name' or 'path' with checker 'service exist'.
You could use service tagging and tag each service you want to use in your ManagerOne class. And either use constructor dependency injection or method injection.
Example:
First of all you need a compiler pass to collect your tagged services:
namespace ...\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ExamplePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition("manager.one")) {
return;
}
$services = array();
foreach ($container->findTaggedServiceIds('managed_service') as $serviceId => $tag) {
$alias = isset($tag[0]['alias'])
? $tag[0]['alias']
: $serviceId;
// Flip, because we want tag aliases (= type identifiers) as keys
$services[$alias] = new Reference($serviceId);
}
$container->getDefinition('manager.one')->replaceArgument(0, $services);
}
}
Then you need to add the compiler pass to your bundle class:
namespace Example\ExampleBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use ...\DependencyInjection\Compiler\ExamplePass;
class ExampleBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ExamplePass());
}
}
Now you can use your services:
# services.yml
manager.one:
class: ManagerClass
arguments:
- [] # will be replaced by our compiler pass
services.one:
class: ServiceOne
tags:
- { name: managed_service, alias: service_one }
services.two:
class: ServiceTwo
tags:
- { name: managed_service, alias: service_two }
But caution if you get your manager, all service classes will be automatically created. If this is a performance drawback for you could pass only the service ids (not the Reference) to your management class. Add the #service_container as second argument and create the service as needed.
Since 2017, Symfony 3.3 and Symplify\PackageBuilder this gets even easier.
Thanks to this package, you can:
drop tags
have simple 5 line CompilerPass using strict types over strings
Let's get to your example
Suppose you have
1 manager - UpdateManager class
many updaters - a class that implements UpdaterInterface
1. Service Config using PSR-4 autodiscovery
# app/config/services.yml
services:
_defaults:
autowire: true
App\:
resource: ../../src/App
2. Collecting Compiler Pass
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symplify\PackageBuilder\Adapter\Symfony\DependencyInjection\DefinitionCollector;
final class CollectorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder)
{
DefinitionCollector::loadCollectorWithType(
$containerBuilder,
UpdateManager::class,
UpdaterInterface::class,
'addUpdater'
);
}
}
It collect all services of UpdaterInterface type
and adds them via addUpdater() method to UpdateManager.
3. Register Compiler Pass in Bundle
namespace App;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class UpdaterBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new CollectorCompilerPass);
}
}
And that's all!
How to add new updater?
Just create class, that implements UpdaterInterface and it will be loaded to UpdateManager.
no tagging
no manual service registration
no boring work
Enjoy!

How to get the $view outside of controller

I understand that the $view can be accessed within a rendered template file, but I wonder if there is anyway I can get it elsewhere?
The reason is that I'm building SF2 on top of a classic/legacy system and there are certain templates that cannot be rendered with the normal render method. However, I still want to be able to access the view helper inside those template, so I want to init a global $view which can be used in these legacy templates
These helpers are provided in the PhpEngine class in the Templating component (docs). You can acces this class with the templating.engine.php service (you need to have PHP enabled as a templating engine).
From a controller it will look like this:
// ...
public function fooAction(...)
{
// ...
$template = $this->get('templating.engine.php')->render(...);
}
If you are using another class, you should use DI:
// src/Acme/DemoBundle/Foo.php
namespace Acme\DemoBundle;
use Symfony\Component\Templating\EngineInterface;
class Foo
{
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
public function bar()
{
// ...
$template = $this->templating->render(...);
}
}
// app/config.yml
services:
acme_demo:
foo:
class: Acme\DemoBundle\Foo
arguments: [#templating.engine.php]
More about DI in the docs.

Resources