I'm quiet new to symfony and spend hours trying to find a solution.
I'm building a multilingual website where page slugs are differents.
For example :
www.mywebsite.com/products EN will be www.mywebsite.com/produits FR but both use the same controller
I have to build a dynamic route and here is the way I did I'm pretty sure I can do better, could you help me?
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class websiteController{
public function __construct(){
$this -> route = array(
'about' => 'page_about',
'contact' => 'page_contact',
);
}
/**
* #Route("/{page}", name="page")
*/
public function pageAction($page)
{
if($page == $this -> route['about']){
return new Response('<html><body>page about</body></html>');
}
if($page == $this -> route['contact']){
return new Response('<html><body>page contact</body></html>');
}
}
}
?>
There is a bundle for routing internationalization called BeSimpleI18nRoutingBundle but it is not available for symfony 4 right now.
Core symfony implementation
With core symfony you could use multiple routes for each controller, that would have a {_locale} parameter with default value, here the problem would be that two different URLs would be returning the same page.
e.g /test would be the same as /test/en
This might cause problems with SEO
here how the annotations would look like if you wish to implement this method
/**
* #Route("/test/{_locale}", defaults={"_locale"="en"}, requirements={"_locale":"en"}, name="default_en")
* #Route("/δοκιμή/{_locale}", defaults={"_locale"="el"}, requirements={"_locale":"el"}, name="default_el", options = {"utf8": true})
* #Route("/tester/{_locale}", defaults={"_locale"="fr"}, requirements={"_locale":"fr"}, name="default_fr")
*/
public function test($_locale)
{
return new Response("Your current locale is : $_locale");
}
Dynamic Route
Another option is to create a Routing Service that would apply your logic.
Here is an example.
this would be the controller that handles all the paths
Controller
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use App\Service\Router;
class RouterController extends Controller {
/**
* #Route("/{path}", name="router", requirements={"path" = ".+"})
*/
public function router($path,Request $request,Router $router) {
$result=$router->handle($path);
if($result){
$result['args']['request']=$request;
return $this->forward($result['class'], $result['args']);
}
throw $this->createNotFoundException('error page not found!');
}
}
This Controller Action depends on a service called Router so you will have to create a Router service that would return the an array (you can change it to return a custom object) with keys class and args that would be used to forward the request to a controller action.
Service
/src/Service/Router.php
Here you should implement a function called handle and you can apply any logic to it
here is a basic example
namespace App\Service;
class Router
{
public function handle($path)
{
switch ($path) {
case "test":
return [
"class" => "App\Controller\TestController::index",
"args" => [
"locale" => 'en'
]
];
case "tester":
return [
"class" => "App\Controller\TestController::index",
"args" => [
"locale" => 'fr'
]
];
default:
return false;
}
}
}
The code above would forward the request to TestController::index function and will add as parameter to that function the locale variable and also it will include the Request object
You could store the routes in a yaml file or database or any other location you like. You can manipulate the $path variable to extract information about id, page etc.
Related
What is the right way to pass more variables to FOSUserBundle settings twig template (Profile/show_content.html.twig) in Symfony 3.4?
I basically want to rewrite showAction() method and pass more than user variable ti twig template.
I tried to following this tutorial. It seems it does no longer work with Symfony 3.4
The way I do it (and there might be better methods) is simply create a new controller with a route to the original 'show route', together with the variables I want to pass. Here is an example of the showAction() with an extra variable rendered_address:
namespace App\Controller;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class ProfileController extends Controller
{
/**
* Show the user.
* #Route("/profile/show")
*/
public function showAction()
{
$user = $this->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw new AccessDeniedException('This user does not have access to this section.');
}
$address = $this->getUser()->renderAddress(); // here is get my variable
return $this->render('#FOSUser/Profile/show.html.twig', array(
'user' => $user,
'rendered_address' => $address // here is pass my variable
));
}
}
I want to create event listener that add some results of db query to all symfony actions
for example:
class BlogController extends Controller
{
/**
* #Route("/blog/")
* #Template()
*/
public function indexAction()
{
....
return array(
'entries' => $posts
);
}
}
This controller is passing entries variable to the view, I want to create listener that take the returned value of all actions and inject another index to the returned array to be (for example)
array(
'entries' => $posts,
'categories' => $categories
);
so I can call the $categories var from any where in my application views
I hope my question is clear to you guys. Thanks in advance.
You should consider creating a global variable or twig extension to make categories available in your templates, you can't do that by using events (since the template is parsed inside the controller, not before/after it)
This approach, although valid and commonly used in some frameworks, is not very common in Symfony as it suits more MVC than HMVC architecture.
I would suggest you a different one with the same result:
Instead of adding parameter to every controller return, render another controller which returns just a subview of what you're trying to show. Simple example:
// article/index.html.twig
<div class="category-bar">{{ render(controller('MyVendorMyBundle:CategoryController:bar')) }}</div>
<div class="article-list">
{% for article in articles %>
{# Print article here #}
{% endfor %}
</div>
// CategoryController
class CategoryController extends Controller
{
/**
* #Template
*/
public function barAction()
{
return ['categories' => $this->fetchCategoriesSomehow()];
}
}
So when you render your article list action, twig will fire a subrequest to render categories bar above it.
Furthermore, if you don't like making subrequests, nothing stops you from creating a twig extension service which would fetch categories and render template for you.
In most cases I would go with #Wouter J's suggestion and create a twig extension or a global variable.
However, what you want to do is actually possible (regardless if that's the right solution or not).
The #Template annotation has a vars attribute, which lets you to specify which atttributes from the request should be passed to the template:
/**
* #ParamConverter("post", class="SensioBlogBundle:Post")
* #Template("SensioBlogBundle:Post:show.html.twig", vars={"post"})
*/
public function showAction()
{
}
Note, that request attributes can be set by you:
$request->attributes->set('categories', []);
So, you could implement a listener which would set the categories attribute on the request and than configure the vars on the #Template annotation:
/**
* #Template("SensioBlogBundle:Post:show.html.twig", vars={"categories"})
*/
public function showAction(Post $post)
{
}
Have a look at the TemplateListener from the SensioFrameworkExtraBundle for more insight. The listener defines template vars on kernel.controller and uses them to render the view on kernel.view.
You could avoid defining vars on the annotation if your listener was registered after the TemplateListener::onController(). It would have to add categories to the _template_vars request attribute.
Use Twig extension to create function that will return list of available categories
<?php
class CategoriesExtension extends \Twig_Extension
{
public function getFunctions()
{
return [
new \Twig_SimpleFunction('getCategories', [$this, 'getCategoriesList'])
];
}
/**
* #return null|string
*/
public function getCategoriesList()
{
return CategoryQuery::create()->find();
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'list_categories';
}
}
You can pass parameter to function if You would like do some conditions on query.
The trick is to get the twig service in your listener and then use addGlobal to add your categories
namespace Cerad\Bundle\CoreBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MyEventListener extends ContainerAware implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
KernelEvents::CONTROLLER => array(
array('doCategories', -1100),
);
}
public function doCategories(FilterControllerEvent $eventx)
{
// Query your categories
$categories = array('cat1','cat2');
// Make them available to all twig templates
$twig = $this->container->get('twig');
$twig->addGlobal('categories',$categories);
}
# services.yml
cerad_core__my__event_listener:
class: '%cerad_core__my__event_listener__class%'
calls:
- [setContainer, ['#service_container']]
tags:
- { name: kernel.event_subscriber }
I'm wondering the best way to create fully compatible application to plug-ins.
I'm used to Wordpress plug-ins concept that you can define actions and filters and then use in your plug-ins. So others can define methods on their plug-ins that are executed when the action is called (or the filter).
My idea is create my app with some actions and filters and then other developers can build a Bundle that interfere in the "normal" app flow...
I was reading about Symfony2 Dependency Injection, but I didn’t found some comprehensive example to do something similar that I want.
Someone has a real example of something similar that I'm looking for?
Is the Dependency Injection the best solution or should I build my own plugin handler?
EDIT:
What I did to allow other bundles to add items to my knp-menu menu.
In my base bundle:
Defining the filter that allow subscribber to get and set menu data:
# BaseBundle/Event/FilterMenuEvent.php
class FilterMenuEvent extends Event
{
protected $menu;
public function __construct($menu)
{
$this->menu = $menu;
}
public function getMenu()
{
return $this->menu;
}
}
Defining the events of the menu:
# Event/MenuEvents.php
final class MenuEvents
{
const BEFORE_ITEMS = 'menu.before.items';
const AFTER_ITEMS = 'menu.after.items';
}
Setting up the subscriber:
# Event/MenuSubscriber.php
class MenuSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'menu.after.items' => array(
array('homeItems', 9000),
array('quickactionsItems', 80),
array('adminItems', 70),
...
array('logoutItems', -9000),
)
);
}
public function homeItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Home', array('route' => 'zashost_zaspanel_homepage'));
}
public function quickactionsItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Quick actions', array( 'route' => null));
$menu['Quick actions']->addChild('Add hosting', array( 'route' => 'zashost_zaspanel_register_host'));
}
}
Dispatching events in the generation of menu:
# Menu\Builder.php
class Builder extends ContainerAware
{
public function userMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$this->container->get('event_dispatcher')->dispatch(MenuEvents::AFTER_ITEMS , new FilterMenuEvent($menu));
return $menu;
}
}
Attach subscriber to kernel event subscriber:
# services.yml
services:
# Menu items added with event listener
base_menu_subscriber:
class: Acme\BaseBundle\Event\MenuSubscriber
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}
Then in third party bundle:
Setting up my third party event subscriber:
class MenuSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'menu.after.items' => array('afterItems', 55)
);
}
public function afterItems(FilterMenuEvent $menu_filter)
{
$menu = $menu_filter->getMenu();
$menu->addChild('Backups', array( 'route' => null));
$menu['Backups']->addChild('Create new backup', array( 'route' => null));
return $menu;
}
}
And attaching to kernel event subscriber:
# srevices.yml
services:
menu_subscriber:
class: Acme\ThirdPartyBundle\Event\MenuSubscriber
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}
In that way I can use the priority of Event Dispatcher to set the position of each group of items of the menu.
A good starting point in providing extension points for your application, in which other developers can hook their custom behaviour, is to use the EventDispatcher component from Symfony - a implementation of the Observer Pattern.
Symfony already uses the component extensively in it's own core ( HttpKernel ) to allow other components (or plugins, if you will) to hook in various points in the http request -> response flow and handle everything from Request matching to Response generation.
For example you can hook to the kernel.request event and return a Response immediately if the Request is not valid or to the kernel.response event and change the response content.
See the full list of default KernelEvents.
By only using these (there are many others related to other components), you can create a plugin sytem that is more capable, more testable and more robust than that of the Wordpress "platform".
Of course, you can easily create and dispatch your own events that will suit your business logic (for example create events like post.created or comment.created) for a blog application.
Now, for the sake of an example, here is how you will configure a "plugin" that will do something with the generated Response and then will fire another event (that can be used by another plugin)
namespace Vendor;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class ResponseAlter implements EventSubscriberInterface
{
private $dispatcher;
public function __construct(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function doSomethingWithResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
/**
* let other plugins hook to the provide.footer event and
* add the result to the response
*/
$footer = new ProvideFooterEvent();
$this->dispatcher->dispatch('provide.footer', $footer);
$this->addFooterProvidedByPluginToResponse($response, $footer->getProvidedFooter());
$event->setResponse($response);
}
static function getSubscribedEvents()
{
return array(
'kernel.response' => 'doSomethingWithResponse'
);
}
}
Now you will simply have to tag your service as a service subscriber and you're done. You've just plugged in the HttpKernel component:
services:
my_subscriber:
class: Vendor\ResponseAlter
arguments: ['#event_dispatcher']
tags:
- {name: kernel.event_subscriber}
I have a slugify method in an Twig Extension which i would like to use in some cases in a controller, f.e with redirects.
Is there an easy way for this?
How could i access functions from Twig Extensions in the controller?
Or do i have to make the slugify method somewere as a helper in order to use it in the code and in twig?
Access function / logic from twig and a controller
I think there are two solutions for this, both should use the Twig_Function_Method class.
1
The first solution gilden already posted, is to encapsulate the logic into a service and make a wrapper for the Twig Extension.
2
Another solution is to use only the Twig Extension. The Twig Extensino is already a service, you have to define it as service with the special <tag name="twig.extension" />.
But it's also a service, which instance you can grab by the service container. And it's also possible to inject other services:
So you have your Twig Extension / Service:
class MyTwigExtension extends \Twig_Extension
{
private $anotherService;
public function __construct(SecurityService $anotherService= null)
{
$this->anotherService = $anotherService;
}
public function foo($param)
{
// do something
$this->anotherService->bar($param);
}
public function getFunctions()
{
// function names in twig => function name in this calss
return array(
'foo' => new \Twig_Function_Method($this, 'foo'),
);
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'my_extension';
}
}
The services.xml looks like this
<service id="acme.my_extension" class="Acme\CoreBundle\Twig\Extension\MyTwigExtension">
<tag name="twig.extension" />
<argument type="service" id="another.service"></argument>
</service>
To acccess to the service from your controller you only have to use this:
$this->container->get('acme.my_extension')
Notice The only difference to a normal service is, that the twig extension is not lazy loaded (http://symfony.com/doc/current/cookbook/templating/twig_extension.html#register-an-extension-as-a-service)
I would advise creating a general service and injecting it to the Twig extension. The extension would act just as a wrapper to the service.
namespace Acme\Bundle\DemoBundle\...;
class MyService
{
public function myFunc($foo, $bar)
{
// some code...
}
// additional methods...
}
EDIT - as mentioned by Squazic, the first argument must implement Twig_ExtensionInterface. An inelegant solution would be to add methods to MyTwigExtension, that in turn call out respective methods in the service.
namespace Acme\Bundle\DemoBundle\Twig\Extension;
class MyTwigExtension extends \Twig_Extension
{
protected $service;
public function __construct(MyService $service)
{
$this->service = $service;
}
public function getFunctions()
{
return array(
'myTwigFunction' => new \Twig_Function_Method($this, 'myFunc'),
'mySecondFunc' => new \Twig_Function_Method($this, 'mySecondFunc'),
);
}
public function myFunc($foo, $bar)
{
return $this->service->myFunc($foo, $bar);
}
// etc...
}
Or another way is to get it via twig... (this is on Symfony 2.7)
$twigExt = $this->container->get('twig')->getExtension(TwigExtensionClassName::class);
So if your Twig extension class is called 'MyFabulousTwigExt', then you'd call
$twigExt = $this->container->get('twig')->getExtension(MyFabulousTwigExt::class);
This worked for me when the above didn't (our extension wasn't also a service)
I've found this to be the best way of calling the extension directly (tested in Symfony 4.4):
use Twig\Environment;
private Environment $twig;
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
public function foo()
{
$extensionOutput = $this->twig
->getExtension(YourExtension::class)
->yourExtensionFunction(
$this->twig,
$value
);
...
}
Useful if you don't want to (or can't) break the logic out of the Twig extension.
I've got an Entity that I want to associate with the users session.
I created a service so that I could reach this info from where ever.
in the service i save the entities id in an session variable
and in the getEntity() method i get the session variable and with doctrine find the entity and return it.
this way to the template i should be able to call {{ myservice.myentity.myproperty }}
The problem is that myservice is used all over the place, and I don't want to have to get it in every since Action and append it to the view array.
Is there a way to make a service accessible from all views like the session {{ app.session }} ?
The solution
By creating a custom service i can get to that from where ever by using
$this->get('myservice');
this is all done by http://symfony.com/doc/current/book/service_container.html
But I'll give you some demo code.
The Service
This first snippet is the actual service
<?php
namespace MyBundle\AppBundle\Extensions;
use Symfony\Component\HttpFoundation\Session;
use Doctrine\ORM\EntityManager;
use MyBundle\AppBundle\Entity\Patient;
class AppState
{
protected $session;
protected $em;
function __construct(Session $session, EntityManager $em)
{
$this->session = $session;
$this->em = $em;
}
public function getPatient()
{
$id = $this->session->get('patient');
return isset($id) ? $em->getRepository('MyBundleStoreBundle:Patient')->find($id) : null;
}
}
Register it in you config.yml with something like this
services:
appstate:
class: MyBundle\AppBundle\Extensions\AppState
arguments: [#session, #doctrine.orm.entity_manager]
Now we can as I said before, get the service in our controllers with
$this->get('myservice');
But since this is a global service I didn't want to have to do this in every controller and every action
public function myAction()
{
$appstate = $this->get('appstate');
return array(
'appstate' => $appstate
);
}
so now we go create a Twig_Extension
Twig Extension
<?php
namespace MyBundle\AppBundle\Extensions;
use MyBundle\AppBundle\Extensions\AppState;
class AppStateExtension extends \Twig_Extension
{
protected $appState;
function __construct(AppState $appState) {
$this->appState = $appState;
}
public function getGlobals() {
return array(
'appstate' => $this->appState
);
}
public function getName()
{
return 'appstate';
}
}
By using dependency injection we now have the AppState Service that we created in the twig extension named appstate
Now we register that with the symfony (again inside the services section inside the config-file)
twig.extension.appstate:
class: MyBundle\AppBundle\Extensions\AppStateExtension
arguments: [#appstate]
tags:
- { name: twig.extension }
The important part being the "tags", since this is what symfony uses to find all twig extensions
We are now set to use our appstate in our twig templates by the variable name
{{ appstate.patient }}
or
{{ appstate.getPatient() }}
Awesome!
Maybe you can try this in your action ? : $this->container->get('templating')->addGlobal($name, $value)