I'm using Symfony 2.8.0 (as I find Symfony 3.x not very mature at the moment, but let's not go into that discussion right now).
According to the official documentation
(http://symfony.com/doc/2.8/book/templating.html#embedding-controllers)
it should be possible to pass arguments to an embedded controller invoked from within a view.
However, this doesn't seem to work. I always end up with the following exception:
"Controller "AppBundle\Controller\DefaultController::buildNavigationAction()" requires that you provide a value for the "$argument1" argument (because there is no default value or because there is a non optional argument after this one)."
Within my view I have the following bit of code:
{{ render(controller('AppBundle:Default:buildNavigation'), {
'argument1': 25,
'argument2': 50
}) }}
The controller looks like this:
public function buildNavigationAction($argument1, $argument2)
{
// ... some logic ...
return $this->render(
'navigation.html.twig', array(
'foo' => $argument1,
'bar' => $argument2
)
);
}
What gives? Is this a bug?
The use case described in the documentation (rendering dynamic content from within the base template and therefor on every page) is exactly what I'm using it for. Repeating the same logic in every single controller is an obvious sin against the DRY principle.
Your syntax is incorrect, as you are not passing values to the controller since you are closing the ) too early. It should instead be:
{{ render(controller('AppBundle:Default:buildNavigation', {
'argument1': 25,
'argument2': 50
})) }}
Related
I've been struggling with this for a while but can't find a clean way to do it, so I'm seeking for some help.
I have custom filters (ApiPlatform 2.5 and Symfony 5.1) on database API outputs, and I need to filter on the current workflow place, or status as you like, of each output.
The Status has the below structure, which is a symfony workflow's place :
Status = { "OPEN": 1 }
My issue is that the status is stored as an array in the DB, and I can't find a way to have the querybuilder finding a match.
I've tried to build locally an array to do an = , a LIKE or an IN :
$status['OPEN'] = 1;
$queryBuilder->andWhere(sprintf('%s.Status = :st', $rootAlias))
->leftJoin(sprintf('%s.Objs', $rootAlias), 'o')
->andWhere('o.Profile = :p')
->setParameters(array(
'st' => $status,
'p' => $profile
));
But no way :(
I implemented a workaround that works but I don't like it as I'm using workflows a lot and need a clean way to filter outputs.
My workaround is fairly simple, when the status is writen as an array in the DB, I also store it as a string in another field called StatusText, then filtering on StatusText is easy and straight.
Status can have different contents obviously : OPEN, CLOSING, CLOSED, ...
Help appreciated !!
Thanks
EDIT & Solution
As proposed by Youssef, use scienta/doctrine-json-functions and use JSON_EXTRACT :
composer require scienta/doctrine-json-functions
Important, that was part of my issue, use the Doctrine type json_array an not array to store the status or the state, however you call it, in the Database.
Integrate the alias provided inside the ApiPlatform custom filter :
$rootAlias = $queryBuilder->getRootAliases()[0];
$json_extract_string = "JSON_EXTRACT(".$rootAlias.".Status, '$.OPEN') = 1";
$queryBuilder->andwhere($json_extract_string )
->leftJoin(sprintf('%s.Objs', $rootAlias), 'o')
->andWhere('o.Profile = :p')
->setParameter('p', $profile);
You need to ask Doctrine if the JSON array contains the status, but you can't do that with the QueryBuilder method.
When you hit the ORM limitations you can use a Native Query with ResultSetMapping. It allows you to write a pure SQL query using specific features of your DBMS but still get entity objects.
Or you can use scienta/doctrine-json-functions and use JSON_EXTRACT
I am trying to implement a search filter in my application which uses react/redux using redux-search. The first gotcha I get is when I try to add the store enhancer as in the example.
// Compose :reduxSearch with other store enhancers
const enhancer = compose(
applyMiddleware(...yourMiddleware),
reduxSearch({
// Configure redux-search by telling it which resources to index for searching
resourceIndexes: {
// In this example Books will be searchable by :title and :author
books: ['author', 'title']
},
// This selector is responsible for returning each collection of searchable resources
resourceSelector: (resourceName, state) => {
// In our example, all resources are stored in the state under a :resources Map
// For example "books" are stored under state.resources.books
return state.resources.get(resourceName)
}
})
)
I understand evarything up to the resourceSelector, when I tried to get a deep dive into the example to see how it works but I can barely see how they are generated and the last line returns an error, Cannot read property 'get' of undefined
My state object looks like this
state: {
//books is an array of objects...each object represents a book
books:[
//a book has these properties
{name, id, author, datePublished}
]
}
Any help from anyone who understands redux-search is helpful
If this line:
return state.resources.get(resourceName)
Is causing this error:
Cannot read property 'get' of undefined
That indicates that state.resources is not defined. And sure enough, your state doesn't define a resources attribute.
The examples were written with the idea in mind of using redux-search to index many types of resources, eg:
state: {
resources: {
books: [...],
authors: [...],
// etc
}
}
The solution to the issue you've reported would be to either:
A: Add an intermediary resources object (if you think you might want to index other things in the future and you like that organization).
B: Replace state.resources.get(resourceName) with state[resourceName] or similar.
I tried the following code to pass data to a template and receive it in onCreated() but I cannot access the data.
deviceInfo.js:
BlazeLayout.render('layout',{main:'deviceInfo',stats:'paramstats',attr:"SOME_DATA"});
deviceInfo.html:
{{>Template.dynamic template=stats data=attr}}
paramstats.js:
Template.paramstats.onCreated( () => {
console.log("onCreated");
console.log("Data is:",this.data.attr);
});
But I get TypeError: Cannot read property 'attr' of undefined.
where am I going wrong?
You need to use the normal function syntax for onCreated callback. Arrow function will bind the context of your function to the outer scope automatically, it is the cause of your problem. Try this:
Template.paramstats.onCreated(function() {
console.log("onCreated");
console.log("Data is:",this.data.attr);
});
I am using Meteor 1.4.# and I was able to retrieve the parameters like so:
BlazeLayout.render("page", {
params: ['fullscreen', 'route']
});
// page.js
Template.page.onCreated(function() {
let params = this.data.params();
console.log(params);
}
Not quite sure why you're using two levels of indirection. BlazeLayout.render() is giving you one level and then you're using a dynamic template within that? Why not directly render the template you ultimately want using BlazeLayout.render()?
In any case, you're dereferencing your data context indirectly.
In the BlazeLayout.render() call you're setting the attr variable to some value.
Then in your dynamic template you're using data=attr but this means that inside your template helpers that this is going be have the value of attr. There will be no data subkey added automatically.
You don't show the value that you're setting for attr so it's not even clear that you have an attr subkey in your attr variable, that would also be confusing to anyone else who ever tries to debug your code.
#khang is correct about not using the arrow function syntax in onCreated(), try:
Template.paramstats.onCreated(function(){
console.log("onCreated");
console.log("Data is:",this);
});
this should have whatever value you stuffed into attr in your BlazeLayout.render()
I have a big entity and a big form. When updating my entity, I only render parts of my form, through ajax calls. On client side, I'm using Jquery and html5 FormData, so I can also send files within my form. To make sure the fields that are not rendered won't be set to null in the process, I'm using PATCH method.
So when a field is not present in the request, it's left as is by Symfony.
But when the field I update is a boolean (rendered a a checkbox) that was set to true and I want to set it to false, it's not passed in the request, so my update is ignored.
Is there an easy way to force unchecked checkboxes to appear in the request?
EDIT
I found a way to force unchecked checkboxes to appear in the request, thanks to Felix Kling's comment on this question :
$("input:checkbox:not(:checked)").each(function() {
formData.set($(this).attr('name'), formData.has($(this).attr('id')) ? '1' : '0');
});
Unfortunately, this didn't solve my problem, because of Symfony's behaviour:
- When using PUT, if the boolean field appears in the request, it's set to true, regardless of its value (even if it's "0" or "false").
- When using PATCH method, the fields not appearing in the request are ignored.
Could that be solved with DataTransformer? (I've never used it)
You are absolutely right, Symfony will ignore it if method is PATCH because of this line in Request Handler:
$form->submit($data, 'PATCH' !== $method);
Now, I would generally suggest that you use a PUT request if that is an option, but if it isn't then second argument to FormInterface::submit($submittedData, $clearMissing = true) is what you're after.
The "proper" way would probably be to make your own implementation of Symfony\Component\Form\RequestHandlerInterface which would force $clearMissing to be true.
Other, way is a lot easier but might not work for all use-cases: use $form->submit() directly.
If you have the following code:
$form->handleRequest($request);
You can do:
$form->submit($request->get($form->getName()), true);
You can also omit second parameter since true is the default value
Here goes a working solution, that could be improved.
To force unchecked checkboxes to appear in the request, thanks to Felix Kling's comment on this question, I've added this js before my ajax request :
$("input:checkbox:not(:checked)").each(function() {
formData.set($(this).attr('name'), formData.has($(this).attr('id')) ? '1' : '0');
});
Then, on the Symfony side, I had to override the BooleanToStringTransformer behaviour, that returns true for whatever string and false only for null value. Making a change in the last line, we now return false if the value doesn't match the value defined for true ("1" by default). So if the value returned by the form is "0", we get false, as expected.
public function reverseTransform($value)
{
if (null === $value) {
return false;
}
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
return ($this->trueValue === $value); // initially: "return true;"
}
Following the docs, I made my own DataTransformer, as well as a custom AjaxCheckboxType
Unfortunately, it seems that Symfony uses both DataTransformers (mine and the original one), one after the other, so it didn't work. In the docs they extend TextType not CheckboxType, that must explain the problems I encountered.
I ended up copying and pasting the whole CheckboxType class in my own AjaxCheckboxType, only changing the DataTransformer's call in order to use mine.
A much nicer solution would be to totally override the DataTransformer, but I don't know how.
Symfony handles this out of the box, just prepare your PATCH-payload properly :)
The Symfony CheckboxType, at least in the current version 3.3 (seems like since 2.3, see update below), accepts an input value of null, interpreted as "not checked" (as you can see in lines 3-5 of the snippet in Roubi's really helpful answer).
So in your client-side AJAX-PATCH-controller you set the value of of your (dirty) unchecked checkbox-field in your application/merge-patch+json payload to null and everything is fine. No form extensions overwriting CheckboxType's behavior needed at all.
Problem is: I think, you cannot set values of HTTP-POST-payload to null, so this only works with JSON (or other compatible) payload within the request body.
A simple demo
To demonstrate this, you can use this simplified test controller:
/**
* #Route("/test/patch.json", name="test_patch")
* #Method({"PATCH"})
*/
public function patchAction(\Symfony\Component\HttpFoundation\Request $request)
{
$form = $this->createFormBuilder(['checkbox' => true, 'dummyfield' => 'presetvalue'], ['csrf_protection' => false])
->setAction($this->generateUrl($request->get('_route')))
->setMethod('PATCH')
->add('checkbox', \Symfony\Component\Form\Extension\Core\Type\CheckboxType::class)
->add('dummyfield', \Symfony\Component\Form\Extension\Core\Type\TextType::class)
->getForm()
;
$form->submit(json_decode($request->getContent(), true), false);
return new \Symfony\Component\HttpFoundation\JsonResponse($form->getData());
}
For PATCH-requests with Content-Type: application/merge-patch+json or in this case also any valid JSON-payload, the following will happen:
Submitting the checkbox with null value
{"checkbox": null}
will overwrite the checkbox to false:
{"checkbox": false, "dummyfield": "presetvalue"}
and submitting the checkbox with its original value
{"checkbox": "1"}
will set the checkbox to true (was also true before)
{"checkbox": true, "dummyfield": "presetvalue"}
and no submitted value for the checkbox
{"dummyfield": "requestvalue"}
will leave the checkbox in its initial true-state and only overwrites the dummyfield:
{"checkbox": true, "dummyfield": "requestvalue"}
This is how a PATCH request should work, no extra hidden inputs needed. Just prepare your JSON-payload on the client-side properly and you are fine.
OK, but what about the expanded ChoiceType/EntityType?
For expanded ChoiceType (or child types of it like EntityType), which renders checkboxes or radiobuttons and expects a simple list of the checked checkboxes/radiobuttons values within the submitted payload, this simple solution doesn't work. I implemented a form extension, adding an event listener for PRE_SUBMIT on those fields, setting the non submitted checkboxes/radiobuttons to null. This event listener must be called after the closure-listener of CheckboxType, transferring the simple list ["1", "3"] to a hash with checkbox-values as keys and values. A priority of -1 workes for me. So ["1" => "1", "3" => "3"] coming out of the closure gets ["1" => "1", "2" => null, "3" => "3"] after my listener. The listener of my PatchableChoiceTypeExtension looks basically like this:
$builder->addEventListener(
\Symfony\Component\Form\FormEvents::PRE_SUBMIT,
function (\Symfony\Component\Form\FormEvent $event) {
if ('PATCH' === $event->getForm()->getRoot()->getConfig()->getMethod()
&& $event->getForm()->getConfig()->getOption('expanded', false)
) {
$data = $event->getData();
foreach ($event->getForm()->all() as $type) {
if (!array_key_exists($type->getName(), $data)) {
$data[$type->getName()] = null;
}
}
ksort($data);
$event->setData($data);
}
}, -1
);
Update: have a look at this comment within the submit-method in /Symfony/Component/Form/Form.php (it is there since Symfony 2.3):
// Treat false as NULL to support binding false to checkboxes.
// Don't convert NULL to a string here in order to determine later
// whether an empty value has been submitted or whether no value has
// been submitted at all. This is important for processing checkboxes
// and radio buttons with empty values.
Update 2017-09-12: Radiogroups must be handled the same way as Checkboxgroups, so my listener handles both. Selects and multi selects work correctly out of the box.
I'd like to add a hook function when the "path namespace" changes. For example, when the router goes from myapp/fields/9837278993 to myapp/lists/183727856. Of course I could use some regex conditional logic in the main onBeforeAction hook. The more abstract logic would be "when namespace goes from fields to any except fields run the hook. What are your suggestions? Thanks for your help!
I think you're on the right track wanting to abstract the logic away from the router layer (makes switching routers much easier!). The problem is getting the data context before it changes.
To accomplish this, I'd recommend turning your Router.go line into a function. Chances are this is inside an event, so the IR reactive current() won't hurt you here.
function changePath(path) {
if (path !== 'fields' && Router.current().route.name === 'fields') {
//do stuff
}
Router.go(path);
}
I ended up with my own fully generic solution, using the reactive var Router.current().location.get() mixed with the Tracker.autorun function. So It's 100% independant of Router configuration, which I think is a good thing. Written in coffeescript.
Trivial to use :
Meteor.startup ->
RouterTransitions.register('/lists/' ,null,-> console.log("Switching from lists to any"))
Expected behaviour :
When the route changes from /lists/* to /(*) where (*)!='lists/', the hook function is run.
You can register as many transitions as you want from the same origin, as long as the destination is not a subpath of the origin (for example : from /lists/ to /lists/1864f will not trigger the hook). However the origin can be a subpath of the destination.
Here is the source :
class RouterTransitions
#could be any sequence of characters with at least one forbidden in URL standards (RFC 3986, section 2 characters)
WILDCARD:'>>'
constructor:->
Tracker.autorun =>
newPath=Router.current()?.location.get().path
if #_oldPath!=newPath
#_matchingDestinations.forEach (destinations) =>
origin=destinations.origin
Object.keys(destinations.targets).forEach (key) =>
if !newPath.match("#{origin}.*")&&(key==#WILDCARD or newPath.match("#{key}.*"))
#call the hook
destinations.targets[key]()
#_matchingOrigins =Object.keys(#dictionnary).filter (origin) => newPath.match("#{origin}.*")
#_matchingDestinations = #_matchingOrigins.map (key)=> {
targets:#dictionnary[key]
origin:key
}
#_oldPath=newPath
###
#param {String} origin : the namespace of the incoming path, null for any match. Origin can be a subset of destination.
#param {String} destination : the namespace of the forthcoming path, null for any match. Important! destination cannot be a subset of origin
#param {String} hook : the callback to be run on matching conditions
###
register:(origin,destination,hook) =>
origin=origin||#WILDCARD
destination=destination||#WILDCARD
if #dictionnary[origin]
#dictionnary[origin][destination]=hook
else
hooks=#dictionnary[origin]={}
hooks[destination]=hook
#A simple dict with keys='origin' and values plain objects with destinations mapped to hooks
dictionnary:{}
_oldPath:'/'
_matchingDestinations:[]
_matchingOrigins:[]
window.RouterTransitions=new RouterTransitions()