Symfony2: 1 form to edit a translatable entity - forms

I have a translatable entity using the translatable behaviour of doctrine2.
I'm trying to build a form that looks like this:
| French |English| Spanish |
+--+--------| |---------+------------+
| |
| name: [___my_english_name___] |
| |
| title: [___my_english_title__] |
| |
+------------------------------------------+
Order: [___1___]
Online: (x) Yes
( ) No
So basically, there are the order & online attributes of the object that are not translatable, and the name & title attribute that have the translatable behaviour.
In case my drawing is not clear: the form contain a 1 tab per locale that hold the field that are translatable.
The problem I have is that by default, Symfony2 bind a form to an entity, but the doctrine translatable behaviour force me to have one entity per locale. Personally the doctrine behaviour is fine (and I like it), but I'm unable to make a form that allow me to edit the entity in all the locale -- in the same form.
So far, I've the main form:
namespace myApp\ProductBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
/**
* Form for the productGroup.
*/
class ProductType extends AbstractType
{
/**
* Decide what field will be present in the form.
*
* #param FormBuilder $builder FormBuilder instance.
* #param array $options Custom options.
*
* #return null;
*/
public function buildForm(FormBuilder $builder, array $options)
{
//Todo: get the available locale from the service.
$arrAvailableLocale = array('en_US', 'en_CA', 'fr_CA', 'es_ES');
//Render a tab for each locale
foreach ($arrAvailableLocale as $locale) {
$builder->add(
'localeTab_' . $locale,
new ProductLocaleType(),
array('property_path' => false, //Do not map the type to an attribute.
));
}
//Uni-locale attributes of the entity.
$builder
->add('isOnline')
->add('sortOrder');
}
/**
* Define the defaults options for the form building process.
*
* #param array $options Custom options.
*
* #return array Options with the defaults values applied.
*/
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'myApp\ProductBundle\Entity\Product',
);
}
/**
* Define the unique name of the form.
*
* #return string
*/
public function getName()
{
return 'myapp_productbundle_producttype';
}
}
And the tab-form:
<?php
namespace MyApp\ProductBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use invalidArgumentException;
/**
* Form for the productGroupLocale tabs.
*/
class ProductLocaleType extends AbstractType
{
/**
* Decide what field will be present in the form.
*
* #param FormBuilder $builder FormBuilder instance.
* #param array $options Custom options.
*
* #return null;
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name', 'text', array('data' => ???));
$builder->add('title', 'text', array('data' => ???));
}
/**
* Define the defaults options for the form building process.
*
* #param array $options Custom options.
*
* #return array Options with the defaults values applied.
*/
public function getDefaultOptions(array $options)
{
return array(
//'data_class' => 'MyApp\ProductBundle\Entity\Product',
'name' => '',
'title' => '',
);
}
/**
* Define the unique name of the form.
*
* #return string
*/
public function getName()
{
return 'myapp_productbundle_productlocaletype';
}
}
But as you can't see, I've no idea how to get the name and title values from the translated entity, and neither I know how to persist them once the form will be submitted.

Hi if you use gedmo extensions Translatable is not meant to handle multiple translations per request. Try using knplabs alternative may be a better option to handle it in more general ways.

You may be interested in TranslationFormBundle, which add a form type to work with DoctrineTranslatable extension.

I've check the Translator extension, and even if it's interesting, it wasn't corresponding to our needs. (Basically, all the examples we found require that we change the site locale in order to edit an entity in another locale. I don't know Chinese, and I don't want my interface to be in Chinese, but I do have a translation that I have to copy/paste. Seems weird to explain that as it's really basic in every solid CMS you'll find out there, but I was looking a bit complex to do that kind of CMS functionnality using Symfony.)
So we've developed a solution and builded a BreadGeneratorBundle that we've decide to share:
https://github.com/idealtech/BreadGeneratorBundle
At the time of posting this, it still under development, but it can be used as an alternative to the CrudGenerator in order to generate form for translatable entity.
We also manage to use the Gedmo Extension -- even if Gediminas said it's not meant to handle multiple translation ;)
Hope this will help someone ! :)

Related

Why doesn't non doctrine-mapped ArrayCollection get initialized?

My Setup is a Symfony 3.4 App with the typical 'ManyToMany'-Relation with additional fields, something like this:
Entity Article
Entity Specialty
Entity ArticleSpecialtyRelation
In a Form for an Article i wanted it to look like as if it were a ManyToMany-Relation rendered as an EntityType with multiple=true and expanded=true, so all entries of Specialty are rendered as checkboxes.
To achieve that i created a non orm-mapped property specialties that is an ArrayCollection, gets initialized in the Constructor and has a Getter, Adder and Remover.
/**
*
* #var ArrayCollection;
*
*/
protected $specialties;
public function __construct()
{
$this->specialties = new ArrayCollection();
}
/**
* #return Collection|Specialty[]
*/
public function getSpecialties()
{
return $this->specialties;
}
/**
* #param Specialty $specialties
*/
public function addSpecialties(Specialty $specialties)
{
$this->specialties->add($specialties);
}
/**
* #param Specialty $specialties
*/
public function removeSpecialties(Specialty $specialties)
{
$this->specialties->removeElement($specialties);
}
This property is used to render the Specialty Entity as checkboxes:
add('specialties', EntityType::class,array(
'class' => Specialty::class,
'expanded'=>true,
'multiple'=>true,
'label'=>'Specialties',
'required' => false,
'mapped'=>true,
));
To populate it with the data from SpecialtyRelation i added a PreSetData Formevent:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$article = $event->getData();
if ($article instanceof Article) {
$form->get('specialties')->setData($article->getUsedSpecialties());
}
});
The used Getter of $artikel just iterates over $article->getArtikelSpecialties and returns a collection of Specialty.
It all works until the submit. Because the formfield is mapped=true, somewhere in handleRequest($form) where the entity is hydrated with the form data, it explodes when the Adder for $specialty is called:
Call to a member function add() on null
Because as i just learned, the Constructor is never called by Doctrine and obviously initializes all ORM-ArrayCollections but not the ArrayCollection for the non-mapped property specialties -
Of course I can check if the ArrayCollection is initialized in the Adder and Remover and initialize it there if it is null, but that just feels a bit hacky in a already at least hacky-felt setup and i am wondering if my setup is completely stupid, especially since i didn't find anybody trying to do that (or getting problems with that) on here or elsewhere.
Is there a better solution to this or should i just check the ArrayCollection in Adder and Remover and live happily ever after?
Also, just curious, is there any other way to initialize the ArrayCollection?
P.S. If there are typos in the names it's because i translated the names into english.
Partial Stacktrace
Symfony\Component\Debug\Exception\FatalThrowableError: Call to a
member function add() on null
at src/Test/Bundle/TestBundle/Entity/Article.php:312 at
Test\Bundle\TestBundle\Entity\Article->addSpecialties(object(Specialty))
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:674)
at
Symfony\Component\PropertyAccess\PropertyAccessor->writeCollection(array(object(Article),
object(Article)), 'specialties', object(ArrayCollection),
'addSpecialties', 'removeSpecialties')
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:622)
at
Symfony\Component\PropertyAccess\PropertyAccessor->writeProperty(array(object(Article),
object(Article)), 'specialties', object(ArrayCollection))
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:216)
at
Symfony\Component\PropertyAccess\PropertyAccessor->setValue(object(Article),
object(PropertyPath), object(ArrayCollection))
(vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php:86)
at
Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper->mapFormsToData(object(RecursiveIteratorIterator),
object(Article))
(vendor/symfony/symfony/src/Symfony/Component/Form/Form.php:636) at Symfony\Component\Form\Form->submit(array(), true)
(vendor/symfony/symfony/src/Symfony/Component/Form/Form.php:580)

Constraint on Value Object in form Symfony

I'm quite new to Symfony and I started digging around Symfony forms.
As described here https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/ I'm using value objects in my subform. A constructor of value object can throw an exception if invalid values are provided. Therefore when I put invalid value to my field I'm getting ugly exception from VO, hence I want to connect a Validator Constraint on this but the validate() function gets already a Value object... Any thoughts on this issue?
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
/....
$builder->add('latitude', LatitudeType::class, [
'label' => false,
'constraints' => [new Valid()],
]);
}
Latitude type
class LatitudeType extends AbstractType implements DataMapperInterface
{
const INPUT_NAME = 'latitude';
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(self::INPUT_NAME, TextType::class, [
'label' => 'FORM.LATITUDE',
'attr' => [
'placeholder' => 'PLACEHOLDER.LATITUDE',
],
'required' => false,
'constraints' => [new LatitudeValidator()],
]);
$builder->setDataMapper($this);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Latitude::class,
'empty_data' => null,
'error_bubbling' => true
]);
}
/**
* #param Latitude $data
* #param FormInterface[]|\Traversable $forms
*/
public function mapDataToForms($data, $forms)
{
$forms = iterator_to_array($forms);
$forms[self::INPUT_NAME]->setData($data);
}
/**
* #param FormInterface[]|\Traversable $forms
* #param mixed $data
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
if ($forms[self::INPUT_NAME]->getData()) {
$data = new Latitude((float)$forms[self::INPUT_NAME]->getData());
}
}
This validation method is receiving already a created VO
class LatitudeValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (null === $value || '' === $value) {
return;
}
But I want to be able to do something like
try {
new \ValueObject\Latitude((float)$value);
} catch (\InvalidArgumentException $e) {
$this->context->buildViolation($e->getMessage())
->addViolation();
}
You have differents methods to use form with Value Objects but after a lot of troubles by my side I decided to stop this. Symfony have to construct your Value Object even if your VO is invalid. You gave an example on an invalid state but you have also others example when you form doesn't fit well your Domain like when you have not enought fields to complete your required properties on your VOs.
Symfony Forms can be complexe and the use of VOs inside them can bring more complexity whereas the forms should be linked to the interface and not always to the domain objects.
The best solution for me is to use the command pattern. You have a simple example with other reasons to use it here. You can also avoid to put this logic into your controllers and avoid code duplication with a command bus librairy like tactician or now the messenger component of Symfony.
With a command you can simply represent an action by the form. The form can have validators related to the VO or directly to the form.
With the command bus you can create your Value Object in a valid state and throw exceptions in a second layer when you forget a use case.
This approach is more robust and avoid a lot of troubles for my point of view.
The best thing you achieve this, is to accept any kind of value into the ValueObject and then perform validation on it.
This way you're not forced to handle exception due to invalid types passed through constructor.
Moreover remember that creation or "value setting" of the underlying object is performed by the framework before validation (otherwise you'll never have to use VO) so you should leverage on this and let the Form component do his job (as you done correclty with transformers). Then, you can perform any kind of validation on underlying object.

ExtbaseObject with relation to multiple occurences of the same object

I've got an ExtbaseObject with a relation that can contain multiple references to the same subobject.
I've extended the mm table for the relation with a uid field and set the option MM_hasUidField in the tca. In the backend everything works as intended:
But if I load the object in the frontend I get only one occurence of "Testzusatzpaket 1":
The tca configuration in question is:
'zusatzpakete' => [
'label' => 'LLL:EXT:ned_beratung/Resources/Private/Language/locallang_db.xlf:tx_nedberatung_domain_model_beratung.zusatzpakete',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_nedshop_domain_model_artikel',
'multiple' => true,
'maxitems' => 99,
'MM' => 'tx_nedberatung_beratung_zusatzpakete_mm',
'MM_hasUidField' => true,
],
],
In the model, the object is defined as an object storage:
/**
* zusatzpakete
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\NED\NedShop\Domain\Model\Artikel>
* #cascade remove
*/
protected $zusatzpakete = null;
/**
* Adds a Zusatzpaket
*
* #param \NED\NedShop\Domain\Model\Artikel $zusatzpakete
* #return void
*/
public function addZusatzpakete(\NED\NedShop\Domain\Model\Artikel $zusatzpakete)
{
$this->zusatzpakete->attach($zusatzpakete);
}
/**
* Removes a Zusatzpakete
*
* #param \NED\NedShop\Domain\Model\Artikel $zusatzpaketeToRemove The Zusatzpakete to be removed
* #return void
*/
public function removeZusatzpakete(\NED\NedShop\Domain\Model\Artikel $zusatzpaketeToRemove)
{
$this->zusatzpakete->detach($zusatzpaketeToRemove);
}
/**
* Returns the Zusatzpakete
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\NED\NedShop\Domain\Model\Artikel> $zusatzpakete
*/
public function getZusatzpakete()
{
return $this->zusatzpakete;
}
/**
* Sets the Zusatzpakete
*
* #param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\NED\NedShop\Domain\Model\Artikel> $zusatzpakete
* #return void
*/
public function setZusatzpakete(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $zusatzpakete)
{
$this->zusatzpakete = $zusatzpakete;
}
Why does this not work in the frontend, what am I missing?
That's the way the Extbase ObjectStorage works. It retrieves the object hash of each object and uses this to only store objects uniquely. And the Extbase persistence ensures that every object is only created once by storing it in the persistence session after retrieval.
One option to bypass this is to promote your MM table to a 1st level table including TCA and an own domain model. This way each relation is a separate object and thus unique which allows you to have multiple relations to the same Artikel. And example of such a promoted MM table is the sys_file_reference table in TYPO3.
Your Beratung model would then have a multi-valued property (ObjectStorage) typed to the newly introduced relation model. That model then needs a property to represent the related Artikel.
If you do this you can still keep the field names in your MM table if you want to continue maintaining it with the current setup. Then you only need to change your model relations as described. However if you want to have more descriptive names in your table, you can rename the fields but then it cannot be managed as MM table in TYPO3 anymore so you'd need to change your base table field type e.g. to inline.
One final suggestion: it is general consensus to use English for everything in code including model and property names. This makes it a lot easier for others to get into your code and blends well with the English of the programming language.

How to preset data on forms 'HiddenType::class' field with relational data?

I just want to preset hidden "fkCar" field with Car object with the id matching the one in the url pattern. So that when a user clicks on a link next to a car entry, he can directly add history to that car, without the need to select that car id from a drop down list. I can preset data on the dropdown list but whenever I try to use HiddenType in my FormType I get this error message:
"Expected argument of type "AdminBundle\Entity\Car", "string" given"
From what I have noticed it's like that because the instance of the Car Object is converted by __toString() magic method which returns "string" and not Car object anymore. On the other hand, the same thing happens on the dropdown choice field but no error are thrown and it works fine...
When I use ->add('fkCar') in my FormType instead, it works fine but I have a dropdown list which I don't want
when I use HiddenType like so:
->add('fkCar',HiddenType::class, [
// ...
])
I get quoted error message.
This is my code:
My FormType
<?php
namespace AdminBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Intl\DateFormatter\IntlDateFormatter;
class CarHistoryCarIdType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('issueDate', DateType::class,[
'format'=> IntlDateFormatter::LONG,
])
->add('invoiceNum')
->add('invoiceTotal')
->add('quoteNum')
->add('mileage')
->add('description')
// ->add('fkCar') // works - but dropdown choice field is there able to be edited
->add('fkCar',HiddenType::class, [ // generate above error message
// ...
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AdminBundle\Entity\CarHistory'
));
}
}
My controller action:
use AdminBundle\Form\CarHistoryCarIdType;
use AdminBundle\Entity\Car;
use AdminBundle\Entity\CarHistory;
...
/**
* Creates a new History for selected car.
*
* #Route("/new/history/{carId}", name="car_new_history")
* #Method({"GET", "POST"})
*/
public function newHistoryAction(Request $request, $carId)
{
// get car data
$car = $this->getDoctrine()->getRepository(Car::class)->find($carId);
dump($car); // test
dump(get_class($car)); // test
// create History Entity Object
$history = new CarHistory();
// set History Entity fkCar to Car Entity Object with id == $carId
$history->setFkCar($car);
$history->setIssueDate(new \DateTime('now'));
// build form and set data
$form = $this->createForm(CarHistoryCarIdType::class, $history);
dump($request->request->all()); // test
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($history);
$em->flush();
return $this->redirectToRoute('full_car_history', array('id' => $carId));
}
return $this->render('car/newCarHistory.html.twig', array(
'carId' => $carId,
'form' => $form->createView(),
));
}
This is my dump test result. As you can see, the doctrine returns the car object as expected but form 'fkCar' stays empty:
the problem is the hidden type in the form won't allow edit the data inside this. you must change the type and in the view you must hide the field using css. usually I do this:
<div style="display:none">{{form_widget(form.fkCar)}}</div>
change the type in your form class and use the default text type

Check if a specific validation_group is valid in Symfony Form?

I'm building a complex symfony form which is a bit long and contains other embedded forms. Thus the form is displayed in the UI in separate tabs to make it more readable and convenient for the user.
Because the form is long and separated in the UI there is a chance you've missed something while populating it or you just inserted something incorrect. That's when the validation would kick in and stop the form from being saved. The validation itself is configured and works flawlessly.
My problem here is I have a gigantic form, separated in tabs, which has an error somewhere and I need to browse each one of the tabs to see exactly what's wrong. I was thinking to make that specific tab, containing fields with errors, in another color so it could stand out and save you the time of wondering what's wrong and where it is located.
From what I could see, I have two options:
Check all fields per tab, manually, using something like:
{% if not form.children.FIELD_NAME.vars.valid %}
which would take forever to complete and I would do only if it's the only possible way.
Try using validation_groups => array('Default', 'my_tab_name') and logically group the fields for each tab.
I'm really hoping to use the second method, but I can't seem to figure out how to check if the validation group i.e. my_tab_1 contains any errors. I'm aware I can do something like this:
$validator = $this->get('validator');
$my_tab_1 = $validator->validate($entity, null, array('my_tab_1'));
$my_tab_2 = $validator->validate($entity, null, array('my_tab_2'));
$my_tab_3 = $validator->validate($entity, null, array('my_tab_3'));
// so on
But the form is already being validated with $form->validate() and using this approach would trigger N more unnecessary validations.
So the question here is how to check if a specific validation group is valid from a twig template? If that's not possible, can one get it from the Controller and pass it as a variable without doing yet another validation?
I don't think I need to post the FormTypes because they're long, nested and might only confuse you. However, this is an oversimplified version of the parent form:
class CompanyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('address')
->add('representedBy')
->add('category')
->add('phone')
->add('member', new MemberType())
->add('contacts', new ContactType())
->add('notes', new NoteType())
// and a couple more embedded form types.
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'App\FooBundle\Entity\Company',
'cascade_validation' => true
));
}
/**
* #return string
*/
public function getName()
{
return 'app_company';
}
}
If anybody has a better idea or solution, I would really appreciate it.
First you can use tabs in two different ways:
a) With javascript. All the content of the tabs are loaded once and can be found in the source of the page. All tab-content is hidden except one.
b) With links and PHP. In this case every tab is another webpage with another URL.
(hopefully you understand the difference)
I always use the second method for my advanced forms. Thus for each page i only add a part of all the formfields in the formtype. For each page i use one validation group too. This is already enough to EDIT existing entities.
But a problem is a new Entity. You might want to avoid partly filled entities in your database, thus you need to validate and then store every 'step' in the session and after the user has finished last step (and validation was okay) you might want to store all the form-fields in one time into the database.
This method is used by the craueformflowbundle.
To get a part of your formfields simply use a switch in your formType or create a formType for each step.
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CompanyType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
switch ($options['flow_step']) {
case 1:
$builder
->add('company')
->add('origin')
;
break;
case 2:
$builder
->add('contactPerson', NULL, array('label' => 'Volledige naam'))
->add('email', 'email', array('label' => 'Email'))
->add('telephone', NULL, array('label' => 'Telefoonnummer'))
;
break;
}
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Company',
'flow_step' => 1
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_company';
}
}