Sylius: require address in registration - php

I want customers to fill in their address right at the registration, not at the checkout of an first order.
I see, that Sylius\Component\Core\Model\Customer has attributes $defaultAddress: AddressInterface and $addresses: Collection|AddressInterface[].
This is where I stucked. If there would be singular ($address: AddressInterface) I would know, I should extend form type and add there address field.
But how to require user to fill in exactly one address into this collection?
I tried this:
My form type used for registration (where parent is Sylius\Bundle\CoreBundle\Form\Type\Customer\CustomerRegistrationType):
->add(
'addresses',
CollectionType::class,
[
'entry_type' => WholesaleCustomerAddressType::class
]
)
Where WholesaleCustomerAddressType is child of Sylius\Bundle\AddressingBundle\Form\Type\AddressType and remove some fields (name, phone, etc.)
How it works:
The address field at the registration page is not rendered (only empty div is). When I dump form.adresses I see it has no children.
It renders only this:
<div data-form-type="collection" id="wholesale_customer_registration_addresses" class="form-control controls collection-widget" placeholder="Addresses"><div data-form-collection="list"></div></div>
How it should work:
The form should render fields for exactly one address. After click submit, the user should be registred, should has exactly one address and this address should be $defaultAddress also.
I see the problem is in the thing, that at the time of registration the collection of addresses is empty. How to add in Sylius a first record?

Might be a little late but if someone else stumbles on your question, this is how I solved it...
As you pointed out yourself there is an attribute $defaultAddress: AddressInterface. Simply add this field to your custom form or in my case the form extension and assign the appropriate type. I used the Sylius\Bundle\AddressingBundle\Form\Type\AddressType and that's it.
My Form extension:
<?php
namespace App\Form\Extension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractTypeExtension;
use Sylius\Bundle\AddressingBundle\Form\Type\AddressType;
use Sylius\Bundle\CoreBundle\Form\Type\Customer\CustomerRegistrationType;
final class CustomerRegistrationTypeExtension extends AbstractTypeExtension
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// Adding the defaultAddress as extra fields to the form
$builder->add('defaultAddress', AddressType::class, [
'label' => 'sylius.form.customer.default_address',
]);
}
/**
* {#inheritdoc}
*/
public function getExtendedTypes(): array
{
return [CustomerRegistrationType::class];
}
}
Don't forget to add the extension as a service:
app.form.extension.type.customer_registration:
class: App\Form\Extension\CustomerRegistrationTypeExtension
tags:
- { name: form.type_extension, extended_type: Sylius\Bundle\CustomerBundle\Form\Type\CustomerRegistrationType }
And don't forget to render the new field in tour template:
{{ form_row(form.defaultAddress) }}

Related

"Unable to reverse value for property path" for Symfony2 form ChoiceType field on Select2

Long story short, in Symfony 2.8 I've got Movie entity with actors field, which is ArrayCollection of entity Actor (ManyToMany) and I wanted the field to be ajax-loaded Select2.
When I don't use Ajax, the form is:
->add('actors', EntityType::class, array(
'class' => Actor::class,
'label' => "Actors of the work",
'multiple' => true,
'attr' => array(
'class' => "select2-select",
),
))
And it works.
I tried to put there an empty Select field:
->add('actors', ChoiceType::class, array(
'mapped' => false,
'multiple' => true,
'attr'=>array(
'class' => "select2-ajax",
'data-entity'=>"actor"
)
))
The Select2 Ajax works, everything in DOM looks the same as in previous example, but on form submit I get errors in the profiler: This value is not valid.:
Symfony\Component\Validator\ConstraintViolation
Object(Symfony\Component\Form\Form).children[actors] = [0 => 20, 1 => 21]
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "actors": Could not find all matching choices for the given values
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Could not find all matching choices for the given values
The funny part is the data received is the same as they were when it was an EntityType: [0 => 20, 1 => 21]
I marked field as not mapped, I even changed field name to other than Movie entity's field name. I tried adding empty choices, I tried to leave it as EntityType but with custom query_builder, returning empty collection. Now I'm out of ideas.
How should I do it?
EDIT after Raymond's answer:
I added DataTransformer:
use Doctrine\Common\Persistence\ObjectManager;
use CompanyName\Common\CommonBundle\Entity\Actor;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class ActorToNumberTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $objectManager)
{
$this->manager = $objectManager;
}
public function transform($actors)
{
if(null === $actors)
return array();
$actorIds = array();
foreach($actors as $actor)
$actorIds[] = $actor->getId();
return $actorIds;
}
public function reverseTransform($actorIds)
{
if($actorIds === null)
return array();
foreach($actorIds as $actorId)
{
$actor = $this->manager->getRepository('CommonBundle:Actor')->find($actorId);
if(null === $actor)
throw new TransformationFailedException(sprintf('An actor with id "%s" does not exist!', $actorId));
$actors[] = $actor;
}
return $actors;
}
}
Added it at the end of the MovieType buildForm():
$builder->get('actors')
->addModelTransformer(new ActorToNumberTransformer($this->manager));
$builder->get('actors')
->addViewTransformer(new ActorToNumberTransformer($this->manager));
And added service:
common.form.type.work:
class: CompanyName\Common\CommonBundle\Form\Type\MovieType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
Nothing changed. On form submit, reverseTransform() gets the proper data, but profiler shows the same error. That's a big mistery for me now...
You'll need to add a DTO (Data Transformer ) to transform the value received from your form and return the appropriate object .
Since you're calling the value from Ajax it doesn't recognized it anymore as a an object but a text value.
Examples :
Symfony2 -Use of DTO
Form with jQuery autocomplete
The correct way isn't Data Transformer but Form Events, look here:
http://symfony.com/doc/current/form/dynamic_form_modification.html#form-events-submitted-data
In the example you have the field sport (an entity, like your Movie) and the field position (another entity, like actors).
The trick is to use ajax in order to reload entirely the form and use
PRE_SET_DATA and POST_SUBMIT.
I'm using Symfony 3.x but I think it's the same with 2.8.x
When you add data transformers and nothing seems to change, it sounds like the data never goes through your data transformers. The transformation probably fails before your new data transformers are called. Try to add a few lines to your code:
$builder->get('actors')->resetViewTransformers();
$builder->get('actors')->resetModelTransformers();
// and then add your own

In Symfony, how to show and submit part of a form type

I have form type that contains multiple fields. One field is a custom form type that itself contains multiple fields as well. I cannot change the child type because it is in use elsewhere.
I only want to show the fields that are currently not empty but I haven't found a way to do so. I tried hiding them on the twig template - as in, not rendering them if the value is not empty - but then they get submitted as empty so previously set values are now unset.
How can I effectively use part of a Form Type?
Editing for clarification:
Say I have a class OrderType with the fields orderNumber and identifiers. The identifiers is a ItentifiersType and contains the fields invoiceNumber, name, purchaseDate and serialNumber.
The page I'm trying to create should only show the missing data. If the values for orderNumber and purchaseDate are already present in the database, I only want to show the input fields for invoiceNumber and serialNumber.
$builder->add('identifiers', 'collection', [
'type' => new IdentifierType(),
'allow_add' => true
])->add('orderNumber', 'int');
Here is how I solved my problem. It was kind of awkward to reach the fields inside the identifiers field but it works and form handling works as expected.
/* Removed non-empty fields from the form */
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
$identifiers = $form->get('identifiers');
foreach ($identifiers->all()[0]->all() as $identifier) {
if ($identifier->getData() != null) {
$identifiers->all()[0]->remove($identifier->getName());
}
}
});

Symfony, forms and many to one

We're running into a small code-design smell with symfony and our forms. It is not a problem per se, but makes me wonder if we could attain our goals any other way.
For the sake of simplicity, let me briefly explain a setup: let "Product" be an entity that represents a product in a database, meant to be sold in an online store. Since the online store is designed to have several languages in it, every single bit of information that could be related to a language is in the entity "Product_descriptions" that is related in a manyToOne fashion to the "Product". Finally we have designed a "Language" entity, representing every single language the user can see the store in.
As you can imagine, the code is pretty standard stuff:
class Language
{
private $language_id;
private $language_name;
private $language_code;
//Some other stuff.
};
class Product
{
private $product_id;
private $product_reference;
private $product_weight;
private $product_descriptions; //As an arrayCollection of "Product_description" objects.
//Some other stuff.
};
class Product_description
{
private $product_description_id;
private $product_name;
private $product_long_description;
private $product_short_description;
private $product; //A reference to the Product itself.
private $language; //A reference to the language this is meant to be seen in.
};
Okay, now for the problem itself. The setup, as expected, works wonderfully. It is in the backend where the smell resides.
To create new products we have designed a symfony form Type. In the same form we would like to be able to set all the product information as well as the information for every possible language. The smell comes in when we need to feed all possible "Language"s to the form type, check if a "Product_description" exists for a "Language" and "Product", show the empty text field (in case it does not exist) or the filled field... Our solution requests that a repository for all languages is injected into the form . Let me show you how it goes (please, take into consideration that this is not the real code... something may be missing):
class ProductType extends AbstractType
{
private $language_repo;
public function __construct($r)
{
$this->language_repo=$r;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('product_name', 'text')
->add('product_code', 'text');
$product=$builder->getData();
//We retrieve all languages here, to check if an entry for that
//language exists and show its data.
$languages=$this->language_repo->findAll();
foreach($languages as $key => &$lan)
{
//Here we look for existing data... This will return null if there's none.
$product_description=$product->get_description_for_language($lan);
$default_name=$product_description ? $product_description->getProductName() : '';
$default_long=$product_description ? $product_description->getProductLongDescription() : '';
$default_short=$product_description ? $product_description->getProductShortDescription() : '';
//Here we manually create the name_#language_id# form data... That we will retrieve later.
$builder->add('name_'.$lan->getLanguageId(), 'text', array(
'label' => 'Name for '.$lan->getName(),
'mapped' => false,
'data' => $default_name))
->add('long_'.$lan->getLanguageId(), 'text', array(
'label' => 'Name for '.$lan->getName(),
'mapped' => false,
'data' => $default_long))
->add('short_'.$lan->getLanguageId(), 'text', array(
'label' => 'Name for '.$lan->getName(),
'mapped' => false,
'data' => $default_short));
}
$builder->add('save', 'submit', array('label' => 'Save data'));
}
//And some other stuff here.
}
As you can see, we are manually setting some data keys that we need to retrieve later in the controller. The setup works, of course. Any new language will yield an empty form field. Any existing language shows the related information.
Now for the controller, this gets messier even... When we're submitting the form we go like this:
private function process_form_data(Form &$f, Product &$item, Request &$request)
{
//Find all languages...
$em=$this->getDoctrine()->getManager();
$languages=$em->getRepository("MyBundle:Language")->findAll();
//Get submitted data for that language..
foreach($languages as $key => &$lan)
{
$name_language=$f->get('name_'.$lan->getLanguageId())->getData();
$long_language=$f->get('long_'.$lan->getLanguageId())->getData();
$short_language=$f->get('short_'.$lan->getLanguageId())->getData();
//Check if the language entry exists... Create it, if it doesn't. Feed the data.
$product_description=$product->get_description_for_language($lan);
if(!$product_description)
{
$product_description=new Product_description();
$product_description->setLanguage($lan);
$product_description->setProduct($product);
}
$product_description->setName($name_language);
$product_description->setLongDescription($long_language);
$product_description->setShortDescription($short_language);
$em->persist($product_description);
}
//Do the product stuff, persist, flush, generate a redirect...Not shown.
}
It works, but seems to me that is not the "symfony" way of doing things. How would you do this?. Have you found a more elegant approach?.
Thanks a lot.
I think you should revisit the way you translate the entities...
An existing way is to use the DoctrineExtensionBundle, translatable to be precise...
You'll find more info here :
https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/translatable.md
Here is an extract to see how it can work :
<?php
// first load the article
$article = $em->find('Entity\Article', 1 /*article id*/);
$article->setTitle('my title in de');
$article->setContent('my content in de');
$article->setTranslatableLocale('de_de'); // change locale
$em->persist($article);
$em->flush();
( now the article has a german translation )

Symfony2: How to use constraints on custom compound form type?

Here is a question I've been breaking my head over for a while now.
Please know that I'm not a Symfony2 expert (yet), so I might have made a rookie mistake somewhere.
Field1: Standard Symfony2 text field type
Field2: Custom field type compoundfield with text field + checkbox field)
My Goal: Getting constraints added to the autoValue field to work on the autoValue's text input child
The reason why the constraints don't work is probably because NotBlank is expecting a string value and the internal data of this form field is an array array('input'=>'value', 'checkbox' => true). This array value gets transformed back into a string with a custom DataTransformer. I suspect however that that happens AFTER validating the field against known constraints.
As you see below in commented code, I have been able to get constraints working on the text input, however only when hardcoded into the autoValue's form type, and I want to validate against the main field's constraints.
My (simplified) sample code for controller and field:
.
Controller code
Setting up a quick form for testing purposes.
<?php
//...
// $entityInstance holds an entity that has it's own constraints
// that have been added via annotations
$formBuilder = $this->createFormBuilder( $entityInstance, array(
'attr' => array(
// added to disable html5 validation
'novalidate' => 'novalidate'
)
));
$formBuilder->add('regular_text', 'text', array(
'constraints' => array(
new \Symfony\Component\Validator\Constraints\NotBlank()
)
));
$formBuilder->add('auto_text', 'textWithAutoValue', array(
'constraints' => array(
new \Symfony\Component\Validator\Constraints\NotBlank()
)
));
.
TextWithAutoValue source files
src/My/Component/Form/Type/TextWithAutoValueType.php
<?php
namespace My\Component\Form\Type;
use My\Component\Form\DataTransformer\TextWithAutoValueTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class TextWithAutoValueType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('value', 'text', array(
// when I uncomment this, the NotBlank constraint works. I just
// want to validate against whatever constraints are added to the
// main form field 'auto_text' instead of hardcoding them here
// 'constraints' => array(
// new \Symfony\Component\Validator\Constraints\NotBlank()
// )
));
$builder->add('checkbox', 'checkbox', array(
));
$builder->addModelTransformer(
new TextWithAutoValueTransformer()
);
}
public function getName()
{
return 'textWithAutoValue';
}
}
src/My/Component/Form/DataTransformer/TextWithAutoValueType.php
<?php
namespace My\Component\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
class TextWithAutoValueTransformer
implements DataTransformerInterface
{
/**
* #inheritdoc
*/
public function transform($value)
{
return array(
'value' => (string) $value,
'checkbox' => true
);
}
/**
* #inheritdoc
*/
public function reverseTransform($value)
{
return $value['value'];
}
}
src/My/ComponentBundle/Resources/config/services.yml
parameters:
services:
my_component.form.type.textWithAutoValue:
class: My\Component\Form\Type\TextWithAutoValueType
tags:
- { name: form.type, alias: textWithAutoValue }
src/My/ComponentBundle/Resources/views/Form/fields.html.twig
{% block textWithAutoValue_widget %}
{% spaceless %}
{{ form_widget(form.value) }}
{{ form_widget(form.checkbox) }}
<label for="{{ form.checkbox.vars.id}}">use default value</label>
{% endspaceless %}
{% endblock %}
.
Question
I have been reading docs and google for quite some hours now and can't figure out how to copy, bind, or reference the original constraints that have been added while building this form.
-> Does anyone know how to accomplish this?
-> For bonus points; how to enable the constraints that have been added to the main form's bound entity? (via annotations on the entity class)
PS
Sorry it became such a long question, I hope that I succeeded in making my issue clear. If not, please ask me for more details!
I suggest you read again the documentation about validation first.
What we can make out of this is that validation primarily occurs on classes rather than form types. That is what you overlooked. What you need to do is:
To create a data class for your TextWithAutoValueType, called src/My/Bundle/Form/Model/TextWithAutoValue for instance. It must contain properties called text and checkbox and their setters/getters;
To associate this data class to your form type. For this, you must create a TextWithAutoValueType::getDefaultOptions() method and populate the data_class option. Go here for more info about this method;
Create validation for your data class. You can either use annotations or a Resources/config/validation.yml file for this. Instead of associating your constraints to the fields of your form, you must associate them to the properties of your class:
validation.yml:
src/My/Bundle/Form/Model/TextWithAutoValue:
properties:
text:
- Type:
type: string
- NotBlank: ~
checkbox:
- Type:
type: boolean
Edit:
I assume that you already know how to use a form type in another. When defining your validation configuration, you can use a very useful something, called validation groups. Here a basic example (in a validation.yml file, since I'm not much proficient with validation annotations):
src/My/Bundle/Form/Model/TextWithAutoValue:
properties:
text:
- Type:
type: string
groups: [ Default, Create, Edit ]
- NotBlank:
groups: [ Edit ]
checkbox:
- Type:
type: boolean
There is a groups parameter that can be added to every constraint. It is an array containing validation group names. When requesting a validation on an object, you can specify with which set of groups you want to validate. The system will then look in the validation file what constraints should be applied.
By default, the "Default" group is set on all constraints. This also is the group that is used when performing a regular validation.
You can specify the default groups of a specific form type in MyFormType::getDefaultOptions(), by setting the validation_groups parameter (an array of strings - names of validation groups),
When appending a form type to another, in MyFormType::buildForm(), you can use specific validation groups.
This, of course, is the standard behaviour for all form type options. An example:
$formBuilder->add('auto_text', 'textWithAutoValue', array(
'label' => 'my_label',
'validation_groups' => array('Default', 'Edit'),
));
As for the use of different entities, you can pile up your data classes following the same architecture than your piled-up forms. In the example above, a form type using textWithAutoValueType will have to have a data_class that has a 'auto_text' property and the corresponding getter/setter.
In the validation file, the Valid constraints will be able to cascade validation. A property with Valid will detect the class of the property and will try to find a corresponding validation configuration for this class, and apply it with the same validation groups:
src/My/Bundle/Form/Model/ContainerDataClass:
properties:
auto_text:
Valid: ~ # Will call the validation conf just below with the same groups
src/My/Bundle/Form/Model/TextWithAutoValue:
properties:
... etc
As described here https://speakerdeck.com/bschussek/3-steps-to-symfony2-form-mastery#39 (slide 39) by Bernhard Schussek (the main contributor of symofny form extension), a transformer should never change the information, but only change its representation.
Adding the information (checkbox' => true), you are doing something wrong.
In
Edit:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('value', 'text', $options);
$builder->add('checkbox', 'checkbox', array('mapped'=>false));
$builder->addModelTransformer(
new TextWithAutoValueTransformer()
);
}

Validating multiple optional form fields with Zend Framework

I am trying to validate Zend_Form which has several optional fields and I want at least one of them to be filled in. In my case I have mobile, home and office phone numbers and I want at least one of them to be provided.
I am trying to achieve this though Validation Context (as suggested here) by creating custom validator which extends Zend_Validate_Abstract. The problem is that if all optional fields are empty they are missing from the form $context (passed to the validator class) and this way not validated at all.
So if you fill any or several of the three options (mobile, home, work) they are all going to be validated (which is fine, but for this no custom validator is needed), but if you fill none of them, there is no option to force the customer to fill at least one of the fields (which is my aim).
Here is what I have:
1. my form
<?php
class Application_Form_Application extends Zend_Form {
public function init() {
$this->setName('application');
// attach sub forms to main form
$this->addSubForms(array(
'application' => $this->application(),
...
));
}
private function application() {
$application = new Zend_Form_SubForm();
// custom phone validation
$phone_validation = array('phone_mobile' => 'Mobile', 'phone_home' => 'Home', 'phone_work' => 'Work');
// phone mobile
$app['phone_mobile'] = new Zend_Form_Element_Text('phone_mobile');
$app['phone_mobile']->setLabel('Mobile')
->addFilter('StripTags')
->addFilter('StringTrim')
->addValidator('Regex', false, array('/^[0-9]{8}$/i'))
->addValidator(new Application_Form_PhoneMobileHomeWork($phone_validation), false);
// phone home
$app['phone_home'] = new Zend_Form_Element_Text('phone_home');
$app['phone_home']->setLabel('Home')
->addFilter('StripTags')
->addFilter('StringTrim')
->addValidator('Regex', false, array('/^[0-9]{8}$/i'))
->addValidator(new Application_Form_PhoneMobileHomeWork($phone_validation), false);
// phone work
$app['phone_work'] = new Zend_Form_Element_Text('phone_work');
$app['phone_work']->setLabel('Work')
->addFilter('StripTags')
->addFilter('StringTrim')
->addValidator('Regex', false, array('/^[0-9]{8}$/i'))
->addValidator(new Application_Form_PhoneMobileHomeWork($phone_validation), false);
$application->AddElements($app);
}
}
?>
2. custom validator
<?php
class Application_Form_PhoneMobileHomeWork extends Zend_Validate_Abstract {
const NOT_PRESENT = 'notPresent';
protected $_messageTemplates = array(
self::NOT_PRESENT => 'At least one contact phone shall be provided!'
);
protected $_listOfFields;
public function __construct(array $listOfFields) {
$this->_listOfFields = $listOfFields;
var_dump($listOfFields);exit;
}
public function isValid($value, $context = null) {
var_dump($context);exit;
...
}
?>
The validator always passes though the first dump ($listOfFields), but if I remove it, isValid() is never called unless some data is typed into some of the phone fields (which we want to prevent).
When I checked further I found a solution in extending the Zend_Validate class by passing empty fields to the $context parameter, but I would like to have a better solution if someone knows any.
Concluding it in short - how to validate certain form, forcing the user to fill at least one out of several optional fields?
If I understand you right, you want your form elements to not be required, but prevent them to be empty (except if one of them is not empty) using a custom validator? Then, in order to not skip the validation chain, you need to prevent them to be empty calling the method setAllowEmpty(false) in each of your elements.
Finally, in your custom validator, you will have something like this:
foreach ($this->_listOfFields as $field) {
if (isset($context[$field]) AND $context[$field])
{
return true;
}
}
Also, make sure your elements are not required (setRequired(false)).
The problem is that if any field is filled, the multi_checkbox element doesn't exists in the form, and then it won't be validated.
One solution is the follow:
Use a hidden option always checked and validate that this always is checked this more one of the others.

Categories