Get data in twig function - symfony

Is it bad practice to get data from db in twig function or I should pass it to view in controller?
My function is some kind of interface widget that is used on all pages of site admin section. Then on data change I will have to make changes in all actions. But when I get data directly in extension class our teamlead tells that it's bad MVC.

It would be best if you pass it to a view from a controller.

Your team leader is right. What you can do is create an action specific to render that widget. I.e create a custom widget, let's say you want to show the number of current active users:
class WidgetController extends Controller
{
public function usersCountWidgetAction()
{
return $this->render('widget/usersCount.html.twig', array(
"usersCount" => $this->getUsersCount();
));
}
public function getUsersCount()
{
// call the manager and get the result
}
}
Now in all your other twigs you can use
{{ render(controller('AppBundle:Widget:usersCountWidget')) }}

Related

Silverstripe 4 - Adding a FormAction via getCMSFields

Goal:
I have a DataObject called "Event". This is in a managed_model for "EventsAdmin" (extending ModelAdmin). When editing an Event, I want a tab on the record called "Moderation" that has a few fields and two buttons: "Approve" and "Reject". These two buttons call an action each that performs relevant actions.
Event extends DataObject
public function getCMSFields() {
$fields = parent::getCMSFields();
$eventStatus = $fields->dataFieldByName("EventStatus")
->setTitle('Current Status')
->setDisabled(true);
$approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-success font-icon-check-mark-circle');
$rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-danger font-icon-cancel-circled');
$fields->addFieldsToTab('Root.Moderation', [
$eventStatus,
$approveButton,
$rejectButton
]);
return $fields;
}
This displays the buttons just fine. But they don't do anything. So I am trying to work out how they can plug into action methods doApproveEvent and doRejectEvent (And where they should go)
I did find docs that led me to adding the buttons to the action bar at the bottom of the CMS page via updateFormActions(). But this isn't what I want as the other fields I am adding above the buttons are part of the Approve/Reject process. Here is the code for this method. This works fine barring the buttons are not in a logical place for the process I'm trying to create.
class CMSActionButtonExtension extends DataExtension
{
public function updateFormActions(FieldList $actions)
{
$record = $this->owner->getRecord();
if (!$record instanceof Event || !$record->exists()) {
return;
}
$approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-success font-icon-check-mark-circle');
$rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-danger font-icon-cancel-circled');
$actions->push($approveButton);
$actions->push($rejectButton);
}
public function doApproveEvent($data, $form) {
$record = $this->owner->getRecord();
// Approve logic
}
public function doRejectEvent($data, $form) {
$record = $this->owner->getRecord();
// Reject logic
}
}
The above Extension is attached to GridFieldDetailForm_ItemRequest
extension.yml
SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
extensions:
- My\Namespace\CMSActionButtonExtension
Interestingly, if I have both sets of buttons on the page at the same time, the updateFormActions option works while my desired option still doesn't. Despite the buttons being of identical markup and sitting inside the exact same form tag. I assume that has something to do with how Silverstripe loads the main content panel and the DOM.
Any thoughts on achieving this? Anyone seen a button added to the main CMS panel in a module that I could take a look at? I found this post from 5 years ago, but it's for SS3 and the answer doesn't work for me.
Short answer:
you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself
Long Answer:
A bit of background on how SilverStripe does forms:
Generally speaking, forms are always served through Controllers/RequestHandlers (they need to be accessible on some route, usually that's an Action on a Controller that is often named Form, EditForm, ItemEditoForm, ...).
Fields
Inside the CMS you rarely ever have to create your own form, that's done by the CMSs built in Controllers/RequestHandlers for the admin area (GridFieldDetailForm_ItemRequest in this case).
Basically (pseudo code here), what those controllers do is:
public function EditForm() {
$fields = $myCurrentlyEditingDataObject->getCMSFields();
$actions = ...;
$validator = ...;
$this->updateFormActions(&$actions);
$form = new Form('ItemRequestForm', $fields, $actions, $validator);
$this->updateItemEditForm(&$form); // or $this->updateEditForm()
return $form;
}
So, getCMSFields() and in some cases getCMSActions()/getCMSValidator() (not sure if those 2 are still used in SilverStripe 4.x), you can add things to the form, without ever seeing the form object.
Also, the getCMSFields() will always be put into the ``` section of the Form, that's why your button is somewhere in the middle with all the fields and not with the other actions.
Submission
When a form is submitted (eg to /admin/pages/edit/EditForm/265/field/NameOfMyGridField/item/542/ItemEditForm), it will call the action GridFieldDetailForm_ItemRequest->ItemEditForm() which returns the Form object where subsequently FormRequestHandler->httpSubmission() is called. This will then look at the submitted data to figure out what action was clicked (eg $_REQUEST['action_doApproveEvent']) and try to find that action.
The way it tries to find that, is checking if it itself has a method called doApproveEvent, if that fails, it will try Form->getController()->doApproveEvent() or something like that. In the case of a GridField, that controller is GridFieldDetailForm_ItemRequest which means it will try to call GridFieldDetailForm_ItemRequest->doApproveEvent()
So, that means DataObject->getCMSFields() lets you easily add FormFields (and FormActions) into your form body.
But it does not provide a means of adding a method to handle the submission.
That's why, for custom actions you need to modify the Controller (GridFieldDetailForm_ItemRequest in this case).
You are doing this by creating a Extension which you attached to GridFieldDetailForm_ItemRequest.
Any method in your Extension is added to the thing it's attached to, so if you add a method called updateFormActions, it will kind of become GridFieldDetailForm_ItemRequest->updateFormActions().
And if you recall from earlier, the controller will call $this->updateFormActions() during the creation of the form.
Additionally, as I explained earlier, when a FormAction is named doApproveEvent it will look for a GridFieldDetailForm_ItemRequest->doApproveEvent(), which now exists because you added it through that Extension.
So, in summary: you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself
PS: the old post from
bummzack you linked to worked in 3.x, because the Controller in his example that created the form was an instance of LeftAndMain.

Sonata Block - Pass custom arguments

I'm new with Sonata Block Bundle.
I would like to put into my block a map. It uses some JS library. Function of the context, I need to pass different height, width etc... for example.
But I don't know if it fits with my needs.
At first, I wanted to use Sonata Block because my Maps has dependencies with some Services. So this is cool, I can centralise them.
But can I pass some arguments functions the parent who calls my block ?
Thanks for your answer.
Redfog
Okay, if I understood your question, what you want to do, is pass some custom arguments from your template (where you call your block to be precise) to the php class that is executing the block. Let's get started:
Lets add option to pass height attribute:
{% sample render of your block %}
{{ sonata_block_render({'type':'your.block.id'}, {'height': 50}) }}
Now, in your block service (php/class). You have to add this attribute as a default option in your method: setDefaultSettings, like this:
public function setDefaultSettings(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
// your options goes here, and we add our new option right after them
'height' => null // or whatever suits your needs
));
}
Finally, all you have to is access your option from your execute method like this:
public function execute(BlockContextInterface $blockContext, Response $response = null) {
$settings = $blockContext->getSettings();
// now your value can be access from $settings['height'];
}
Let me know if that's what you're looking for.

Share a method with all controllers : Best practice

I'm developing a notification system in symfony2 and I need to get the notifications for every page I'm running.
the trivial solution is to copy the content of the function in every controller and call the function from $this.
How can I make the notification function accessible for every controller? I heard that setting a controller as service is bad practice. what's the best practice then ?
If just using it for output in the template then best approach would be to use a custom TwigFunction and then calling that in a base/layout/extended template like so..
TwigExtension
namespace Acme\NotificationBundle\Twig;
use Acme\NotificationBundle\Provider\NotificationProviderInterface;
class AcmeNotificationExtension extends \Twig_Extension
{
protected $container;
protected $notificationProvider;
public function __construct(
ContainerInterface $container,
NotificationProviderInterface $notificationProvider
)
{
$this->notificationProvider = $notificationProvider;
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction(
'acme_render_notifications',
array($this, 'renderNotifications')
),
);
}
public function renderNotification($template = 'default:template.html.twig')
{
$notifications = $this->notificationsProvider->getCurrentNotifications();
// Or whatever method provides your notifications
return $this->container->get('templating')->render(
$template,
array('notifications' => $notifications)
);
}
public function getName()
{
return 'acme_notification_extension';
}
}
Services
parameters:
acme.twig.notification_extension.class:
Acme\NotificationBundle\Twig\AcmeNotificationExtension
services:
acme.twig.notification_extension:
class: %acme.twig.notification_extension.class%
arguments:
- #service_container
- #acme.provider.notifcation
// Or what ever your notification provider service is named
tags:
- { name: twig.extension }
This way you would be able to call your notifications in any template using acme_render_notifications() (with the default template) or acme_render_notifications('AcmeOtherBundle:Notifications:in_depth.html.twig') (with a different template if needed) and your controller aren't even touched.
If it was put in a parent template in a block like ..
{% block notifications %}
{{ acme_render_notifications() }}
{% endblock notifications %}
..then it would run on every page unless you had overridden the block in your child class.
The way I would do it, and I think it is among the best practices, is setting up a service with the function then just instantiate it in each controller.
No doubt its bad practice,
Many solutions are possible, Here we will discuss on abstract level
A global utility can be used with different scopes (application, session scope) depending upon the requirements
Make this utility accessible to all available controllers

Global values: Define as service or define abstract Controller class?

In my Symfony2 app, I want to globally fetch a value from my database on each template and don't want to call on each Controller. I know I could define that as a service and inject that service into my twig templates (by defining it as a twig global).
Is that the common and recommended way? Or should I rather create an abstract Controller class where I fetch that value in my constructor and then inherit from all my other Controllers?
Note: It is actually not a static value that is the same for all users, but it is a user specific value which is different for each user.
If this variables are used to render the same spot on your page you can render an embedded controller. Like this:
<div id="sidebar">
{{ render(controller('YourBundle:User:stats')) }}
</div>
This will inject whole output of YourBundle/UserController/statsAction to the #sidebar div. Inside this action you can extract all inforamtion that you need.
If you need to use this variables in other way maybe you should look at response event.
Are you familiar with event listeners? http://symfony.com/doc/current/cookbook/service_container/event_listener.html
An event listener can be used to inject twig globals.
class ModelEventListener extends ContainerAware implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
KernelEvents::CONTROLLER => array(
array('doProject', -1300),
),
KernelEvents::VIEW => array(
array('doView', -2100),
),
);
}
public function doProject(FilterControllerEvent $event)
{
$project = $whatever_is_needed_to_find_the_project();
if (!$project) throw new NotFoundHttpException('Project not found ' . $projectSearch);
// Add to request
$event->getRequest()->attributes->set('project',$project);
// Give all twig templates access to project
$twig = $this->container->get('twig');
$twig->addGlobal('project',$project);
}
# services.yml
cerad_core__model__event_listener:
class: '%cerad_core__model__event_listener__class%'
calls:
- [setContainer, ['#service_container']]
tags:
- { name: kernel.event_subscriber }
If it's a user value like you said you can get app.user.XXX on every twig template you need without processing nothing ;)

Symfony2 twig mobile template fallback

I need a simple way to fallback on a default template if no mobile version exists.
With some regular expressions I recognize mobile platforms and want to render a template with the following pattern:
<template_name>.mobile.html.twig
But if this template doesn't exist, I want it to automatically fallback on:
<template_name>.html.twig
which always exists.
I tried nearly all the answers from this post:
Symfony 2 load different template depending on user agent properties
but with no success. Unfortunately there are no version numbers referenced.
At the moment I am trying to copy and modify the default twig loader.
By the way, What I want to achieve with this is the possibility to deploy different templates for mobile devices by just adding a template of the same name and adding a .mobile.
UPDATE:
http://www.99bugs.com/handling-mobile-template-switching-in-symfony2/
This one is also a good approach. It modifies the format property of the request object which affects the automatic template guessing when you don't specify a template in the controller with the render function (or annotation) but just return an array.
Resulting template name:
view/<controller>/<action>.<request format>.<engine>
So you could switch the request format from html to mobile.html based on the device detection.
The downside of this is that every template needs a mobile.html pendant (which then could just include the non-mobile version if not needed).
UPDATE:
Besides using a custom templating provider there is also the possibility to hook into the kernel.view event.
You could create a service to handle it and then use it in the same way that you do the templating service like so..
Create a service with the templating and request service injected into it..
Service (YAML)
acme.templating:
class: Acme\AcmeBundle\Templating\TemplatingProvider
scope: request
arguments:
- #templating
- #request // I assume you use request in your platform decision logic,
// otherwise you don't needs this or the scope part
- 'html'
Class
class TemplatingProvider
{
private $fallback;
private $platform;
... __construct($templating, $request, $fallback) etc
private function setPlatform() ... Your platform decision logic
private function getPlatform()
{
if (null === $this->platform) {
$this->setPlatform();
}
return $this->platform;
}
private function getTemplateName($name, $platform)
{
if ($platform === 'html') {
return $name;
}
$template = explode('.', $name);
$template = array_merge(
array_slice($template, 0, -2),
array($platform),
array_slice($template, -2)
);
return implode('.', $template);
}
public function renderResponse($name, array $parameters = array())
{
$newname = $this->getTemplateName($name, $this->getPlatform());
if ($this->templating->exists($newname)) {
return $this->templating->render($newname);
}
return $this->templating->renderResponse($this->getTemplateName(
$name, $this->fallback));
}
And then you could just call your templating service instead of the current one..
return $this->container->get('acme.templating')
->renderResponse('<template_name>.html.twig', array());
Can't you check if the template exist before ?
if ( $this->get('templating')->exists('<templatename>.html.twig') ) {
// return this->render(yourtemplate)
} else {
// return your default template
}
OR :
You can create a generic method, to insert in your root controller like :
public function renderMobile($templateName, $params)
{
$templateShortName = explode('.html.twig', $templateName)[0];
$mobileName = $templateShortName.'.mobile.html.twig';
if ( $this->get('templating')->exists($mobileName) ) {
return $this->renderView($mobileName, $params);
} else {
return $this->renderView($templateName, $params)
}
}
with this you can do :
return $this->renderMobile('yourtemplate', [yourparams]);
You can easily do this by harnessing the bundle inheritance properties in Symfony2 http://symfony.com/doc/current/cookbook/bundles/inheritance.html
create a bundle which holds your desktop templates (AcmeDemoDesktopBundle)
create a bundle which will hold your mobile templates (AcmeDemoMobileBundle) and mark the parent as AcmeDemoDesktopBundle
Then when you render a template simply call AcmeDemoMobileBundle:: - if the template exists, it'll be rendered otherwise you'll neatly fall back to the desktop version. No extra code, listeners or anything none-obvious required.
The downside of this of course is that you move your templates out of the individual bundles.
The fallback behavior you describe isn't that easy to implement (we found out the hard way..). Good news is we wanted the same setup as you ask for and ended up using the LiipThemeBundle for this purpose. It allows you to have different "themes" based on for example a device. It will do the fallback part for you.
For example:
Rendering a template:
#BundleName/Resources/template.html.twig
Will render and fallback to in order:
app/Resources/themes/phone/BundleName/template.html.twig
app/Resources/BundleName/views/template.html.twig
src/BundleName/Resources/themes/phone/template.html.twig
src/BundleName/Resources/views/template.html.twig
Edit: so with this approach you can have default templates that will always be the final fallback and have a special template for mobile where you need it.

Resources