Phalcon / Volt dynamically build/render common template areas (partials) - php

I'm starting a project using Phalcon framework with Volt as a template engine. I have some experience with Symfony/Twig.
I read documentation and tried searching all over the internet but can't find a satisfying way to accomplish what I want (I find ugly the solution described here: How do I create common template with header and footer for phalcon projects with Volt Engine; it's not using Volt per se for the navigation.)
So the story is pretty easy: my base template consists of 4 parts: navigation, header, content and footer. I use partials to include the common areas in the base template like the navigation, header and the footer, works fine with "static data".
Now the question is: how do I get to dynamically generate the navigation menu with items coming from the database? The template will have common areas that have to come from DB also in the header, footer and a sidebar. Having to fetch that in all Controller actions sounds like overkill and not very DRY (maybe do it on the init part? but will have to be done in every controller. Maybe in an abstract controller, I dunno.)
What is the best way to accomplish this in Phalcon/Volt? In Symfony/Twig you can call from the view a controller action, so you can have like a LayoutController that renders partials from a page.
Thanks!

Here are few variants:
1) Your controllers can extend a BaseController and in it's initialize() method you can assign those variables to the view.
class BaseController extends \Phalcon\Mvc\Controller
{
public function initialize()
{
// Common Variables
$this->view->assetsSuffix = $this->config->debug ? '' : '.min';
}
2) Create a custom Volt function which loads the data.
// In your Volt service:
$compiler->addFunction('getMenu', function($resolvedArgs, $exprArgs){
return 'Helpers\CommonFunctions::getMenu(' . $resolvedArgs . ')';
})
// Helper file and function
public static function getMenu()
{
return \Models\Menu::find();
}
// Volt usage
{% set menuItems = getMenu() %}
{% for item in menuItems %}
{% endfor %}
3) Use the models to query the DB directly from the template. However this is not yet supported with Volt (not sure if it is added in latest version, have to confirm).
<?php $menuItems = \Models\Menu::find(); ?>
{% for item in menuItems %}
{% endfor %}
4) Ajax/Javascript, but this is really dependant on your application. Something like Angular approach, but I will not get into details with this varaiant.

Related

Manage different base templates from database in Symfony

I'm trying to manage multiple templates in Symfony. The active Template comes from a database and I have a controller which gives the correct path entry.
My Problem is to tell symfony about this path. I have searched the Twig render method in multiple classes but changes are not successfully.
My TemplateController.php
public function loadtpl() {
$repo = $this->getDoctrine()->getRepository(Templates::class);
$found = $repo->findByActive(1);
$tpl = $found[0]->getPath();
return $tpl;
}
This gives me the template path but I find no way to tell symfony about it.
UPDATE:
What I have - 2 different layouts located in templates/layout1 and template/layout2
What I get - my TemplateController (above) returns the active layout path (layout1/)
Now I can edit my twig.yaml to say twig my template path to ../templates/layout1 so I can use render(mysite.html.twig); which is located in layout1 (and layout2) but its not what I want.
What I want - I want to extend the base template path dynamically with my layout paths so that I can use the method render (mysite.html.twig) without editing twig.yaml manually.
What I need - I need the twig or symfony class to edit the main render() method but I can't find the right File. OR: Anyone have an idea which is better to solve this problem.
The render funtion expects the following params:
/**
* Renders a view.
*
* #param string $view The view name
* #param array $parameters An array of parameters to pass to the view
* #param Response $response A response instance
*
* #return Response A Response instance
*
* #final since version 3.4
*/
So as long as you pass the correct view name: #BUNDLE_NAME:RESOURCE_NAME_FOLDER:TWIG_FILE then you should be good to go.
UPDATE:
I think I understand what you need after you provided more details. As no example was provided I will try to work with realistic scenario.
Imagine you have 2 templates, blue and red, they have structure and color differences but the content is mostly the same. I would then have 2 directories under my template folder.
/app
/Resources
/views
/blue
base_template.html.twig
/red
base_template.html.twig (they can have different names it doesn't really matter)
each one would define a base_template where you set your imports and other specifics of the template.
Now on you controller you get the base_template value from the DB as your function already does.
Then in your controller, you can use that value and pass it to your template that will extend it dynamically.
public function indexAction()
{
return $this->render('AppBundle:Home:index.html.twig',["base_template"=>loadTpl()]);
}
Finally in your twig file you would extend the template like this:
{% extends base_template %}
{% block content %}
<div class="container">
My content
</div>
{% endblock %}
Here is a link to : Twig Dynamic Inheritance

How to send repeating data from all symfony's controller routes to twig?

Here is the use case:
I have a simple navbar with a simple dropdown menu.
I have a list of cities in the Database with the name of the cities. I want to get all the cities and add them to the dropdown menu.
Simple way is to send additional data, like array of cities to twig. But i have a lot of routes and i don't think that this is good to repeat fetching every time. This is not good, right?
public function testpageAction(){
$em = $this->getDoctrine()->getEntityManager();
$cities = $em->getRepository('AppBundle:City')->findAll();
return $this->render('appviews/testpage.html.twig',array('cities'=>$cities));
}
There is a method to inject variable into all templates, but this is static data, so it is not the solution.
There is a plan in my head to make a method that will be run before controller methods and it will send the data to those methods. But it does not seem good and I believe there is a better way to solve it.
You can have a CityController or a BaseController with method getAllCities, and embed this controller in your base template.
From Symfony docs :
In some cases, you need to do more than include a simple template. Suppose you have a sidebar in your layout that contains the three most recent articles. Retrieving the three articles may include querying the database or performing other heavy logic that can't be done from within a template.
The solution is to simply embed the result of an entire controller from your template.
Something like this:
class BaseController extends Controller
{
public function getAllCities()
{
// make a database call or other logic
// to get all cities
$cities = ...;
return $this->render(
'cities.html.twig',
array('cities' => $cities)
);
}
}
Your cities template:
{# app/Resources/views/cities.html.twig #}
{% for city in cities %}
{{ city}}
{% endfor %}
And in your base template:
{# app/Resources/views/base.html.twig #}
<div id="sidebar">
{{ render(controller(
'AppBundle:Base:getAllCities'
)) }}
</div>

Sharing data between embedded controllers in Symfony2

I'm new to Symfony2 and after several tutorials decided to migrate on of my projects to this framework.
In my project I've got few modules that got different templates but share the same data. For example, menu: menu items are lifted from database and used by standart website menu and also by footer menu. I've read that the best practice is to create controller for such task, and embed it straight into main layout, like this:
//Controller
class PageComponentController extends Controller
{
public function menuAction()
{
//get menu items from database...
$menu_items = ...
return $this->render('MyProjectBundle:PageComponent:menu.html.twig', [
'menu_items' => $menu_items
]);
}
}
//Template
{% block body %}
{% render controller('MyProjectBundle:PageComponent:menu', { 'current_page': current_page }) %}
{% endblock %}
But then I need to pass those $menu_items in footer as well, and render footer from the footerAction() in PageComponentController.
I know that there are ways to do it, but what is the best way to share such mutual data between different embedded controllers in Symfony2?
UPDATE (solution based on oligan's answer):
Service container scope is both accessible from main controller (responsible for page rendering) and from embedded controllers. So it would be pretty clean and dry to write service class with getMenu() method that fetches data from DB, and outputMenu() method that returns existing data. Then, service data is set from main controller and could be retrieved from service container in embedded controllers.
I think the wisest is to use a service which would retrieve all the data you want http://symfony.com/doc/current/book/service_container.html (it's more or less like a controller but accessible everywhere )
To give you an idea what it is
class MenuService
{
public function getMyMenu()
{
... your code to get the menu
}
}
Then declare it in services.yml in your bundle
services:
getMenuService:
class: ..\...\...\MenuService
And then just use it in any controller by doing so
$menu = $this->container->get('getMenuService');
if you have to use template heritage then you can still access to an object in the parent template by doing
{% extends "...Bundle..:myMenu.html.twig" %}
{% set menu = myMenuTwigvar %}
and then myMenu for example menu.button1

What's the most convenient way to globally set 2 different form themes for app back-end and front-end

So, theming a form in Symfony2 is easy. You create a custom theme file and you add it to your config.yml file to load it. Done.
However, I have 2 different form themes. One for the front-end of the application and one for the back-end of the application.
I went through the documentation (http://symfony.com/doc/2.3/cookbook/form/form_customization.html) but couldn't find a good and easy way to do this.
When I add the theme to the config.yml file, I have the same theme in both front-end and back-end. I could also include the form within each view like this
{% form_theme form 'form_table_layout.html.twig' %}
However, that means I have to do it within each view.
Is there somehow a way to create a separate config file for front-end and back-end? Can I somehow indicate in the base template file which form theme should be used?
Anything else I could do?
If you use the Symfony2 default directory structure, i.e. you have a single kernel for both the frontend and the backend, you can only (as you mentioned) either set the form theme in each template, or use the same template application-wide by setting it in the config.yml file.
The alternative solution you mentioned, that is creating two base templates, each one setting a "global" form_theme tag would theoretically work. Create a base front-end.html.twig template for all your frontend pages with the following tag:
{% form_theme form 'form-front-end.html.twig' %}
That would work, but you would be forced to have a form variable in each inherited template. You would also not be able to set the theme to multiple forms in the same page.
You could improve the solution by checking if the form variable is defined before styling it:
{% if form is defined %}
{% form_theme form 'form-front-end.html.twig' %}
{% endif %}
or even better, if you want to be able to passing multiple form to the same template, you could do it by using a forms array:
{% if forms is defined %}
{% for form in forms %}
{% form_theme form 'form-front-end.html.twig' %}
{% endfor %}
{% endif %}
The good thing is that this would not throw any exceptions, even if you don't pass the variable at all, but you have to remember to put any forms to be rendered into the forms array.
Obviously, you would do the same thing for the back-end base template.
There may be better solutions, but in the meanwhile I hope this helps!
I also faced this problem some times ago, found this post, and followed the way as Andrea Sprega suggested. Recently I came out of a way which can save some repetitive typings.
Warning: This is somewhat "hackish", is probably not the best way, and is not guarantee to work in next framework release. You'll see why when you read below.
Goal: We want to have different default form templates depending on the application "environment" (the most common examples would be "frontend" and "backend). But fundamentally Symfony isn't aware of such "environment". Although it is possible to split the project into different environments and load different configurations, this is definitely an overkill if what we want is just having different form templates.
I think the best way would be an ability to set/override the default form themes in the base template. So we can have form theme A in frontend base template, and form theme B in backend base template. This sounds the best solution to me because as form theme is a "view" stuff, it makes perfect sense to have it changed in the view. However, the problem seems to be that, when the twig (template) code is executed, the form view is already initialized. So it would be too late to change the default form theme there. (I could be wrong here, because I didn't dig deep enough to have 100% confidence)
So I decided to do it another way. It works like this:
First, I made an assumption that all frontend controllers will extend the same base class (e.g. "BaseFrontendController"). Similarly, all backend controllers will extend the same BaseBackendController class. Here's how we distinguish a frontend and backend environment. This is actually the case in my project.
The default twig templates will be added to these base controller classes. It can be done via a method, or an annotation. In this post I'll use a public method.
Before the controller is executed, overwrite the defined default twig templates.
When the form view is initialized, it will use the default twig template defined in the controller.
Here's how it's done:
Firstly, the default form themes defined in your config.yml will be passed to the constructor of \Symfony\Component\Form\AbstractRendererEngine, and is assigned to the local instance $defaultThemes. This field is protected so that it can be used by its derived classes, but there is no setter to change its value.
So, we need to roll our own Symfony\Bridge\Twig\Form\TwigRendererEngine:
namespace AppBundle\Form\Twig;
use Symfony\Bridge\Twig\Form\TwigRendererEngine as BaseTwigRendererEngine;
class TwigRendererEngine extends BaseTwigRendererEngine
{
/**
* #param array $defaultThemes
*/
public function addDefaultThemes($defaultThemes)
{
$this->defaultThemes = array_merge($this->defaultThemes, $defaultThemes);
}
}
This custom renderer engine is plain simple - it just add a new method to append the default themes to the existing one. And this is why I say it's "hackish" - it will no longer work when the internal is changed.
Secondly, define an interface TwigTemplateProvider which is going to be implemented by the base frontend/backend controller classes:
namespace AppBundle\Form\Twig;
interface TwigTemplateProvider
{
/**
* #return array|string The form template path
*/
public function getDefaultFormTwigTemplates();
}
Thirdly, we need a listener which will run when the controller is executed.
<?php
namespace AppBundle\EventListener;
use AppBundle\Form\Twig\TwigRendererEngine;
use AppBundle\Form\Twig\TwigTemplateProvider;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class PerControllerFormTemplateListener
{
/**
* #var \Twig_Environment
*/
private $twig;
public function __construct(\Twig_Environment $twig)
{
$this->twig = $twig;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof TwigTemplateProvider) {
/** #var \Symfony\Bridge\Twig\Extension\FormExtension $formExtension */
$formExtension = $this->twig->getExtension('form');
$engine = $formExtension->renderer->getEngine();
if ($engine instanceof TwigRendererEngine) {
$templates = (array)$controller[0]->getDefaultFormTwigTemplates();
$engine->addDefaultThemes($templates);
}
}
}
}
The listener will get the form template name (in string or array) supplied by controllers implementing TwigTemplateProvider interface. Then it will add it to the default theme list and pass it to the form renderer engine.
Now, wire them together by adding the followings to your services.yml:
parameters:
twig.form.engine.class: AppBundle\Form\Twig\TwigRendererEngine
services:
app.form.per_controller_template_listener:
class: AppBundle\EventListener\PerControllerFormTemplateListener
arguments: ["#twig"]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Here we set the %twig.form.engine.class% parameter to our own implementation, and added our event listener to the stack.
Using it is very simple. For example, in your base frontend controller, implement TwigTemplateProvider and add the following method:
public function getDefaultFormTwigTemplates()
{
return 'frontend/form_layout.html.twig';
}
Then this layout will be added to the form template stack when your front controller is executed.

How to load a view from a view?

I used to work with CodeIgniter. Now I am starting to learn Symfony2. I was just wondering, in CodeIgniter I could load a view from another view. Like, I could load menu.php from index.php. This is how I used to do it -
//in index.php
<?php $this->load->view('menu.php'); ?>
Is it possible to do the same thing in Symfony2 and Twig?
There are a few different ways you can do it depending on what you're trying to accomplish.
If you want to render the response of a controller you can do this in your twig template.
{{ render(controller('AcmeArticleBundle:Article:recentArticles', {
'max': 3
})) }}
In the above example, the parameter passed max would be passed as an argument to your controller. Then the controller would be responsible for returning a response that will be inserted into the view where it was called from.
You can also use include to render just the twig template as an embedded view:
{% for article in articles %}
{{ include(
'AcmeArticleBundle:Article:articleDetails.html.twig',
{ 'article': article }
) }}
{% endfor %}
In the above example article would be passed into the context of the twig template articleDetails.html.twig but not to any controller. So this method is ideal for repetitious front-end code that is used in many places such as templates for tables, lists, sidebars, etc.
http://symfony.com/doc/current/book/templating.html#including-other-templates
http://twig.sensiolabs.org/doc/functions/include.html

Categories