Dynamically adding normalizer to Symfony - php

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).

Related

Route to URL helper default parameters

I am using the code found here https://laracasts.com/discuss/channels/laravel/how-to-prefixing-in-routes-for-localization to prefix my routes with a locale.
Route::prefix('{lang?}')->middleware('locale')->group(function() {
Route::get('/', function () {
return view('index');
})->name('index');
});
This locale is optional, but then my question is how am I supposed to reinject the locale when calling the route helper.
route('index');
to generate: /it/ or /, or another, depending on the current locale.
If have tried this with no success :
Route::resourceParameters(['lang' => 'en']);
Actually, I'm a bit unsure about what the question is. I took it as a way to generate URL without setting its dynamic parameters using route helper based on #rahulsm's answer.
So, I just figured out that you can set default parameters to UrlGenerator class. By listening on container url and request rebind event, it's possible to set default parameters there.
Inside AppServiceProvider boot
$this->app->rebinding('url', function ($url, $app) {
$url->defaults(['lang' => $app->getLocale()]);
});
$this->app->rebinding('request', function ($app) {
$app['url']->defaults(['lang' => $app->getLocale()]);
});
And then tried to call it,
route('index') // http://test.dev/en (based on config)
route('index', ['lang' => 'what']) //http://test.dev/what
This was tested only on Laravel 5.5, but I'm sure would also working on Laravel 5.4.
To explain a bit more about rebinding method available Laravel container[1], firstly it's good to know how is Laravel request lifecycle works. In a one line simplified words, it should be the same as how an application in general works, which is: receive a request, do the logic, return a response.
At the second part, appear stage commonly said as bootstrapping, which one of its logic is try to store (register) classes (services) that mostly are important for the application[2] to work. Into a container. Thus, intended to be shared or even just for easily to be called, whether for the high-end developers or the framework itself.
Once registered, it will then be booted. This booting process has several actions to call[3]. One of it that suit this case is firing (local) events. Once a service has been resolved, it will try to call all registered rebound functions (listeners) at this stage. I can only assume this purpose is to make the application can be "easily mutate" the currently-resolving instance (service). Thus, defining rebinding method to listen to recalled event is the way to go.
Since Laravel allows to re-resolve (re-instantiate) a service, which means our previous stored value in the class is lost[4], waiting it to be resolved (which then the listener called) is much make sense, right?
Back to rebinding snippet above, I used to listen to both url and request rebound services because url is request's dependent. It waits for request service to be fully resolved and then call setRequest[5] method which flushes the needed instance which is Illuminate\Routing\RouteUrlGenerator that holds default parameter.
And as the name implies, defaults, used to set the default named parameters used by the URL generator[6].
cit
[1] Container here refer to both Illuminate\Foundation\Application also Illuminate\Container\Container
[2] configure error handling, configure logging, detect the application environment
[3] resolving type-hinted dependencies, storing/caching stuffs to buffer, etc
[4] Unless retained like stored in class' static property
[5] Illuminate\Routing\UrlGenerator#setRequest
[6] Illuminate\Routing\UrlGenerator, either calling URL::route('route.name'), url('your-url'), app('url')->route('route.name'), or route('route.name'), they refer to the same class
This should should be,
route('index', ['lang' => 'fr']);
And change route helper with
/**
* Generate a URL to a named route.
*
* #param string $name
* #param array $parameters
* #param bool $absolute
* #param \Illuminate\Routing\Route $route
* #return string
*/
function route($name, $parameters = [], $absolute = true, $route = null)
{
if (!isset($parameters['lang'])) $parameters['lang'] = App::getLocale();
return app('url')->route($name, $parameters, $absolute, $route);
}
Refer link for more details.

Injecting Doctrine Entity into Symfony controller based on route parameters

I want to inject Doctrine entities into controller actions based on the route parameters in an attempt to reduce the insane amount of code duplication inside my controllers.
For example I have the following route
product:
path: /product/edit/{productId}
defaults: { _controller: ExampleBundle:Product:edit }
Instead of my current approach
public function editAction($productId)
{
$manager = $this->getDoctrine()->getManager();
$product = $manager->getRepository('ExampleBundle:Product')
->findOneByProductId($productId);
if (!$product) {
$this->addFlash('error', 'Selected product does not exist');
return $this->redirect($this->generateUrl('products'));
}
// ...
}
I'd like this to be handled else where as it's repeated in at least 6 controller actions currently. So it would be more along the lines of
public function editAction(Product $product)
{
// ...
}
It seems this has in fact been done before and the best example I can find is done by the SensioFrameworkBundle http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
I'd use this but were not using annotations in our Symfony projects so need to look at alternatives. Any suggestions on how this can be achieved?
If you read the docs carefully, you'll learn that param converters actually work without annotations:
To detect which converter is run on a parameter the following process is run:
If an explicit converter choice was made with #ParamConverter(converter="name") the converter with the given name is chosen.
Otherwise all registered parameter converters are iterated by priority. The supports() method is invoked to check if a param converter can convert the request into the required parameter. If it returns true the param converter is invoked.
In other words if you don't specify a param converter in an annotation, Symfony will iterate through all registered converters and find the most appropriate one to handle your argument (based on a type hint).
I prefer to put an annotation in order to:
be explicit
save some processing time

How to do autorendering in Symfony

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.

Symfony Model Callback Equivalent

I'm working on a Symfony project (my first) where I have to retrieve, from my Widget class, a set of widgets that belong to a Page. Before returning the results, though, I need to verify--against an external service--that the user is authorized to view each widget. If not, of course, I need to remove the widget from the result set.
Using CakePHP or Rails, I'd use callbacks, but I haven't found anything similar for Symfony. I see events, but those seem more relevant to controllers/actions if I'm reading things correctly (which is always up for discussion). My fallback solution is to override the various retrieval methods in the WidgetPeer class, divert them through a custom method that does the authorization and modifies the result set appropriately. That feels like massive overkill, though, since I'd have to override every selection method to ensure that authorization was done without future developers having to think about it.
It looks like behaviors could be useful for this (especially since it's conceivable that I might need to authorize other class instances in the future), but I can't find any decent documentation on them to make a qualified evaluation.
Am I missing something? It seems like there must be a better way, but I haven't found it.
First of all, I think behavior-based approach is wrong, since it increases model layer coupling level.
There's sfEventDispatcher::filter() method which allows you to, respectively, filter parameters passed to it.
So, draft code will look like:
<somewhere>/actions.class.php
public function executeBlabla(sfWebRequest $request)
{
//...skip...
$widgets = WidgetPeer::getWidgetsYouNeedTo();
$widgets = $this->getEventDispatcher()->filter(new sfEvent($this, 'widgets.filter'), $widgets));
//...skip...
}
apps/<appname>/config/<appname>Configuration.class.php
//...skip...
public function configure()
{
$this->registerHandlers();
}
public function registerHandlers()
{
$this->getEventDispatcher()->connect('widgets.filter', array('WidgetFilter', 'filter'));
}
//...skip
lib/utility/WidgetFilter.class.php
class WidgetFilter
{
public static function filter(sfEvent $evt, $value)
{
$result = array();
foreach ($value as $v)
{
if (!Something::isWrong($v))
{
$result[] = $v;
}
}
}
}
Hope you got an idea.
Here's some documentation on Symfony 1.2 Propel behaviors: http://www.symfony-project.org/cookbook/1_2/en/behaviors.
Why not just have a 'getAllowedWidgets' method on your Page object that does the checks you're looking for? Something like:
public function getAllowedWidgets($criteria = null, PropelPDO $con = null) {
$widgets = $this->getWidgets($criteria, $con);
$allowed = array();
// get access rights from remote service
foreach($widgets as $widget) {
// widget allowed?
$allowed[] = $widget;
}
return $allowed;
}
However, if you always want this check to be performed when selecting a Page's Widgets then Propel's behaviours are your best bet.
Although, at least in theory, I still think that a behavior is the right approach, I can't find a sufficient level of documentation about their implementation in Symfony 1.4.x to give me a warm and fuzzy that it can be accomplished without a lot of heartburn, if at all. Even looking at Propel's own documentation for behaviors, I see no pre- or post-retrieval hook on which to trigger the action I need to take.
As a result, I took my fallback path. After some source code sifting, though, I realized that it wasn't quite as laborious as I'd first thought. Every retrieval method goes through the BasePeer model's doSelect() method, so I just overrode that one in the customizable Peer model:
static public function doSelect( Criteria $criteria, PropelPDO $con = null ) {
$all_widgets = parent::doSelect( $criteria, $con );
$widgets = array();
foreach ( $widgets as $i => $widget ) {
#if( authorized ) {
# array_push( $widgets, $widget );
#}
}
return $widgets;
}
I haven't wired up the service call for authorization yet, but this appears to work as expected for modifying result sets. When and if I have to provide authorization for additional model instances, I'll have to revisit behaviors to remain DRY, but it looks like this will suffice nicely until then.

How Can I Write Zend Framework URLs That Have Anchor Tags In The Body?

Using the standard MVC set up in Zend Framework, I want to be able to display pages that have anchors throughout. Right now I'm just adding a meaningless parameter with the '#anchor' that I want inside the .phtml file.
<?= $this->url(array(
'controller'=>'my.controller',
'action'=>'my.action',
'anchor'=>'#myanchor'
));
This sets the URL to look like /my.controller/my.action/anchor/#myanchor
Is there a better way to accomplish this? After navigation to the anchor link, the extra item parameter gets set in the user's URL which is something I would rather not happen.
one of possibilities is to override url helper, or to create a new one.
class My_View_Helper_Url extends Zend_View_Helper_Url
{
public function url(array $urlOptions = array(), $name = null, $reset = false, $encode = true)
{
if (isset($urlOptions['anchor']) && !empty($urlOptions['anchor']))
{
$anchor = $urlOptions['anchor'];
unset($urlOptions['anchor']);
}
else
{
$anchor = '';
}
return parent::url($urlOptions, $name, $reset, $encode).$anchor;
}
}
this helper override url helper, problem is, that you can't use parameter called 'anchor', because it will be changed into anchor in url.
you will call it as in your's example
<?= $this->url(array(
'controller'=>'my.controller',
'action'=>'my.action',
'anchor'=>'#myanchor'
));
I hope it helps
There are multiple ways you could go about implementing a fragment id into your URLs. Below are some options, along with some pros and cons for each.
Direct Add
You could simply add the "#$fragment_id" after your url() call. Inelegant, but simple. If you don't use page anchors much (i.e. One or two pages only), this is the way to go.
Write a custom url() helper
You could write a custom version of url() appending an optional 5th argument for the fragment id:
class My_View_Helper_Url extends Zend_View_Helper_Url
{
public function url(array $urlOptions = array(), $name = null,
$reset = false, $encode = true,
$fragment_id = null)
{
$uri = parent::url($urlOptions, $name, $reset, $encode);
if(!is_null($fragment_id)) {
$uri .= "#$fragment_id";
}
return $uri;
}
}
This way, anchor (and anchor/fragment id) information is kept strictly withing the realm of the View. This is good for general use, but can get a little unwieldy for the default route. Also, this is still a little too hard-coded for some uses.
Write a custom Route class (Extreme)
As a third option, you could write a custom version of the Zend_Controller_Router_Route class(es), specifically the assemble($data, $reset, $encode) method (the match($path) method ignores fragment ids by default).
Using this method can be quite tricky, but very useful, especially if use is only limited to specific routes (this method can be used to base the fragment id off of any variable).
Caveat
Certain considerations must be taken into account when using fragment ids. For example, query strings have to precede the fragment id in the uri, otherwise, the query string ignored by PHP. However, most ZF applications tend to avoid use of query strings, so it may not be an issue.
The url view helper accepts a 'fragment' key for the third option:
url('[route]',array([params]),array('fragment'=>'anchor'));
this will automatically end the url with #anchor.
-Thanks to Exlord
I think the Extreme method of writing a custom route class is better because other helper will have the same behavior (like the redirector action helper).

Categories