How to inject content to Twig generated content? - symfony

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.

Related

Symfony 5 dynamic routing resolve

I am migrating legacy project routing (Yii1) to Symfony 5
Right now my config/routing.yaml looks something like this:
- {path: '/login', methods: ['GET'], controller: 'App\Controller\RestController::actionLogin'}
- {path: '/logout', methods: ['GET'], controller: 'App\Controller\RestController::actionLogout'}
# [...]
- {path: '/readme', methods: ['GET'], controller: 'App\Controller\RestController::actionReadme'}
As you can see there is plenty of repetitive url to action conversion.
Is it possible to dynamically resolve controller method depending on some parameter. E.g.
- {path: '/{action<login|logout|...|readme>}', methods: ['GET'], controller: 'App\Controller\RestController::action<action>'}
One option would be to write annotations, but that somehow does not work for me and throws Route.php not found
The controller is determined by a RequestListener, specifically the router RouterListener. This in turn uses UrlMatcher to check the uri against the RouteCollection. You could implement a Matcher that resolves the controller based on the route. All you have to do is return an array with a _controller key.
Take note that this solution won't allow you to generate a url from a route name, since that's a different Interface, but you could wire it together.
// src/Routing/NaiveRequestMatcher
namespace App\Routing;
use App\Controller\RestController;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\RequestContext;
class NaiveRequestMatcher implements UrlMatcherInterface
{
private $matcher;
/**
* #param $matcher The original 'router' service (implements UrlMatcher)
*/
public function __construct($matcher)
{
$this->matcher = $matcher;
}
public function setContext(RequestContext $context)
{
return $this->matcher->setContext($context);
}
public function getContext()
{
return $this->matcher->getContext();
}
public function match(string $pathinfo)
{
try {
// Check if the route is already defined
return $this->matcher->match($pathinfo);
} catch (ResourceNotFoundException $resourceNotFoundException) {
// Allow only GET requests
if ('GET' != $this->getContext()->getMethod()) {
throw $resourceNotFoundException;
}
// Get the first component of the uri
$routeName = current(explode('/', ltrim($pathinfo, '/')));
// Check that the method is available...
$baseControllerClass = RestController::class;
$controller = $baseControllerClass.'::action'.ucfirst($routeName);
if (is_callable($controller)) {
return [
'_controller' => $controller,
];
}
// Or bail
throw $resourceNotFoundException;
}
}
}
Now you need to override the Listener configuration:
// config/services.yaml
Symfony\Component\HttpKernel\EventListener\RouterListener:
arguments:
- '#App\Routing\NaiveRequestMatcher'
App\Routing\NaiveRequestMatcher:
arguments:
- '#router.default'
Not sure if it's the best approach, but seems the simpler one. The other option that comes to mind is to hook into the RouteCompiler itself.

SilverStripe 3.5.1 - Redirect to AbsoluteLink of SilverStripe page if user tries to navigate to amp.html version

I am working on a fork of the SilverStripe AMP module (https://github.com/thezenmonkey/silverstripe-amp) and have to prevent anyone from navigating to the amp.html version of a SilverStripe page if the amp.html link has not been generated into the head of the page.
A little additional info: We have added 2 fields that are meant to appear on the AMP versions of every page: AmpImage and AmpContent (a rich text editor). If either of these is empty, I have the code setup to NOT generate an AMP page for the SilverStripe page in question. This would seem to be enough but an additional requirement has been added, which is the redirect functionality mentioned about so no one can actually navigate to the amp.html page.
I was thinking of doing a redirect with the AmpSiteTreeExtension file but it does not appear to allow for redirects, then I thought of having a function in Page.php that would check if the url contains amp.html, then referencing it with AmpSiteTreeExtension, but every time I try, I get an error saying the function does not exist on "Page" or it's not public.
Is there a good way to handle this kind of situation? Is it best to do it with Page.php or using some other method?
Here are the files that I am working with:
AmpSiteTreeExtension
public function MetaTags(&$tags)
{
if ($this->owner->AmpContent != "" && $this->owner->AmpImageID != "") {
if ($this->owner->class != "HomePage") {
$ampLink = $this->owner->AbsoluteLink() . "amp.html";
} else {
$ampLink = $this->owner->AbsoluteLink() . "home/" . "amp.html";
}
$tags .= "<link rel='amphtml' href='$ampLink' /> \n";
}
//add a redirect here? Referencing a function from Page.php like so: $this->owner->functionName() causes the error mentioned above
}
}
<?php
AmpController
class AmpController extends Extension
{
private static $allowed_actions = array('amp');
private static $url_handlers = array(
'amp.html' => 'amp'
);
public function amp()
{
Requirements::clear();
$class = Controller::curr()->ClassName;
$page = $this->owner->renderWith(array("$class"."_amp", "Amp"));
return $this->AmplfyHTML($page);
}
public function AmplfyHTML($content)
{
if (!$content) {
return false;
}
$content = preg_replace('/style=\\"[^\\"]*\\"/', '', $content);
$content = str_replace("<img", "<amp-img", $content);
return $content;
}
}
From what I can tell, you're trying to redirect in the MetaTags() method of the SiteTree extension... and from what I can tell, that MetaTags() method it's probably used in some Silverstripe templates like this: $MetaTags
...and there's no way you can apply redirects this way.
You should do all this redirect stuff in a controller class, and from your example that controller it's probably the AmpController class which is probalby extending the functionality of the Page_Controller class.
Now I'll assume that AmpController it's an extension of Page_Controller, so I would do it like this:
class Page_Controller extends ContentController {
public function init() {
parent::init();
// you might have some other stuff here
// make sure this is the last line in this method
$this->extend('updateInit');
}
public function yourRedirectMethod() {
// do your redirect thing here
}
}
Key here is the following:
I extend the init() method in the controller - this will allow me to
extend the page controller's init() functionality using the
updateInit() method in the extension class (AmpController in this
case).
Instead of adding the method that's doing the redirect, to the Page
class, I added it to the Page_Controller class (the
yourRedirectMethod() method).
Now here comes the AmpController class, where I implement the updateInit() method:
class AmpController extends Extension {
private static $allowed_actions = array('amp');
private static $url_handlers = array(
'amp.html' => 'amp'
);
public function amp()
{
Requirements::clear();
$class = Controller::curr()->ClassName;
$page = $this->owner->renderWith(array("$class"."_amp", "Amp"));
return $this->AmplfyHTML($page);
}
public function AmplfyHTML($content)
{
if (!$content) {
return false;
}
$content = preg_replace('/style=\\"[^\\"]*\\"/', '', $content);
$content = str_replace("<img", "<amp-img", $content);
return $content;
}
public function updateInit() {
$should_redirect = true; // of course you add your own condition here to decide wether to redirect or not
if ($should_redirect) {
$this->owner->yourRedirectFunction();
}
}
}
The only thing here, is that you'll need to update the $should_redirect variable above (I've set it to true by default for this example - but here's where you decide if you should redirect or not)... and yes, you can reference here in the AmpController class stuff from the Page class I think, like this for exmaple: $this->owner->Title

PHP/Symfony - Parsing object properties from Request

We're building a REST API in Symfony and in many Controllers we're repeating the same code for parsing and settings properties of objects/entities such as this:
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
I'm aware that Symfony forms provide this functionality, however, we've decided in the company that we want to move away from Symfony forms and want to use something simplier and more customisable instead.
Could anybody please provide any ideas or examples of libraries that might achieve property parsing and settings to an object/entity? Thank you!
It seems like a good use case for ParamConverter. Basically it allows you, by using #ParamConverter annotation to convert params which are coming into your controller into anything you want, so you might just create ParamConverter with code which is repeated in many controllers and have it in one place. Then, when using ParamConverter your controller will receive your entity/object as a parameter.
class ExampleParamConverter implements ParamConverterInterface
{
public function apply(Request $request, ParamConverter $configuration)
{
//put any code you want here
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
//now you are setting object which will be injected into controller action
$request->attributes->set($configuration->getName(), $solution);
return true;
}
public function supports(ParamConverter $configuration)
{
return true;
}
}
And in controller:
/**
* #ParamConverter("exampleParamConverter", converter="your_converter")
*/
public function action(Entity $entity)
{
//you have your object available
}

Inject parameters to entity

I encounter this issue a lot of times, but only until now do I want to learn the best way to do it.
Say I have an Image entity, it has a 'path' property, which stores the relative path to the image file. For example, an image has its 'path' as '20141129/123456789.jpg'.
In parameters.yml, I set the absolute path to the directory that stores image files. Like this:
image_dir: %user_static%/images/galery/
I want to add the method 'getFullPath()' to Image entity, inside which the 'image_dir' parameter will be concatenated with 'path' property. I don't want to do the concatenation in controllers because I will be using it a lot. Also I don't want to insert image dir into Image's 'path' property, because I may change the image dir path later (which means I'll have to update the 'path' of all images in database).
So how can I inject the parameter into Image entity, so that getFullPath() can use it? Since Image entities will be fetched by repository methods instead of creating a new instance of Image, passing variables to construction method won't work.
Or is there a more elegant approach? I just want Image entities to have getFullPath() method, and I will be fetching images via both repository methods (find, findBy...) and query builder.
You could listen to the doctrine postLoad event and set the image directory in that so that when you later call getFullPath() it can return the concatenated string of the image directory and the path.
postLoad listener
namespace Acme\ImageBundle\Doctrine\EventSubscriber;
use Acme\ImageBundle\Model\ImageInterface;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
class ImageDirectorySubscriber implements EventSubscriber
{
protected $imageDirectory;
public function __construct($imageDirectory)
{
$this->imageDirectory = $imageDirectory;
}
public function getSubscribedEvents()
{
return array(
Events::postLoad,
);
}
public function postLoad(LifecycleEventArgs $args)
{
$image = $args->getEntity();
if (!$image instanceof ImageInterface) {
return;
}
$image->setImageDirectory($this->imageDirectory);
}
}
services.yml
parameters:
acme_image.subscriber.doctrine.image_directory.class:
Acme\ImageBundle\Doctrine\EventSubscriber\ImageDirectorySubscriber
services:
acme_image.subscriber.doctrine.image_directory:
class: %acme_image.subscriber.doctrine.image_directory.class%
arguments:
- %acme_image.image_directory%
tags:
- { name: doctrine.event_subscriber }
Image Model
class Image implements ImageInterface
{
protected $path;
protected $imageDirectory;
.. getter and setter for path..
public function setImageDirectory($imageDirectory)
{
// Remove trailing slash if exists
$this->imageDirectory = rtrim($imageDirectory, '/');
return $this;
}
public function getFullPath()
{
return sprintf('%s/%s', $this->imageDirectory, $this->path);
}
}
An alternative to #Qoop's approach is to make an image manager service and do the path stuff in it. The code will be a bit simpler.
class ImageManager
{
public function __construct($imageDirectory)
{
$this->imageDirectory = $imageDirectory;
}
public function getFullPath($image)
{
return $this->imageDirectory . $image->getPath();
}
}
// Controller
$imageManager = $this->get('image_manager');
echo $imageManager->getFullPath($image);
It's a trade off. Explicitly managing images vs using "behind the scenes" events.

Symfony Custom Route loader, loading multiple times, getting exception

I'm trying to make custom routeloader according to http://symfony.com/doc/current/cookbook/routing/custom_route_loader.html
my code looks like this
//the routeloader:
//the namespace and use code ....
class FooLoader extends Loader{
private $loaded = false;
private $service;
public function __construct($service){
$this->service = $service;
}
public function load($resource, $type=null){
if (true === $this->loaded)
throw new \RuntimeException('xmlRouteLoader is already loaded');
//process some routes and make $routeCollection
$this->loaded = true;
return $routeCollection;
}
public function getResolver()
{
// needed, but can be blank, unless you want to load other resources
// and if you do, using the Loader base class is easier (see below)
}
public function setResolver(LoaderResolverInterface $resolver)
{
// same as above
}
function supports($resource, $type = null){
return $type === 'xmlmenu';
}
}
//the service definition
foo.xml_router:
class: "%route_loader.class%"
arguments: [#foo.bar_service] //this service and the injection has been tested and works.
tags:
- { name: routing.loader }
//the routing definitions
//routing_dev.yml
_foo:
resource: "#FooBarBundle/Resources/config/routing.yml"
-----------------------------
//FooBarBundle/Resources/config/routing.yml
_xml_routes:
resource: .
type: xmlmenu
and when I try to access any route I get the exception:
RuntimeException: xmlRouteLoader is already loaded
which is the exception I defined if the loader is loaded multiple times.So why does it try to load this loader more than once? and I'm pretty sure I've defined it only there.
Actually the answer was quite simple.it seems like this method only supports one level of imports.I only needed to put the _xml_routes directly under routing_dev.yml, otherwise it somehow winds out in a loop.explanations to why that is are appreciated.

Resources