Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

symfony2: multiple entities one form

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()
    ));
}
like image 748
Justin Avatar asked Aug 01 '14 17:08

Justin


People also ask

Can I use multiple doctrine entity managers in Symfony?

You can use multiple Doctrine entity managers or connections in a Symfony application. This is necessary if you are using different databases or even vendors with entirely different sets of entities. In other words, one entity manager that connects to one database will handle some entities...

Can I have multiple connections for the same entity manager?

You've also defined two connections, one for each entity manager, but you are free to define the same connection for both. When working with multiple connections and entity managers, you should be explicit about which configuration you want. If you do omit the name of the connection or entity manager, the default (i.e. default) is used.

Can I use a different name for the default entity manager?

If you do omit the name of the connection or entity manager, the default (i.e. default) is used. If you use a different name than default for the default entity manager, you will need to redefine the default entity manager in the prod environment configuration and in the Doctrine migrations configuration (if you use that):

What is the difference between one entity manager and another?

In other words, one entity manager that connects to one database will handle some entities while another entity manager that connects to another database might handle the rest. It is also possible to use multiple entity managers to manage a common set of entities, each with their own database connection strings or separate cache configuration.


Video Answer


1 Answers

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 = $('<a href="#" class="add_tag_link btn btn-xs btn-success">Add Another Destination</a>');
    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.

like image 160
Justin Avatar answered Oct 27 '22 05:10

Justin