How to share variables to all views (including behavior) in twig? - symfony

I have this controller action:
public function index(Request $request)
{
$start = $request->get('start', 0);
$limit = $request->get('limit', 10);
$articles = $this->articleRepository->all($start, $limit);
$navigation = $this->menu->build()->render(new RenderStrategyBootstrap4());
return $this->render('article/index.html.twig', [
'articles' => $articles,
'navigation'=>$navigation
]);
}
I build a menu with:
$navigation = $this->menu->build()->render(new RenderStrategyBootstrap4());
Now this is high level behavior and I do not want to render this for every action there is. Is there a way in Symfony to move this behavior to a sort of view composer (like in Laravel?) and then share the variable with the view?
Or is there no way and do I need to create a base controller?

You could create a Custom Twig Extension as described here: https://symfony.com/doc/current/templating/twig_extension.html
There you can register a custom Twig Function like this:
public function getFunctions()
{
return array('renderNavigation' => new TwigFunction(
'renderNavigation',
array($this, 'renderNavigation'),
array('needs_environment' => true, 'is_safe' => array('html'))
);
}
public function renderNavigation(Environment $twig)
{
/* ... */
return $twig->render(/* ... */);
}
Then you can use the function in every template like {{ renderNavigation() }}
Since the Twig Extension itself is a service you can inject whatever service else you need (like RequestStack, EntityManager and so on) and even cache expensive operations within the extension if you need to function to be run more than once.

Related

How to automatically add a property on every Entities

It's been days that I'm trying to achieve the following :
Add a deletedAt property on every entities by default without having to use a trait, or adding inheritance (or mapped superclasses) to all of my already created entities.
The goal here is to also make it work with doctrine migration script so that it would automatically add the associated column in every tables.
Tried to subscribe to the Doctrine\ORM\Events::loadClassMetadata event this way
class ClassMetadataEventSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata
];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args) {
$metadata = $args->getClassMetadata();
$metadata->mapField([
'fieldName' => 'deletedAt',
'type' => 'datetime',
'nullable' => true,
'columnName' => 'deleted_at'
]);
}
}
But I get the ReflectionProperty::_construct() Exception
App\Entity\Etudiant::$deletedAt does not exist
Exception trace:
at /srv/api/vendor/doctrine/persistence/lib/Doctrine/Persistence/Mapping/RuntimeReflectionService.php:90
ReflectionProperty->__construct()
Doctrine\Persistence\Mapping\RuntimeReflectionService->getAccessibleProperty()
Doctrine\ORM\Mapping\ClassMetadataInfo->wakeupReflection()
Doctrine\ORM\Mapping\ClassMetadataFactory->wakeupReflection()
Doctrine\Persistence\Mapping\AbstractClassMetadataFactory->loadMetadata()
Doctrine\Persistence\Mapping\AbstractClassMetadataFactory->getMetadataFor()
Doctrine\Persistence\Mapping\AbstractClassMetadataFactory->getAllMetadata()
Doctrine\Migrations\Provider\OrmSchemaProvider->createSchema()
Doctrine\Migrations\Generator\DiffGenerator->createToSchema()
Doctrine\Migrations\Generator\DiffGenerator->generate()
Doctrine\Migrations\Tools\Console\Command\DiffCommand->execute()
Also, the deletedAt property is here to enable soft deletion through gedmo/doctrine-extensions

How can I use something like the onAfterPublish() hook on a versioned dataobject in SilverStripe

I have a simple versioned dataobject in SilverStripe. I'm trying to hook into the publication action and send out an email whenever the dataobject is published.
I don't think the onAfterPublish() method is available on dataobjects (only pages), so I'm looking to either mimic that or get enough logic working in the onAfterWrite() function.
Here's my code at the moment:
static $has_written = false; // Hack so it only fires once on write()
public function onAfterWrite()
{
parent::onAfterWrite();
if (!self::$has_written) {
$stage = $this->getSourceQueryParam("Versioned.stage");
if ($stage === 'Live') {
$email = new Email();
...
$email->send();
}
}
self::$has_written = true;
}
The Versioned class, that is used for versioning DataObjects, does not have an onAfterPublish hook but it does have an onBeforeVersionedPublish hook that could be used to send out emails:
public function onBeforeVersionedPublish($fromStage, $toStage, $createNewVersion = false) {
$email = Email::create();
// ...
$email->send();
}

Symfony StopWatch events not appearing in profiler timeline

I'm trying to get additional timing information into the Symfony Profiler Timeline, but I can't get anything to appear. According to the documentation I've read, it should be as simple as the following example, but this doesn't cause any additional info to appear on the timeline.
Do I need to somehow make the profiler aware of the events I'm starting and stopping?
use Symfony\Component\Stopwatch\Stopwatch;
class DefaultController extends Controller
{
public function testAction()
{
$stopwatch = new Stopwatch();
$stopwatch->start('testController');
usleep(1000000);
$response = new Response(
'<body>Hi</body>',
Response::HTTP_OK,
array('content-type' => 'text/html')
);
$event = $stopwatch->stop('testController');
return $response;
}
}
Symfony's profiler can't scan to code for all stopwatch instances and put that into the timeline. You have to use the preconfigured stopwatch provided by the profiler instead:
public function testAction()
{
$stopwatch = $this->get('debug.stopwatch');
$stopwatch->start('testController');
// ...
$event = $stopwatch->stop('testController');
return $response;
}
However, your controller is already on the timeline...
You should inject the stopwacth as a service in your constructor or a specific function, becasue after Symfony 3.4: Services are private by default.
testAction(\Symfony\Component\Stopwatch\Stopwatch $stopwatch) {
$stopwatch->start('testController');
// ...
$event = $stopwatch->stop('testController');
}

How to inject content to Twig generated content?

I would like to inject content to Twig generated content after everything else has been parsed and done.
Right now I'm using this code below:
public function onResponse(KernelEvent $event)
{
// TODO: find a better way to inject
$event->getResponse()->setContent(
$this->asseticProcessor->inject($event->getResponse()->getContent()));
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::RESPONSE => array('onResponse', -9999),
);
}
However, I feel like it may not be the optimal way to do so. First of all, at least I want to do the injection only when Twig HTML templates are actually rendered (in some cases, the controller can simply return a response without rendering anything, or they can render json and in such case I don't have to manipulate the content)
Late to the party here, but you could use a kernel event listener for this. For example:
services:
response_modifier:
class: Some\Bundle\Listener\ModifyResponseListener
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -127 }
Set the priority very low so that it fires towards the of the response chain. The listener might work like so:
class ModifyResponseListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
// We probably want to ignore sub-requests
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$content = $response->getContent();
$content = preg_replace('/foo/is', 'bar', $content);
$response->setContent($content);
}
}
The above will replace all occurrences of "foo" with "bar" in the final output.
Note that you might also have to check that the response is not a redirection, ajax, stream or whatever before modifying the content.

Dynamic validation groups in Symfony2

I need to implement form validation depending on submitted data. While data object's invoice property is true then validation_groups array should contain not only 'add' validation but also 'company'.
I've found "Groups based on Submitted Data" chapter in Symfony Docs https://github.com/symfony/symfony-docs/blob/master/book/forms.rst.
The problem is that :
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Strict\PublicBundle\Entity\Booking',
'validation_groups' => function(FormInterface $form)
{
return array('booking');
},
);
}
throws this error:
Warning: Illegal offset type in /var/www/vendor/symfony/src/Symfony/Component/Validator/GraphWalker.php line 101
500 Internal Server Error - ErrorException
Any ideas what can be wrong?
According to this pull request using callbacks for validation_groups will be posible in Symfony 2.1 (not yet released, currently master branch).
Are you sure you are using master branch? If you are using current stable (2.0.x), it has no support for Groups based on Submitted Data, you have to use arrays only. See proper documentation on http://symfony.com/doc/current/book/forms.html#book-forms-validation-groups.
I've got an alternative: If you're able to determine the condition prior to binding the form, you can simply override the default list of validation groups when you create the form.
In my case I've got an order object in session that gets updated across multiple form pages.
Order can be "Delivery" or "Pickup" and if delivery is selected on a previous screen I need to validate address details on this screen:
if ($order->getOrderType() == "Delivery")
{
$validationGroups = array('step3', 'delivery');
}
else
{
$validationGroups = array('step3');
}
$formType = new Form\Order3Type();
$form = $this->createForm($formType, $order, array("validation_groups" => $validationGroups));
$form->bindRequest($request);
If your condition is in the form and not already in session, you could always just pull the value straight from the request object.
// MyFormType.php
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'Strict\PublicBundle\Entity\Booking',
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
$groups = ['booking'];
if ($data->invoice) {
$groups[] = 'company';
}
return $groups;
},
]);
}

Resources