symfony2: multiple entities one form - symfony

I have 2 entities:
ADS\LinkBundle\Entity\Link:
type: entity
table: null
repositoryClass: ADS\LinkBundle\Entity\LinkRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
dateAdded:
type: datetime
expirationDate:
type: datetime
nullable: true
designator:
type: string
length: 255
nullable: false
unique: true
slug:
type: string
length: 255
nullable: true
unique: true
manyToOne:
company:
targetEntity: ADS\UserBundle\Entity\Company
inversedBy: link
joinColumn:
name: company_id
referencedColumnName: id
nullable: true
createdBy:
targetEntity: ADS\UserBundle\Entity\User
inversedBy: link
joinColumn:
name: createdBy_id
referencedColumnName: id
domain:
targetEntity: ADS\DomainBundle\Entity\Domain
inversedBy: link
joinColumn:
name: domain_id
referencedColumnNames: id
oneToMany:
paths:
targetEntity: ADS\LinkBundle\Entity\Path
mappedBy: link
cascade: [persist]
lifecycleCallbacks: { }
and
ADS\LinkBundle\Entity\Path:
type: entity
table: null
repositoryClass: ADS\LinkBundle\Entity\PathRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
pathAddress:
type: string
length: 255
pathWeight:
type: string
length: 255
manyToOne:
link:
targetEntity: ADS\LinkBundle\Entity\Link
inversedBy: paths
joinColumn:
name: link_id
referencedColumnName: id
lifecycleCallbacks: { }
I have everything figured out except for the paths portion of the entity. This is for an A/B split test, so each link can have 2 paths. Each path will consist of a web address, and a number ( 0 - 100 )
Here is my form in it's current state:
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PathType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('pathAddress')
->add('pathWeight')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Path'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
and
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LinkType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('designator')
->add('domain', 'entity', array(
'class' => 'ADS\DomainBundle\Entity\Domain',
'property' => 'domainAddress'
))
->add('paths', 'collection', array('type' => new PathType(), 'allow_add' => true))
->add('Submit', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Link'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
What I need to figure out, is when creating a link, I need to also be able to create the correct path and weight to go with it. The paths won't be in the database before a link is created.
Here is what I have for my controller:
public function newAction(Request $request) {
$entity = new Link();
$form = $this->createForm(new LinkType(), $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$code = $this->get('ads.default');
$em = $this->getDoctrine()->getManager();
$user = $this->getUser();
$entity->setDateAdded(new \DateTime("now"));
$entity->setCreatedBy($user);
$entity->setSlug($code->generateToken(5));
$entity->setCompany($user->getParentCompany());
$em->persist($entity);
$em->flush();
return new Response(json_encode(array('error' => '0', 'success' => '1')));
}
return new Response(json_encode(array('error' => count($form->getErrors()), 'success' => '0')));
}
return $this->render('ADSLinkBundle:Default:form.html.twig', array(
'entity' => $entity,
'saction' => $this->generateUrl('ads.link.new'),
'form' => $form->createView()
));
}

Thanks to #Onema ( read the comments above ), I've figured this out. By reading the documentation at http://symfony.com/doc/current/cookbook/form/form_collections.html It gave me information I needed to get this done.
First step in doing what I needed to do, was to create a new form type called PathsType.php which houses the fields associated with the Paths Entity
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PathType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('pathAddress')
->add('pathWeight')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Path'));
}
public function getName() { return 'ads_linkbundle_path'; }
}
Then modifying the LinkType.php to utilize this new form
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LinkType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('designator')
->add('domain', 'entity', array(
'class' => 'ADS\DomainBundle\Entity\Domain',
'property' => 'domainAddress'
))
->add('paths', 'collection', array(
'type' => new PathType(),
'allow_add' => true,))
->add('Submit', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Link'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
The addition of allow_add makes it so that you can add multiple instances of that form.
Within the view, I now utilize the data-prototype attribute. In the documentation, it has the example using a list item - so that's where I started.
<ul class="tags" data-prototype="{{ form_widget(form.paths.vars.prototype)|e }}"></ul>
Then came the jQuery functions ( listed on the documentation link above, simple copy/paste will work )
This got the system working, with 1 small issue and that in my paths entity, I have a relationship to the Link entity but it was not noticing this relationship and had the link_id field as null
To combat this, we edit LinkType.php one more time, and add by_reference = false to the collection definition. We then edit the addPath method inside the entity to look like so:
public function addPath(\ADS\LinkBundle\Entity\Path $paths)
{
$paths->setLink($this);
$this->paths->add($paths);
}
This sets the current link object, as the link the path is associated with.
At this point, the system is working flawlessly. It's creating everything that it needs to, only need to adjust the display a little bit. I personally opted to use a twig macro to modify the html output contained in data-prototype
my macro as it currently sits (incomplete - but working ) which I added to the beginning of my form.html.twig
{% macro path_prototype(paths) %}
<div class="form-group col-md-10">
<div class="col-md-3">
<label class="control-label">Address</label>
</div>
<div class="col-md-9">
{{ form_widget(paths.pathAddress, { 'attr' : { 'class' : 'form-control required' }}) }}
</div>
</div>
{% endmacro %}
In the HTML for the form itself, I removed the list creation, and replaced it with:
<div class="form-group">
{{ form_label(form.paths,'Destination(s)', { 'label_attr' : {'class' : 'col-md-12 control-label align-left text-left' }}) }}
<div class="tags" data-prototype="{{ _self.path_prototype(form.paths.vars.prototype)|e }}">
</div>
</div>
I then modified my javascript to use the div as a starting point instead of the ul in the example.
<script type="text/javascript">
var $collectionHolder;
// setup an "add a tag" link
var $addTagLink = $('Add Another Destination');
var $newLinkLi = $('<div></div>').append($addTagLink);
jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
$collectionHolder = $('div.tags');
// add the "add a tag" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
addTagForm($collectionHolder, $newLinkLi);
$addTagLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addTagForm($collectionHolder, $newLinkLi);
});
});
function addTagForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
console.log(index);
if (index == 1) {
console.log('something');
$('a.add_tag_link').remove();
}
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = newForm;
$newLinkLi.before($newFormLi);
}
</script>
Being that these paths are destination addresses for an A/B split test within my marketing app, I opted to limit the paths to 2 per link. And with this, I have successfully setup a form to use a collections type.

Related

Sylius Grid EntityFilter - sort or use repository

I added a large entity filter to one of my Sylius grid configurations. I haven't found any configuration options besides class name and from the looks of it, option values are just ordered by ID. Is there a way to use a repository method or at least provide sort field? Do I need to use a custom filter for this?
You can define what repository method to use on your YAML file and what arguments you want to send to that method:
sylius_grid:
grids:
app_user: # Your grid name
driver:
name: doctrine/orm
options:
class: "%app.model.user%"
repository:
method: myCustomMethod
arguments:
id: resource.id
sorting:
name: asc
limits: [10, 25, 50, 100]
Check the Sylius Grid Bundle documentation for more information: Configuration Reference
Here's how to define a new type :
final class CustomFilterType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'shop',
EntityType::class,
[
'class' => Shop::class,
'label' => false,
'multiple' => true, //if you need multiple selection
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('s');
// define your query builder here
},
'choice_label' => function ($shop) { /** #var $shop Shop */
return $shop->getName();
},
]
);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix(): string
{
return 'sylius_grid_filter_entity_multiple';
}
}
Then in your services.yaml :
app.grid_filter.custom_filter:
class: Sylius\Component\Grid\Filter\SelectFilter
tags: [{
name: 'sylius.grid_filter',
type: 'custom_type',
form_type: App\Form\Grid\Filter\CustomFilterType
}]
And finally, in your grid definition:
filters:
shops:
type: custom_type
label: app.ui.shops

Sylius EntityFilter choice from a part of resources

I used a Sylius 1.0.0-beta1 and ported EntityFilter from dev-master due to lack of this functionality in last stable version. Everything works fine but is there any way of choosing not from all resources but only from part of them?
I need to make a filter based on Taxonomies. I have a few taxons which are city names and all of them have parent taxon called City (code: city). So I want to display in that filter all children of city taxon.
My grid configuration is shown below:
sylius_grid:
grids:
smartbyte_admin_products_by_event_archetype:
...
filters:
...
taxon:
type: app_entity
options:
fields: [taxon.id]
class: "%sylius.model.taxon.class%"
city:
type: app_taxon
The first filter from configuration works and filters fine, except it takes all taxons, but I need to show only some.
I tried also make my own filter (the second one) but I get a text field instead of entity field in filter. Following the docs I created custom one. Here is my try:
<?php
namespace SyliusExtensionBundle\Form\Type\Filter;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class TaxonFilterType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('city', EntityType::class, array(
'class' => 'Sylius\Component\Core\Model\Taxon',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('t')
->leftJoin('t.parent', 'taxon')
->where("taxon.code = 'city'");
},
'label' => 'Miasto',
'required' => false
));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'label' => false,
'placeholder' => 'sylius.ui.all',
])
->setDefined('fields')
->setAllowedTypes('fields', 'array')
;
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'sylius_grid_filter_taxon';
}
}
Service configuration:
services:
sylius.grid_filter.entity:
class: SyliusExtensionBundle\Grid\Filter\EntityFilter
tags:
- { name: sylius.grid_filter, type: app_entity, form-type: SyliusExtensionBundle\Form\Type\Filter\EntityFilterType }
- { name: sylius.grid_filter, type: app_taxon, form-type: SyliusExtensionBundle\Form\Type\Filter\EntityFilterType }
sylius.form.type.grid_filter.entity:
class: SyliusExtensionBundle\Form\Type\Filter\EntityFilterType
tags:
- { name: form.type, alias: sylius_grid_filter_entity }
app.form.type.grid_filter.taxon:
class: SyliusExtensionBundle\Form\Type\Filter\TaxonFilterType
tags:
- { name: form.type, alias: sylius_grid_filter_taxon }
And lastly filter templates:
sylius_grid:
templates:
filter:
app_entity: "SyliusExtensionBundle:Grid/Filter:entity.html.twig"
app_taxon: "SyliusExtensionBundle:Grid/Filter:entity.html.twig"
Please guide my how can I restrict EntityFilter or how to make the custom filter work. I spent many hours on this subject and cannot see whereis the error.
Current effect below:
EDIT:
Current TaxonFilterType according to Paweł Jędrzejewski tips. Still doesn't work and dont detect fields option in configuration.
<?php
/**
* Created by PhpStorm.
* User: Krzysztof Wędrowicz krzysztof#wedrowicz.me
* Date: 23.01.17
* Time: 14:56
*/
namespace SyliusExtensionBundle\Form\Type\Filter;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class TaxonFilterType extends AbstractType {
public function getParent()
{
return EntityType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'label' => false,
'placeholder' => 'sylius.ui.all',
'class' => 'Sylius\Component\Core\Model\Taxon',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('t')
->leftJoin('t.parent', 'taxon')
->where("taxon.code = 'city'")
;
},
'required' => false
])
->setDefined('fields')
->setAllowedTypes('fields', 'array')
;
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'sylius_grid_filter_city';
}
}
Currently this is not possible via configuration. I will add it to the backlog, but not sure when it can be implemented. That being said, your custom filter is a good idea. You should do a small change and it should work: The form type should have EntityType::class in getParent() instead of using the buildForm method. And the custom query builder should be configured in configureOptions method, then it will render a proper field. Here is full code that should work:
<?php
namespace AcmeExtension\Form\Type\Filter;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class CityFilterType extends AbstractType
{
public function getParent()
{
return EntityType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'label' => false,
'placeholder' => 'sylius.ui.all',
'class' => 'Sylius\Component\Core\Model\Taxon',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('t')
->leftJoin('t.parent', 'taxon')
->where("taxon.code = 'city'")
;
},
'required' => false
])
->setDefined('fields')
->setAllowedTypes('fields', 'array')
;
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'sylius_grid_filter_city';
}
}

choice field symfony "Notice: Array to string conversion "

Trying to validate a choice field (multiple checkboxes) Im having this problem:
"Notice: Array to string conversion "
My validation file looks like this one:
Cgboard\AppBundle\Forms\UploadImageEntity:
properties:
image:
...
cgnetworks:
- Choice:
choices: [flickr, tumblr] //<--- this is giving me problems!!!
My form entity class (Im not going to save this to db for now):
class UploadImageEntity {
public $image;
public $cgnetworks;
}
And my form class:
class UploadImageForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('image', 'file')
->add('cgnetworks', 'choice', [
'choices' => $this->getCgNetworks(),
'multiple' => TRUE,
'expanded' => TRUE
]
);
}
public function getCgNetworks()
{
return [
'tumblr' => 'Tumblr',
'flickr' => 'Flickr'
];
}
}
Any idea?
perhaps you need to specify multiple in your validation
cgnetworks:
- Choice:
choices: [flickr, tumblr] //<--- this is giving me problems!!!
multiple: true
Check your Entity field getter. If you have something else instead of
public function getValue(){
return $this->value;
}
You can reach this error.
Form builder uses get and set entity methods, that's why you need to return an allowable value.

Symfony2 Forms: automatic register block in forms

I have some form and if user isn't logged in yet i want to show some additional fields (like email, username) in this form and validate it.
Now i do it in that way:
Create new FormType:
class QuickRegisterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
->add('username')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Entity\User',
'validation_groups' => array(
'QuickRegister'
),
));
}
public function getName()
{
return 'quick_register';
}
}
In Controller:
$user = $this->getUser();
$formBuilder = $this->createFormBuilder($message, array(
'cascade_validation' => true
));
$formBuilder->add('body', 'textarea');
if (!$user) {
$formBuilder->add('quick_register', new QuickRegisterType(), array(
'property_path' => 'sender'
));
}
In template:
{% if form.quick_register is defined %}
{# render this fields #}
{% endif %}
And just after $form->bind($request); i get valid user in $message->getSender(); Everything is good! But i need to perform some other actions just after form validation:
if ($form->isValid()) {
$sender = $message->getSender();
if (!$sender->getId()) {
// perform some work with just registered user
// like sending email, generate passwords, so on
}
}
And this actions will be generally the same for such form from all project, so i want to optimize it.
At first i think i can create new service in DI container and just call it like
$this->container->get('my.user_service')->afterQuickRegister($sender);
But now i read about Form events in symfony2 on page http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html
I don't understand right now, can i use it for my purposes? Can i create such form subscriber in DI container? And generally, can i somehow change User just after creation? What is right form event for this? Thanks!
I implement it with FormEvents.
form.type.quick_register:
class: Acme\UserBundle\Form\Type\QuickRegisterType
arguments: [ #form.type.quick_register.subscriber ]
tags:
- { name: form.type, alias: quick_register }
And pass subscriber by form.type.quick_register.subscriber service. In subscriber i check form validation in POST_BIND event and do my stuff. Great!

Symfony2 FOSUserBundle override profile form : field form empty?

I overrided registration form from FOSUserBundle with additionals fields: it works well.
When I apply the same logic to override Profile Form : the form appears well with my additionals fields but all is empty (the fields do not contain values ​​of the connected user).
Note: when I use the default form from the bundle the profile form contains the values ​​of the connected user.
Is there a specific action compared to override the registration form to retrieve the values ​​of the connected user ?
HERE IS CODE :
src/Vn/UserBundle/Resources/config/services.yml
services:
...
vn_user.profile.form.type:
class: Vn\UserBundle\Form\Type\ProfileFormType
arguments: [%fos_user.model.user.class%]
tags:
- { name: form.type, alias: vn_user_profile }
vn_user.form.handler.profile:
class: Vn\UserBundle\Form\Handler\ProfileFormHandler
arguments: ["#fos_user.profile.form", "#request", "#fos_user.user_manager"]
scope: request
public: false
symfony/app/config/config.yml
fos_user:
...
profile:
form:
type: vn_user_profile
handler: vn_user.form.handler.profile
src/Vn/UserBundle/Form/Type/ProfileFormType.php
namespace Vn\UserBundle\Form\Type;
use Symfony\Component\Form\FormBuilder;
use FOS\UserBundle\Form\Type\ProfileFormType as BaseType;
class ProfileFormType extends BaseType
{
public function buildUserForm(FormBuilder $builder, array $options)
{
parent::buildUserForm($builder, $options);
// custom field
$builder->add('profile',new MyProfileFormType(),array(
'label' => 'PROFILE'
));
}
public function getName()
{
return 'vn_user_profile';
}
}
src/Vn/UserBundle/Form/Type/MyProfileFormType.php
namespace Vn\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class MyProfileFormType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('birthday','birthday', array(
'input' => 'array',
'widget' => 'choice',
'label' => 'Birthday',
))
->add('firstname','text', array(
'trim' => true,
'label' => 'Firstname',
))
->add('lastname','text', array(
'trim' => true,
'label' => 'Lastname',
))
->add('gender','choice', array(
'choices' => array('1' => 'Male', '2' => 'Female'),
'expanded' => true,
'required' => true,
'label' => 'Vous êtes',
));
}
public function getName()
{
return 'vn_user_myprofile';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Vn\UserBundle\Document\Profile',
);
}
}
I found the mistake in my file ProfilFormeHandler.php : in the function process() I called parent::onSucess() instead of parent::process() ...
The result is a "silent" bug (silent because not fatal error appears) due to my fault of course
Thanks for time you spent to try to help me, very sorry !
<?php
// src/Vn/UserBundle/Form/Handler/RegistrationFormHandler.php
namespace Vn\UserBundle\Form\Handler;
use FOS\UserBundle\Form\Handler\ProfileFormHandler as BaseHandler;
use FOS\UserBundle\Model\UserInterface;
class ProfileFormHandler extends BaseHandler
{
public function process(UserInterface $user)
{
//parent::onSuccess($user);
parent::process($user); // sound better of course : )
}
protected function onSuccess(UserInterface $user)
{
$this->userManager->updateUser($user);
}
}

Resources