How to do autorendering in Symfony - php

I'm guessing its just not done since I can't find any reference to this anywhere, even people asking without response. Although I'm hoping Symfony calls it something else.
How can I get Symfony to auto render views/{controller}/{action}.html.php

No, it's not possible in standard edition of Symfony. However, routing is just one of its components. So if you really want, you can create your own component and use it instead.

You have two options:
1) Use SensioFrameworkExtraBundle - it allows to use #Template annotation. (It's included in SE).
2) Write your own method. I found it annoying to write #Template and any annotations in controllers every time so I added this method in the base controller (it's only an example, examine before using it in production):
public function view(array $parameters = array(), Response $response = null, $extension = '')
{
$extension = !empty($extension) ? $extension : $this->templateExtension;
$view = ViewTemplateResolver::resolve($this->get('request')->get('_controller'), get_called_class());
return $this->render($view . '.' . $extension, $parameters, $response);
}
class ViewTemplateResolver
{
public static function resolve($controller, $class)
{
$action = preg_replace('/(.*?:|Action$)/', '', $controller);
if (preg_match('~(\w+)\\\\(\w+Bundle).*?(\w+(?=Controller$))~', $class, $name)) {
return implode(':', array($name[1] . $name[2], $name[3], $action));
}
}
}
Now in the controller we can do: return $this->view();

When you do routing with annotations instead of yaml, you can add a #Template() to your action method and it's rendering the default template as requested by you.
To do this, change your routing to annotations:
AcmeDemoBundle:
resource: "#PAcmeDemoBundle/Controller/"
type: annotation
prefix: /
Within your controllers, add this for each action:
/**
* #Route("/index", name="demo_index")
* #Template()
*/
Actually I don't know if there is a way to get this behaviour when not using annotations. But as there seems to be logic for this, there might be one.

#meze's answers are both more desirable than out of the box behaviour.
However I think the SensioFrameworkExtraBundle he pointed me to has given me the clue that I needed to achieve this without replacing my own route.
That is to hook into the kernel view event.
Its purpose is specifically stated as:
The purpose of the event is to allow some other return value to be converted into a Response.
I'm assuming then that it can be used to convert a null return from the controller action to a response.

Related

Dynamically adding normalizer to Symfony

I am looking for a way to use a custom normalizer based on some conditions. Conditions being dependent on some parameter in the URL.
The normalizer I created gets autowired automatically, but I want to use it ONLY if a parameter is passed in the URL.
Is there a way to do this? Dynamically add the normalizer to the container in a request listener?
Currently if I set autoconfigure to true, it uses my normalizer, and set to false, it does not get used. But I need to make this configuration outside of the services.yml file.
#services.yaml
App\Framework\Serializer\CustomNormalizer:
autoconfigure: false
That's not how autowiring works. You'll need to put your logic somewhere else. Container compilation happens before any request is made, so it cannot be made dependant on parameters values.
But you can use the supportsNomralization() method and the $context to decide to use your normalizer or not.
public function supportsNormalization($data, string $format = null, array $context = [])
{
if (!$data instanceof Foo) {
return false;
}
if (!\array_key_exists('foo_bar', $context) || $context['foo_bar'] !== '1') {
return false;
}
return true;
}
And then when serializing your Foo, pass the appropriate parameter in the context if the parameter comes in the request:
$fooBar = $request->query->get('foo_bar');
$normalized = $this->serializer->normalize($someFoo, ['foo_bar' => $fooBar]);
This is a simple, untested example, but you should get the general idea to implement your own.
Again, implementing this on the DI layer is a no-go. The DI container is compiled ahead of time, so there is no information about the request there (nor should be).

Symfony 4 Voter Annotations (#IsGranted)

I'm trying to use Symfony Voters and Controller Annotation to allow or restrict access to certain actions in my Symfony 4 Application.
As an example, My front-end provides the ability to delete a "Post", but only if the user has the "DELETE_POST" attribute set for that post.
The front end sends an HTTP "DELETE" action to my symfony endpoint, passing the id of the post in the URL (i.e. /api/post/delete/19).
I'm trying to use the #IsGranted Annotation, as described here.
Here's my symfony endpoint:
/**
* #Route("/delete/{id}")
* #Method("DELETE")
* #IsGranted("DELETE_POST", subject="post")
*/
public function deletePost($post) {
... some logic to delete post
return new Response("Deleting " . $post->getId());
}
Here's my Voter:
class PostVoter extends Voter {
private $attributes = array(
"VIEW_POST", "EDIT_POST", "DELETE_POST", "CREATE_POST"
);
protected function supports($attribute, $subject) {
return in_array($attribute, $this->attributes, true) && $subject instanceof Post;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
... logic to figure out if user has permissions.
return $check;
}
}
The problem I'm having is that my front end is simply sending the resource ID to my endpoint. Symfony is then resolving the #IsGranted Annotation by calling the Voters and passing in the attribute "DELETE_POST" and the post id.
The problem is, $post is just a post id, not an actual Post object. So when the Voter gets to $subject instanceof Post it returns false.
I've tried injecting Post into my controller method by changing the method signature to public function deletePost(Post $post). Of course this does not work, because javascript is sending an id in the URL, not a Post object.
(BTW: I know this type of injection should work with Doctrine, but I am not using Doctrine).
My question is how do I get #IsGranted to understand that "post" should be a post object? Is there a way to tell it to look up Post from the id passed in and evaluated based on that? Or even defer to another controller method to determine what subject="post" should represent?
Thanks.
UPDATE
Thanks to #NicolasB, I've added a ParamConverter:
class PostConverter implements ParamConverterInterface {
private $dao;
public function __construct(MySqlPostDAO $dao) {
$this->dao = $dao;
}
public function apply(Request $request, ParamConverter $configuration) {
$name = $configuration->getName();
$object = $this->dao->getById($request->get("id"));
if (!$object) {
throw new NotFoundHttpException("Post not found!");
}
$request->attributes->set($name, $object);
return true;
}
public function supports(ParamConverter $configuration) {
if ($configuration->getClass() === "App\\Model\\Objects\\Post") {
return true;
}
return false;
}
}
This appears to be working as expected. I didn't even have to use the #ParamConverter annotation to make it work. The only other change I had to make to the controller was changing the method signature of my route to public function deletePost(Post $post) (as I had tried previously - but now works due to my PostConverter).
My final two questions would be:
What exactly should I check for in the supports() method? I'm currently just checking that the class matches. Should I also be checking that $configuration->getName() == "id", to ensure I'm working with the correct field?
How might I go about making it more generic? Am I correct in assuming that anytime you inject an entity in a controller method, Symfony will call the supports method on everything that implements ParamConverterInterface?
Thanks.
What would happen if you used Doctrine is that you'd need to type-hint your $post variable. After you've done that, Doctrine's ParamConverter would take care of the rest. Right now, Symfony has no idea how about how to related your id url placeholder to your $post parameter, because it doesn't know which Entity $post refers to. By type-hinting it with something like public function deletePost(Post $post) and using a ParamConverter, Symfony would know that $post refers to the Post entity with the id from the url's id placeholder.
From the doc:
Normally, you'd expect a $id argument to show(). Instead, by creating a new argument ($post) and type-hinting it with the Post class (which is a Doctrine entity), the ParamConverter automatically queries for an object whose $id property matches the {id} value. It will also show a 404 page if no Post can be found.
The Voter would then also know what $post is and how to treat it.
Now since you are not using Doctrine, you don't have a ParamConverter by default, and as we just saw, this is the crucial element here. So what you're going to have to do is simply to define your own ParamConverter.
This page of the Symfony documentation will tell you more about how to do that, especially the last section "Creating a Converter". You will have to tell it how to convert the string "id" into a Post object using your model's logic. At first, you can make it very specific to Post objects (and you may want to refer to that one ParamConverter explicitly in the annotation using the converter="name" option). Later on once you've got a working version, you can make it work more generic.

How to create a route with custom path in Symfony RoutingBundle (PHPCR)?

I'm currently researching Symfony CMF and PHPCR for a project I recently started. What I'm currently trying to figure out is how to create a Route and save it into the database. As far as I understand, I must use Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route and persist the element into the database. This works fine, but automatically generates a route path, which is not what I want. What I need to do is generate a custom route which links to a specific controller. Here is my code:
$em = $this->get('doctrine_phpcr.odm.document_manager');
$parent = $em->find(null, '/cms/routes');
$route = new \Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route();
$route->setParentDocument($parent);
$route->setName('my_route_name');
$route->setDefault('_controller', 'AppBaseBundle:Frontend/Users:index');
$em->persist($route);
$em->flush();
If i execute this code, the generated route will be /cms/routes/my_route_name. From what I can see, you could use $route->setPath('/testing');, but that generates the following exception:
Can not determine the prefix. Either this is a new, unpersisted document or the listener that calls setPrefix is not set up correctly.
Does anybody have any ideas how to solve this?
In PHPCR, every document has a path where it is store. If you are familiar with doctrine ORM, the path has the role of the ID. The difference with ORM is that all documents (regardless of their type) live in the same tree. This is great, because your route can reference just anything, it is not limited to specific document types. But we need to create some structure with the paths. This is why we have the prefix concept. All routes are placed under a prefix (/cms/routes by default). That part of the document path is removed for the URL path. So repository path /cms/route/testing is the url domain.com/testing.
About your sample code: Usually, you want to configure the controller either by class of the content document or by route "type" attribute to avoid storing a controller name into your database to allow for future refactoring. A lot of this is explained in the [routing chapter of the CMF documentation][1] but the prefix is only used there, not explicitly explained. We need to improve the documentation there.
[1] http://symfony.com/doc/master/cmf/book/routing.html
I managed to find a way to overcome this issue. Because in my project I also have the RouteAutoBundle, I created a class which extends \Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route. Inside that class I added:
/**
* #PHPCR\Document(referenceable=true)
*/
class MenuRoute extends Route
{
protected $url;
/**
* Get $this->url
*
* #return mixed
*/
public function getUrl() {
return $this->url;
}
/**
* Set $this->url
*
* #param mixed $url
*/
public function setUrl($url) {
$this->url = $url;
}
}
After that I added this to cmf_routing_auto.yml:
App\MenuBundle\Document\MenuRoute:
uri_schema: /{getUrl}
token_providers:
getUrl: [content_method, { method: getUrl }]
So now one would just create an instance of MenuRoute (just like when using Route) and call the method setUrl($your_url) passing the desired url.
If anybody finds a better way, I'm opened to suggestions.

Symfony2 Translation Add Html

I want to add some HTML to Symfony 2 Translation. This way I will know which phrases in my application are translated and which not.
I found in "Symfony\Component\Translation\Translator.php" function "trans". Now i want to add something in function return, for example "< /br>":
/**
* {#inheritdoc}
*
* #api
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
if (null === $locale) {
$locale = $this->getLocale();
} else {
$this->assertValidLocale($locale);
}
if (null === $domain) {
$domain = 'messages';
}
if (!isset($this->catalogues[$locale])) {
$this->loadCatalogue($locale);
}
return strtr($this->catalogues[$locale]->get((string) $id, $domain)."</br>", $parameters);
}
The issue is that when I run my application I'm getting for example "Tag< / b r>" (I have add spaces because in normal way it doesn't show here. HTML doesn't interprate this as HTML code but as a string. Is there any way to achieve what I want ? Maybe it is but in the other way ?
This happens because you have the Twig Escaper extension active. That extension adds automatic output escaping to Twig, it defines the autoescape tag and the raw filter.
So I think the best option you got here is to define a new twig extension to let you translate your html strings without having to repeat myvar|raw each time.
To see how it is possible to create a new Twig extension please check the docs here.
Use the same extension when escaping for JS and there should be no need to use anything else especially in your PHP controllers. That's because the escaping is done at the Twig level. Just remember to declare your new Twig filter as safe to avoid automatic escaping again:
$filter = new Twig_SimpleFilter('nl2br', 'nl2br', array('is_safe' => array('html')));
If you need to do some extra processing with the requested data so that you can track what strings are being requested and what not then just declare a new service as a proxy to the Symfony translation one. Your Twig extension can use the same service. This way you can converge all the requests to one single service.
Here a few useful links for you:
http://twig.sensiolabs.org/doc/api.html#escaper-extension
http://twig.sensiolabs.org/doc/advanced.html#automatic-escaping
I'll suggest you simply to use Markdown in your translation.
Then you can parse your translated message with a Markdown parser.
Example in Twig: 'my.message'|trans|markdown (I suppose you have a Markdown filter, there is KnpMarkdownBundle)
MY SOLUTION:
I have achieved what I wanted in few steps:
Override Translator.php and change translator.class in parameters.yml
public function trans($id, $parameters, $domain, $locale)
{
$return = parent::trans($id, $parameters, $domain, $locale);
return "".$return.'');
}
Set translation class in your css.
Set autoescape option to false in parameters.yml
twig:
autoescape: false

PhalconPHP: content negotiation?

Does Phalcon support content negotiation out-of-the-box or is there some easy-to-implement solution? I'm scouring the 'nets and not seeing it.
Thanks!
Short answer is no and thank god for that, or we'd have another 100 bugs for a non-major component :)
You can easily plug an existing library, like Negotiation, into DI and use it later globally throughout the app.
$di->setShared('negotiator', function(){
return new \Negotiation\Negotiator();
});
$bestHeader = $di->getShared('negotiator')->getBest('en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2');
Keep in mind that with the default server config (.htaccess / Nginx) from examples static files will be served as is, without interception by Phalcon. So, to server files from the server it would be best to create a separate controller / action to handle that rather than making all request go through your app.
Edit:
If it's simply about enabling your app sending either xml or json based on the common distinction (header, param, method), then you can easily accomplish it without external frameworks. There are many strategies, the simplest would be to intercept Dispatcher::dispatch(), decide in there what content to return and configure the view and response accordingly – Phalcon will do the rest.
/**
* All controllers must extend the base class and actions must set result to `$this->responseContent` property,
* that value will be later converted to the appropriate form.
*/
abstract class AbstractController extends \Phalcon\Mvc\Controller
{
/**
* Response content in a common format that can be converted to either json or xml.
*
* #var array
*/
public $responseContent;
}
/**
* New dispatcher checks if the last dispatched controller has `$responseContent` property it will convert it
* to the right format, disable the view and direcly return the result.
*/
class Dispatcher extends \Phalcon\Mvc\Dispatcher
{
/**
* #inheritdoc
*/
public function dispatch()
{
$result = parent::dispatch();
$headerAccept = $this->request->getHeader('Accept');
$headerContentType = $this->request->getHeader('Content-Type');
$lastController = $this->getLastController();
// If controller is an "alien" or the response content is not provided, just return the original result.
if (!$lastController instanceof AbstractController || !isset($lastController->responseContent)) {
return $result;
}
// Decide what content format has been requested and prepare the response.
if ($headerAccept === 'application/json' && $headerContentType === 'application/json') {
$response = json_encode($lastController->responseContent);
$contentType = 'application/json';
} else {
$response = your_xml_convertion_method_call($lastController->responseContent);
$contentType = 'application/xml';
}
// Disable the view – we are not rendering anything (unless you've already disabled it globally).
$view->disable();
// Prepare the actual response object.
$response = $lastController->response
->setContent($response)
->setContentType($contentType);
// The returned value must also be set explicitly.
$this->setReturnedValue($response);
return $result;
}
}
// In your configuration you must insert the right dispatcher into DI.
$di->setShared('dispatcher', function(){
return new \The\Above\Dispatcher();
});
Just thought that you can probably achieve the same using dispatch loop events. The solution in theory might look more elegant but I never attempted this, so you might want to try this yourself.

Categories