Dynamic routing & templates in symfony - symfony

Recently while building my CMS in Symfony, I've run into a problem. I have 2 controllers, publicationcontroller and contactcontroller. In both controllers, an instance of the entity Page is loaded whenever a corresponding slug matches. Below is the code:
class ContactController extends AbstractController
{
/**
* #Route("/{slug}", name="contact")
*/
public function index(PageRetriever $pageRetriever, $slug)
{
$page = $pageRetriever->getPage($slug);
return $this->render('contact/index.html.twig', [
'page' => $page
]);
}
}
class PublicationController extends AbstractController
{
/**
* #Route("/{slug}", name="publications")
*/
public function index(PageRetriever $pageRetriever, $slug)
{
$page = $pageRetriever->getPage($slug);
return $this->render('publication/index.html.twig', [
'page' => $page
]);
}
}
My problem is that both the content of publication and contact are loaded in the same template, depending on which controller is initialized first.
Does anyone here have an idea or some tips on how to load the proper template, depending on which slug is called?
Any hope is greatly appreciated

There is no way Symfony could know which controller should be called. You have same route for both contact and publication controller.
There is 2 possible solutions which I can think of.
1. Use different routes
#Route("/publication/{slug}", name="publications")
#Route("/contact/{slug}", name="contact")
2. Use one controller but write your own logic to choose template
$page = $pageRetriever->getPage($slug);
if ($page->getType() === 'publication') {
return $this->render('publication/index.html.twig', [
'page' => $page
]);
}
return $this->render('contact/index.html.twig', [
'page' => $page
]);

Related

Drupal: "Error: Class not found" when calling a function from a controller within submitForm()

I'm trying to do some stuff while submitting a form in a custom module. Some of that is done by calling a function from a controller. That's when i get:
Error: Class 'Drupal\ice_cream\Controller\OrderController' not found in Drupal\ice_cream\Form\OrderForm->submitForm() (line 77 of modules\custom\ice_cream\src\Form\OrderForm.php).
As far as I can tell the namespaces aren't wrong? Or is that not related to this error?
This is how my OrderForm.php and submitForm() looks like:
<?php
namespace Drupal\ice_cream\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ice_cream\Controller\OrderController;
/**
* Implements the order form.
*/
class OrderForm extends FormBase {
... (omitted code for getFormid and buildForm)
public function submitForm(array &$form, FormStateInterface $form_state) {
//Check if the order is ice or waffles.
if($form_state->getValue('foodType') == 'ice'){
//Save order to the DB.
OrderController::saveOrder($form_state->getValue('foodType'), $form_state->getValue('taste'));
... (more code)
}
}
}
This is how the controller looks like:
<?php
namespace Drupal\ice_cream\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Database;
/**
* Order controller for interacting (insert, select,...) with the ice cream table in the DB.
*/
class OrderController extends ControllerBase {
/**
* Saves an order for either Ice or Waffles with their options (tast or toppings).
*/
public function saveOrder($foodType, $options) {
$connection = Database::getConnection();
//Check if ice or waffles (to only insert the right field with $options).
if($foodType == "ice"){
$result = $connection->insert('ice_cream')
->fields([
'foodType' => $foodType,
'taste' => $options,
'toppings' => "",
])
->execute();
return true;
}elseif($foodType == "waffles"){
$result = $connection->insert('ice_cream')
->fields([
'foodType' => $foodType,
'taste' => "",
'toppings' => $options,
])
->execute();
return true;
}
}
}
Try using the below code:
$obj= new OrderController;
$obj->saveOrder($form_state->getValue('foodType'), $form_state->getValue('taste'));
Solved:
Just solved it with my mentor. The code was more or less correct, still needed to make my functions static in the OrderController and also I made a stupid error of forgetting the .php extension in my filename when I created it with the 'touch' terminal command...

symfony 4 dynamic route with array

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.

implementing template hooks in symfony2

My project has a core bundle which holds the main templates,
and I have optional features implemented as bundles.
Different clients get copies of the project with different bundles(=features) activated.
One thing that the feature bundles need to be able to do is add things to the main templates (in the core bundle).
Obviously I don't want the templates in the core bundle to "know" about the optional bundles, so I want to have "hooks" in the templates that other bundles can pour content into.
I have one solution, which seems to work. I would appreciate feedback/suggestions.
I have a twig function which gets a "hook" name. I place that function at different locations in my template.
That function raises an event (through the event dispatcher) that collects html snippets from various event listeners (the feature bundles are responsible for those listeners), joins those snippets and returns them.
code:
twig function:
new \Twig_SimpleFunction('template_hook', array( $this, 'templateHookFunction' ), array('is_safe' => array('html'))),
public function templateHookFunction($hookName)
{
$hookData = $this->eventDispatcher->dispatch(
TemplateHookEvent::TEMPLATE_HOOK,
new TemplateHookEvent($hookName)
)->getData();
return $this->hookDataRenderer->render($hookData);
}
the event:
<?php
namespace CRM\TwigBundle\Event;
use CRM\TwigBundle\Hook\HookData;
use Symfony\Component\EventDispatcher\Event;
class TemplateHookEvent extends Event
{
const TEMPLATE_HOOK = 'crm_twig.template_hook';
private $hookName;
/** #var HookData $data */
private $data;
function __construct($hookName)
{
$this->hookName = $hookName;
$this->data = new HookData();
}
/**
* #return mixed
*/
public function getHookName()
{
return $this->hookName;
}
/**
* #return HookData
*/
public function getData()
{
return $this->data;
}
/**
* #param HookData $data
*/
public function setData(HookData $data)
{
$this->data = $data;
}
public function addData($label, $html, $overwrite = true)
{
if ($overwrite || !isset($this->data[$label])) {
$this->data[$label] = $html;
} else {
$this->data[$label] .= $html;
}
}
}
an example for a listener:
<?php
namespace CRM\ComputersBundle\EventListener;
use CRM\TwigBundle\Event\TemplateHookEvent;
class TemplateHookListener {
public function onTemplateHook(TemplateHookEvent $event)
{
$event->addData('computersBundle', '<li>CAKE</li>');
}
}
HookData is just a wrapper for an array (for now), each listener can push to it its own snippet.
and the HookDataRenderer just joins(implode) the array, and returns the result.
I don't see the point in your architecture. Why do you want such a hook system ? Why not using, for example, controller embedding in Twig as described here ?

How to create event listener that inject the view data in symfony2?

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 }

Best way to develeop plugin compatible application. Dependency injection?

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}

Resources