Grav CMS: Hide modular pages in page selection lists - php

I'm working on a project which uses a lot of modular pages.
So naturally, the dropdown which lists all the pages (e.g. when selecting a parent page when creating a new page) gets very bloated with options you don't really use.
I edited /user/plugins/admin/themes/grav/templates/forms/fields/pages/pages.html.twig to prevent the modular pages from rendering.
Original code (line 12):
{% for page_route, option in pages_list %}
<option {% if page_route == value or (field.multiple and page_route in value) %}selected="selected"{% endif %} value="{{ page_route }}">{{ option|raw }}</option>
{% endfor %}
My code:
{% for page_route, option in pages_list %}
{% if page_route|split('/')|last matches '/^(?!_).*/' %}
<option {% if page_route == value or (field.multiple and page_route in value) %}selected="selected"{% endif %} value="{{ page_route }}">{{ option|raw }}</option>
{% endif %}
{% endfor %}
So this works pretty well and hides all the modular pages in the dropdown. Unfortunately, I fear this may be overwritten with the next admin plugin update.
I'd love to create a plugin which forces the admin plugin to use my templates instead of messing around in the original ones, but I don't know how to do that. Is it even possible?
Thanks!

I found something: https://github.com/OleVik/grav-plugin-adminidenticons
It overrides a partial of the admin plugin.
Step 1: Create a plugin
There are plenty of tutorials how to do that.
I have the following file structure inside my plugin folder:
user/plugins
-- myplugin
---- blueprints.yaml
---- myplugin.php
---- myplugin.yaml
Step 2: Copy files
/user/plugins/admin/themes/grav/templates/forms/fields/pages/pages.html.twig
to /user/plugins/myplugin/admin/themes/grav/templates/forms/fields/pages/pages.html.twig
/user/plugins/admin/themes/grav/templates/partials/blueprints-new.html.twig
to /user/plugins/myplugin/admin/themes/grav/templates/partials/blueprints-new.html.twig
Don't forget to add the snippet I posted above into the copied pages.html.twig. You can leave the blueprints-new.html.twig unedited, but you need the partial inside your plugin folder to get Twig to load the local pages.html.twig.
Step 3: PHP
Edit myplugin.php
<?php
namespace Grav\Plugin;
use Grav\Common\Grav;
use Grav\Common\Plugin;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;
class MyPluginPlugin extends Plugin
{
public static function getSubscribedEvents() {
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
];
}
public function onPluginsInitialized()
{
/** #var Uri $uri */
$uri = $this->grav['uri'];
$route = $this->config->get('plugins.admin.route');
if ($route && preg_match('#' . $route . '#', $uri->path())) {
$this->enable([
'onPageInitialized' => ['onPageInitialized', 0],
'onAdminTwigTemplatePaths' => ['onAdminTwigTemplatePaths', 0]
]);
}
}
/**
* Load custom CSS into backend
*
*/
public function onPageInitialized()
{
$assets = $this->grav['assets'];
$assets->addCss('user/plugins/myplugin/style/custom-backend.css', 1);
}
/**
* Register templates and page
*
* #param RocketTheme\Toolbox\Event\Event $event Event handler
*
* #return array
*/
public function onAdminTwigTemplatePaths($event)
{
$event['paths'] = array_merge(
$event['paths'],
[__DIR__ . '/admin/themes/grav/templates']
);
return $event;
}
}
Bonus: Hide unique page templates
Sometimes, you create page templates which are supposed to be used once on just one page. Just the way Grav works, they will still appear inside the list when creating a new page. Of course, you don't want your editors to get creative with the page templates, so it's best to hide them.
If you're actually also reading the code you're copy-pasting, you'll notice, that there is a function onPageInitialized(), which loads a CSS file into the backend.
Update your files:
user/plugins
-- myplugin
---- style
------ custom-backend.scss
------ custom-backend.css
---- blueprints.yaml
---- myplugin.php
---- myplugin.yaml
Edit custom-backend.scss
// hide unique templates
.selectize-dropdown-content {
.option {
$hide_values: "this-template", "home", "that-template";
#each $current_value in $hide_values {
&[data-value="#{$current_value}"] {
display: none;
}
}
}
}
Set the $hide_values to the page templates you want to hide in the backend. I use CSS, that way you can re-enable them with the browser inspector in case you need them. Of course, don't forget to compile your SCSS.
I know this turned into a fully fledged tutorial, but having the modular pages littering the page list is actually a problem I had on multiple projects now and I never found something online. Unfortunately, I don't really have a way to publish this properly, so I hope someday someone will stumble upon this Q&A and put it on some webblog, where it belongs.
Thanks!

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

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

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.

How to use multiple routes to one controllerAction with Symfony3

I would like to configure multiple URLs to one Action in Controller (internationalization purposes).
According to this answer it was surely possible in symfony2 to:
Make double annotation route.
Use 3rd party Bundle (for example "BeSimple's").
But I'm using Symfony 3.0.3 which prohibits me from doing so until I change the route's name (example):
/**
* #Route("/welcome", name="welcome", defaults={"_locale" = "en"})
* #Route("/bienvenue", name="welcomeFR", defaults={"_locale" = "fr"})
* #Route("/willkommen", name="welcomeDE", defaults={"_locale" = "de"})
*/
But adding additional "FR/DE" chars to routes change their presence and ruins my URL generating logic in template, I'm forced to make on all links:
{# homepage example #}
{% if _locale = 'en' %}
{{ path('welcome') }} {# Routes from set only for "en" #}
{% elseif _locale = 'fr' %}
{{ path('welcomeFR') }} {# "fr" only links #}
{% endif %} {# and so on #}
Anyone found the proper solution for this problem?
AFAIK, this is the preferred way to point multiple routes to an unique controller action. So, your current problem is to regenerate the current path, depending on which route is being used
Maybe you don't have to modify your logic if you use {{ app.request.get('_route') }} to get the name of your current routing. This way, you could use:
{{ path(app.request.get('_route')) }}
UPDATE:
What about create an action per route and forwarding them to the main language action? Maybe it is not the best practice, but could work fine
/**
* #Route("/welcome", name="welcome", defaults={"_locale" = "en"})
*/
public function welcomeAction()
{
/* code here */
}
/**
* #Route("/bienvenue", name="welcomeFR", defaults={"_locale" = "fr"})
*/
public function welcomeFrAction()
{
$response = $this->forward('AppBundle:ControllerName:welcome');
}
/*
* #Route("/willkommen", name="welcomeDE", defaults={"_locale" = "de"})
*/
public function welcomeDeAction()
{
$response = $this->forward('AppBundle:ControllerName:welcome');
}
After wasting 10 hours on finding solution on Symfony3 I've made those assumptions:
None of the route trick with exactly same "name" wouldn't work,
Any kind like double annotation, double "routing.yml" importing, host based matching gives the same effect - only one route is matched, usually the last one. Would be ("/willkommen", name="welcomeDE") in my example.
Both of the i18n bundles does not work on Symfony3 through the versioning constraints,
Composer wouldn't even let us install the bundle.
Locale Listener and Loader (Routing Component) is dying on caching.
My solution to match the "_locale" and pass it to the LocaleLoader in order to load "routing_en.yml/routing_fr.yml" etc. files respectively is cached on the first match by the Symfony and after that, changing the "_locale" does not affect route's mapping.
In conclusion
Symfony3 seems not supporting route "example" with more than one "url" at all. Through the constraints and caching.
After the disappointment I'm considering going back to Symfony 2.8, have no idea what was pointing Symony's masters to block "double annotation" solution and what is their current idea on links internationalization.
Hope someday it will be possible to achieve with S3, as link i18n is quite common SEO practice.
EDIT: Confirmed, working like a charm on 2.8.5 with BeSimple's i18n.

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.

Categories