Multiple Symfony form types, all for same object, validate at once by faking submission - symfony-forms

I have several FormTypes, all for the same entity type (Car, e.g.), each handles a different aspect (DoorsTyoe, EngineType, TrunkType, SeatsType). When the user has walked through all these forms, on different pages, I want to check before finally saving the results, once more all FormTypes.
So the idea was: Create a new FormType CarType, put all the different pieces on it (DoorsType, EngineType, ...), "fake a submit" and get the errors. But when I want to add the different "sub-forms", I cannot add them without specifying a "child name" for them. Is there any way around this? Right now, I create an array with all the child names, and have a the car object in a separate key:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'doors',
DoorsType::class,
[
'getter' => function ($data, FormInterface $form): Car {
return $data['theCar'];
},
'setter' => function (array $data, Car $submittedData, FormInterface $form): void {
// just throw away what we get.
},
],
);
// And so on for EngineType etc.
}
Then I call this with:
$fooData = [
"doors" => "PLACEHOLDER",
"engine" => "PLACEHOLDER",
"theCar" => $car
];
$form = $this->formFactory->create($formType, $fooData, [
]);
$form->submit($fooData, true);
$errors = $form->getErrors(true, true);
dump($errors);
return $form->isValid();
My issue is the crazy structure with the $fooData array. I would prefer to work on $car only. When I change the DoorsType etc. to unmapped fields, the getters are not called so I assume they will not work at all?
Somehow I am missing a point...

Related

How to insert a record with a Relationship

Apologies, if this question is obvious, but I can't seem to find sufficient documentation. I might be lacking knowledge with restful methodologies. How do I store a record with a relationship?
I have a place. I want to store posts of users made to this place. So a place can have many posts. A post belongs to one place.
I'm using Aqueduct 3.0 Pre-Release.
I have following models:
place.dart
class Place extends ManagedObject<_Place> implements _Place {}
class _Place {
#primaryKey
int id;
#Column(unique: true)
String name;
ManagedSet<Post> posts;
}
post.dart
import 'package:places_api/model/place.dart';
import 'package:places_api/places_api.dart';
class Post extends ManagedObject<_Post> implements _Post {}
class _Post {
#primaryKey
int id;
#Column()
String text;
#Relate(#posts)
Place place;
}
I try to save the post, but there is only the possibility to store a place object, and not a place_id. Obviously below post query does not work, as there is only a values.place object, and not a values.place_id property. Is it intended to load the place, and then store all the posts to it?
Also without relationship, I can't store the place_id, as it seems that Aqueduct treats the _ as something special. Can't I use database properties, that have an underscore?
Is there any example that explains this?
#Operation.post()
Future<Response> createPost() async {
final body = request.body.asMap();
final query = new Query<Post>(context)
..values.place_id = body['place_id']
..values.text = body['text'];
final insertedPost = await query.insert();
return new Response.ok(insertedPost);
}
Currently I'm sending following body as POST:
{
"place_id": 1,
"text": "My post here"
}
To following URL: http://localhost:8888/posts
Would it be better to send something like this?
{
"text": "My post here"
}
To URL: http://localhost:8888/place/1/posts
Then fetch the place first, and store the post to it?
When represented as JSON, a relationship is always a list or an object. In your case:
{
"text": "text",
"place": {
"id": 1
}
}
This allows client application parsing code to remain consistent - a related object is always an object, never a synthetic field (e.g., place_id). The underlying database does name the column place_id by joining the relationship name and its primary key name with an underscore, but that's an implementation detail that isn't exposed through the API.
When inserting an object, foreign keys are inserted because they are a column in the table. Therefore, you can write your operation method as so:
#Operation.post()
Future<Response> createPost(#Bind.body() Post post) async {
final query = new Query<Post>(context)
..values = post;
final insertedPost = await query.insert();
return new Response.ok(insertedPost);
}
If you were to use the example JSON and this code, you'd get this SQL query: INSERT INTO _post (text, place_id) VALUES ('text', 1).
When inserting the 'has' side of a relationship, you have to insert the related objects as a separate query. An update/insert query will only set/insert values on a single table. If it makes sense for your API, you may want to POST the following place JSON:
{
"name": "Place",
"posts": [
{"text": "text"}
]
}
Your code to insert this object graph might look like:
await context.transaction((t) async {
final q = Query<Place>(t)..values = body;
final insertedPlace = await q.insert();
await Future.wait(body.posts, (p) async {
final postQuery = Query<Post>(t)
..values = p
..values.place.id = insertedPlace.id;
return postQuery.insert();
});
});
Couple of other small notes: asMap has been removed and replaced with as<T> or decode<T>. You also do not need an #Column annotation if you aren't adding any flags. All fields declared in the table definition type are database columns. Transient fields are declared in the ManagedObject subclass, and can be annotated with #Serialize() if you want them to be a part of the API surface.

How to replace a Form Element by a custom one in ZF2?

I'm struggling with the MultipleCheckbox and particularly with the problem, that the form validation fails, if no checkbox of the list is selected. (See details here.)
To workaround the issue I want to replace the Zend\Form\Element\Checkbox by a custom one where I override the getInputSpecification() method:
Checkbox.php
namespace My\Form\Element;
use Zend\Form\Element\Checkbox as ZendCheckbox;
class Checkbox extends ZendCheckbox
{
public function getInputSpecification()
{
$spec = parent::getInputSpecification();
$spec['required'] = false;
return $spec;
}
}
module.config.php
return [
'form_elements' => [
'invokables' => [
'Zend\Form\Element\Checkbox' => 'My\Form\Element\Checkbox',
],
]
];
But my custom Checkbox class has not replaced the on of Zend. It's not ignored completely -- I see with Xdebug, that it's used in some cases (e.g. for a RadioButton element and for some single Checkbox elements). But in some other cases (particularly for the checkboxes of my MultiCheckbox) not.
What am I doing wrong? How to replace a Zend\Form\Element for the whole application?

BreezeJS: Why aren't deletions made in Before/AfterSaveEntitiesDelegate propagated back to the client?

I am writing a simple event planning web app (using BreezeJS/Entity Framework) - users create a tournament entity, and ask the server to generate one or more suggested plans (just one for the purposes of this post).
Whenever the user clicks "generate plan", the tournament (including lots of details needed to generate a plan) should be submitted to the server, the server should delete any existing plan, generate a new one, and the client-side model should be updated.
A perfect fit for a named save, I thought!
The problem is the last step: updating the client-side model. The plan entity added by the server appears as expected in the client, but the deletion is ignored. I.e. the client ends up with both the new and the old plan!
Here's my named save:
[Note: The description and code in this question omits a lot of irrelevant details (like 20 properties and entity types) to keep the size of the question down]
[HttpPost]
public SaveResult MyNamedSave(JObject saveBundle)
{
_contextProvider.BeforeSaveEntitiesDelegate = RecalculatePlan;
return _contextProvider.SaveChanges(saveBundle);
}
private Dictionary<Type, List<EntityInfo>> RecalculatePlan(Dictionary<Type, List<EntityInfo>> arg)
{
// See https://stackoverflow.com/questions/14517945/using-this-context-inside-beforesaveentity:
var readonlyContext = new PontifexContext();
foreach (var eventInfo in arg[typeof(Tournament)])
{
var tournament = (Tournament)eventInfo.Entity;
var deletePlan = readonlyContext.Plans.First(p => p.TournamentId == tournament.Id);
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(deletePlan, EntityState.Deleted););
var addPlan = new Plan {TournamentId = tournament.Id, };
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(addPlan, EntityState.Added););
}
}
Am I trying to use named-saves for something they're not meant to do (i.e. deleting and adding entities)?
PS: I tried doing an explicit addition and save using both readonlyContext and _contextProvider.Context, but that really didn't work.
EDIT:
If I try to explicitly delete the old plan from the DB like below, nothing happens:
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(deletePlan, EntityState.Deleted););
// Add this:
context.PlanEntries.Remove(deletePlan);
context.SaveChanges();
I'm guessing it's because _contextProvider.Context already has the old plan in cache, so deleting it "behind its back" (i.e. using another context) doesn't make a difference.
If I then try removing it using _contextProvider.Context, I get a weird duplicate-entry error from the framework.
I'm at my wits' end!
EDIT 2:
Here's the data in the save request and response, as logged by IEs developer tools.
Request first:
{
"entities": [
{
"Id": 1,
"EventName": "Test Tournament",
"EventTime": "2015-03-21T20:00:00.000Z",
"entityAspect": {
"entityTypeName": "Tournament:#Pontifex.Model",
"defaultResourceName": "Tournaments",
"entityState": "Unchanged",
"originalValuesMap": { },
"autoGeneratedKey": {
"propertyName": "Id",
"autoGeneratedKeyType": "Identity"
}
}
}
],
"saveOptions": { }
}
The server then deletes the existing Plan entry (Id=10), and adds a new (Id=11), which I verified using a SELECT directly in the DB. That is good.
But the response is:
[
{
"$id": "1",
"$type": "Pontifex.Model.Tournament, Pontifex.Server",
"Id": 1,
"EventName": "Test Tournament",
"EventTime": "2015-03-21T20:00:00.000",
"Plans": [
{
"$id": "17",
"$type": "Pontifex.Model.Plan, Pontifex.Server",
"Id": 11,
"TournamentId": 1,
"Tournament": { "$ref": "1" }
}
],
"BoardPlan": null
}
]
In this response, the deleted entity never appears, so the client understandably leaves it in its model.
The added Plan (Id 11) does appear, and is integrated in the client model.
BUT: judging from sbelinis answer to Server added object showing as added in client after save changes, the fact that the added Plan appears may be a lucky coincidence:
In your particular example, the new entity made into the save because it happened to be related to the entity of the BeforeSaveEntity method, but you should not rely on it.
But sbelinis example of how to properly add an entity appears incomplete (e.g. it refers to a local variable saveMapAdditions which isn't used elsewhere)
OK, I figured out how to work around this!
I still can't get the deletion reflected back into the client cache...BUT if my server-side code ALSO removes the deleted entity from all relations, the removals WILL be reflected back, and the entity will disappear from the client-side model.
The updated code (I've added the statement tournament.Plans.Remove(deletePlan)):
[HttpPost]
public SaveResult MyNamedSave(JObject saveBundle)
{
_contextProvider.BeforeSaveEntitiesDelegate = RecalculatePlan;
return _contextProvider.SaveChanges(saveBundle);
}
private Dictionary<Type, List<EntityInfo>> RecalculatePlan(Dictionary<Type, List<EntityInfo>> arg)
{
// See http://stackoverflow.com/questions/14517945/using-this-context-inside-beforesaveentity:
var readonlyContext = new PontifexContext();
foreach (var eventInfo in arg[typeof(Tournament)])
{
var tournament = (Tournament)eventInfo.Entity;
var deletePlan = readonlyContext.Plans.First(p => p.TournamentId == tournament.Id);
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(deletePlan, EntityState.Deleted););
// Workaround: Remove the deleted plan from all relations:
tournament.Plans.Remove(deletePlan);
var addPlan = new Plan {TournamentId = tournament.Id, };
arg[typeof(Plan)].Add(_contextProvider.CreateEntityInfo(addPlan, EntityState.Added););
}
}
Of course, if you search the client local cache for Plan entities, I suspect the deleted plan will still appear, so it's not perfect. But it works for me!

Selected attributes of genemu_jqueryselect2_entity not stored to database

I'm using genemu_jqueryselect2_entity for a multiple selection field within a form (located in an Sonata admin class) for a so called Uni (university) entity:
->add('courses', 'genemu_jqueryselect2_entity',array('multiple' => true, 'class' => 'PROJECT\UniBundle\Entity\Course'))
But the selected entries are not filled into my entity. With firebug I was able to detect, that the ids of the courses are passed correctly via POST.
Maybe the field is not correctly mapped to the Uni entity, but I have no idea why.
This is the adding method of my Uni entity, which doesn't even get called:
public function addCourse(\PROJECT\UniBundle\Entity\Course $courses)
{
$this->courses[] = $courses;
return $this;
}
How can I get the field to be mapped with the courses attribute of Uni? How could I debug this?
Any help will be appriciated!
Try writing that method like this:
public function addCourse(\PROJECT\UniBundle\Entity\Course $course)
{
$this->courses[] = $course;
$course->setUniversity($this); // Or similar.
return $this;
}
Otherwise foreign key is not set on a course row in the DB.
Try to create method setCourses
public function setCourses(\Doctrine\Common\Collections\Collection $courses)
{
$this->courses = $courses;
...
I don't know why, but the method addCourse isn't called.
Anyway, Tautrimas Pajarskas's answer was usefull to me so I gave an upvote.
The foreign key relationship was the necessary and missing part of my code.
I implemented it in the university sonata admin like this:
private function addUniToCourses ($university) {
foreach($university->getCourses() as $course) {
if(!$course->getUniversities()->contains($university)) {
$course->addUniversity($university);
}
}
}
public function prePersist($university) {
$this->addUniToCourses($university);
}
public function preUpdate($university) {
$this->addUniToCourses($university);
}
This was the solution to my problem.
I had the same problem a while ago: Symfony2, $form->bind() not calling adder methods of entity
Solution:
For the adder (addCourse()) to be called, you have to disable the by_reference option of the field:
->add('courses', 'genemu_jqueryselect2_entity',
array(
'by_reference' => false, // This line should do the trick
'multiple' => true,
'class' => 'PROJECT\UniBundle\Entity\Course'))

Symonfy 1.4 dynamic validation possible?

I'm trying to create a form that change the validation of a field based on the select option from the html form field.
Ex: if user select a option 1 from drop down field "options", I want the field "metric" to validate as sfValidatorInteger. If user select option 2 from field "options", I want the field "metric" to validate as sfValidatorEmail, etc.
So inside the public function configure() { I have the switch statement to capture the value of "options", and create the validator based on that value returned from the "options".
1.) How do I capture the value of "options" ? I've tried:
$this->getObject()->options
$this->getTaintedValues()
The only thing that's currently working for me is but it's not really MVC:
$params = sfcontext::getInstance()->getRequest()->getParameter('options');
2.) Once I've captured that information, how can I assign the value of "metric" to a different field? ("metric" is not a real column in db). So I need to assign the value of "metric" to different field such as "email", "age" ... Currently I'm handling this at the post validator like this, just wondering if I can assign value within the configure():
$this->validatorSchema->setPostValidator(new sfValidatorCallback(array('callback' => array($this, 'checkMetric'))));
public function checkMetric($validator, $values) {
}
Thanks!
You want to use a post validator. Try doing something like this in your form:
public function configure()
{
$choices = array('email', 'integer');
$this->setWidget('option', new sfWidgetFormChoice(array('choices' => $choices))); //option determines how field "dynamic_validation" is validated
$this->setValidator('option', new sfValidatorChoice(array('choices' => array_keys($choices)));
$this->setValidator('dynamic_validation', new sfValidatorPass()); //we're doing validation in the post validator
$this->mergePostValidator(new sfValidatorCallback(array(
'callback' => array($this, 'postValidatorCallback')
)));
}
public function postValidatorCallback($validator, $values, $arguments)
{
if ($values['option'] == 'email')
{
$validator = new sfValidatorEmail();
}
else //we know it's one of email or integer at this point because it was already validated
{
$validator = new sfValidatorInteger();
}
$values['dynamic_validation'] = $validator->clean($values['dynamic_validation']); //clean will throw exception if not valid
return $values;
}
1) In a post validator, values can be accessed by using the $values parameter. Just use $values['options'] and it should be fine... or did you want to access this values from another part of you code? $this->getObject()->widgetSchema['options'] should work too I think, once your form is bound to an object.
2) The configure() method is called at the end of the form constructor, so values are not bound nor accessible yet, unless you are initializing your form with an object from the db (which does not require any validation). But if you want to initialize your form from $_POST, a post validator is definitely the way to go IMHO.
I got the validation error to appear alongside the field by throwing a sfValidatorErrorSchema instead of a sfValidatorError.
$values['dynamic_validation'] = $validator->clean($values['dynamic_validation']);
…becomes…
try
{
$values['dynamic_validation'] = $validator->clean($values['dynamic_validation']);
}
catch(sfValidatorError $e)
{
$this->getErrorSchema()->addError($e, 'dynamic_validation');
throw $this->getErrorSchema();
}
Not sure if this is the best way to get this result, but it seems to be working for me at the moment.

Resources