I'm creating a form using built-in Symfony3 services:
1) creating new class in AppBundle\Form which extends AbstractType
2) creating a form object in my controller (using createForm())
3) pushing that object directly to twig layer (by createView())
In my Entities direction I've got two classes, already mapped to database by ORM.
First one is User, and the second one is UserAttribute. User is related to UserAttribute by OneToMany annotation. Relationship looks like:
class UserAttr
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="userAttr" )
* #ORM\JoinColumn(nullable=false)
*/
private $user;
And from the User side:
class User
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="UserAttr", mappedBy="user")
* #ORM\JoinColumn(nullable=false)
*/
private $userAttr;
When I'm adding new fields (using $builder->add()) everything works fine if they are associated to User class properties. But if I'm doing the same with UserAttribute properties - symfony can't find get/set methods for that properties. I know - I can fix it by class User extends UserAttribute but probably that's not the point. Symfony must have another solution for that, probably I missed something.
Thanks for your time !
// SOLVED | there should be defined an EntityClassType as below:
$builder->add('credit',EntityType::class,array(
'class' => UserAttr::class
));
You have One-To-Many Association to UserAttr from User Entity. Hence, A User might have multiple credit.
OPTION 1 :
Considering this, you have to use a collection field type in UserFormType, which is a bit lengthy process.
$builder->add('userAttr', CollectionType::class, array(
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'entry_type' => UserAttrType::class
));
Then create another FormType : UserAttrType to represent UserAttr where you can have the credit field as a property from UserAttr.
$builder
->add('credit', TextType::class, array(
'label' => 'Credit',
))
This way, The form will load and submit accordingly, The credit value will also be updated when User form gets updated. Here is a link for collection docs. This is how to embed a collection form.
OPTION 2 :
But, If you want to simplify it further, add mapped = false (doc) to the credit field. This will ignore the current error. However, you have to manually collect the credit value from Form Object and set value to appropriate UserAttr Object in Submit Handler.
Hope it helps!
Related
So I've been struggling for the past 4 days with this and I am not sure if it is possible with Symfony or if I need to intervene with a custom fix. I feel like this is a common widget used through applications and it could be pretty simple to do but I am missing the point on how to do it.
The idea is to add already existing Products to a new Order in a form. The Products are already in the database, they just need to be brought in the frontend. To do this, a text field that searches them by name, returns a clickable drop-down list and on clicking one item from the list, the Product is added to the Order. With this, no new Products should be created.
The problem is that I want only the existing relations to be initially displayed, and not all of the Products from the database.
Now, what I've tried so far:
class Order
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Product", inversedBy="orders")
* #ORM\JoinTable()
*/
protected $products;
}
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Order", mappedBy="products")
*/
protected $orders;
}
And now OrderType form:
This will show a multi-select with all products and the selected ones are the products of the order. The problem is that the select contains all of the products.
->add('products')
The default one is actually an EntityType with multiple=true, expanded = false. I can expand it for checkboxes:
This will show only the Order's products in the select, but when I try to add a new one (I'll copy/paste one of the inputs's html and change its id to an existing product's) I will get "The choices "8" do not exist in the choice list.".
->add('products',EntityType::class,array('class'=>Product::class,'choices'=>$order->getProducts(),'multiple'=>true,'expanded'=>true))
This makes me think that I need to either allow EntityType to allow choices to be added that are not in the choices passed to the form, but I don't know how to do that, can't seem to find a way, OR use CollectionType which allows me to add/remove associations.
->add('products', CollectionType::class, array('by_reference' => false, 'entry_type' => EntityType::class, 'allow_add' => true, 'allow_delete' => true, 'entry_options' => array('class'=>Product::class,'expanded'=>true)))
This will render radio containing ALL products for each relation. I can add new products to the order thanks to the CollectionType, but the initial relations show all products as radio list for each relation. If only I could somehow tell EntityType to only render the current relation.
Is there a way to do this?
I have two entities Rental and Item. Items are associated to Rentals via a join table including some metadata, so that the join table effectively becomes a third entity RentedItem.
Because a RentedItem can be identified by its associated Rental and Item, it doesn't need its own ID but uses a compound key consisting of those two foreign keys as primary key instead.
/**
* #ORM\Entity
*/
class Rental
{
// ...
/**
* #ORM\OneToMany(targetEntity="RentedItem", mappedBy="rental", cascade={"all"})
* #var ArrayCollection $rented_items
*/
protected $rented_items;
// ...
}
/**
* #ORM\Entity
*/
class Item
{
// ...
// Note: The Item has no notion of any references to it.
}
/**
* #ORM\Entity
*/
class RentedItem
{
// ...
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Rental", inversedBy="rented_items")
* #var Rental $rental
*/
protected $rental;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Item")
* #var Item $item
*/
protected $item;
/**
* #ORM\Column(type="boolean")
* #var bool $is_returned
*/
protected $is_returned = false;
// ...
}
A Rental can be created or altered via a RESTful API, including some of its related objects. The corresponding controller uses the DoctrineObject hydrator of the ZF2 DoctrineModule to hydrate the rental object with the given form data. The new data is passed to the hydrator as an array of the form
$data = [
// We only use the customer's ID to create a reference to an
// existing customer. Altering or creating a customer via the
// RestfulRentalController is not possible
'customer' => 1,
'from_date' => '2016-03-09',
'to_date' => '2016-03-22',
// Rented items however should be alterable via the RestfulRentalController,
// because they don't have their own API. Therefore we pass
// the complete array representation to the hydrator
'rented_items' => [
[
// Again, just as we did with the customer,
// only use the referenced item's ID, so that
// changing an item is not possible
'item' => 6,
'is_returned' => false
// NOTE: obviously, the full array representation of
// a rented item would also contain the 'rental' reference,
// but since this is a new rental, there is no id yet, and
// the reference should be implicitly clear via the array hirarchy
],
[
'item' => 42,
'is_returned' => false
]
]
];
Usually the hydrator sets up the references correctly, even for completely new entities and new relations. However, with this complex association, hydrating the Rental fails. The code
$hydrator = new \DoctrineModule\Stdlib\Hydrator\DoctrineObject($entity_manager);
$rental = $hydrator->hydrate($data, $rental);
fails with the following exception
Doctrine\ORM\ORMException
File:
/vagrant/app/vendor/doctrine/orm/lib/Doctrine/ORM/ORMException.php:294
Message:
The identifier rental is missing for a query of Entity\RentedItem
Do I have to manually set up the references for the rented items? Or may this be caused by a faulty configuration or something?
I am developing an application and I came across the following: Lets say I have an entity called Contact, that Contact belongs to a Company and the Company has a Primary Contact and a Secondary Contact and also has the remaining Contacts which I've named Normal.
My question is, what is the best approach for this when talking about entities properties and also form handling. I've though about two things:
Having 2 fields on the Company entity called PrimaryContact and SecondaryContact and also have a one-to-many relationship to a property called contacts.
What I don't like (or I'm not 100% how to do) about this option is that on the Contact entity I would need an inversedBy field for each of the 2 one-to-one properties and also 1 for the one-to-many relationship and my personal thought is that this is kind of messy for the purpose.
Having a property on the Contact entity called Type which would hold if it's primary, secondary or normal and in the Company methods that has to do with Contacts I would modify it and add the getPrimaryContact, getSecondaryContact, etc.
What I don't like about this option is that I would need to have 2 unmapped properties for the Company and I would need to do a lot on the form types in order to get this to work smoothly.
My question is what is the best approach for this structure and how to deal with forms and these dependencies. Let me know if this is not clear enough and I will take time and preparate an example with code and images.
I'm not yet a Symfony expert but i'm currently learning entites manipulation and relations !
And there is not simple way to do relations with attributes.
You have to create an entity that represent your relation.
Let's suppose you have an entity Company and and entity Contact
Then you will have an entity named CompanyContact whick will represent the relation between your objects. (you can have as many attributes as you wish in your relation entity). (Not sure for the Many-to-One for your case but the idea is the same)
<?php
namespace My\Namespace\Entity
use Doctrine\ORM\Mapping as ORM
/**
* #ORM\Entity(repositoryClass="My\Namespace\Entity\CompanyContactRepository")
*/
class CompanyContact
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="contact_type", type="string", length=255)
*/
private $contactType;
/**
* #ORM\ManyToOne(targetEntity="My\Namespace\Entity\Company")
* #ORM\JoinColumn(nullable=false)
*/
private $company;
/**
* #ORM\ManyToOne(targetEntity="My\Namespace\Entity\Contact")
* #ORM\JoinColumn(nullable=false)
*/
private $contact;
}
And in your controller you can do this:
$em = $this->getDoctrine()->getManager();
$company = $em->getRepository('YourBundle:Company')->find($yourCompanyId);
$yourType = "primary";
$companyContacts = $em->getRepository('YourBundle:CompanyContact')
->findBy(array('company' => $company, 'type' => $yourType));
What do you think about this approach ?
If i learn more soon i will get you posted ;)
Thanks to #Cerad this is the following approach I took:
I have a OneToMany property on the Company to hold all the contacts.
Implemented the getPrimaryContact/setPrimaryContact methods and looped through all the contacts and retrieving the one of the type I want. Did the same for the secondary.
On the Form type of the company my issue was that I had the 'mapped' => 'false' option, I removed this since I implemented the getters and setters SF2 knows it has to go to these methods.
`
<?php
namespace XYZ\Entity;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class Company
{
...
/**
* #ORM\OneToMany(targetEntity="\XYZ\Entity\Contact", mappedBy="company", cascade={"persist", "remove"})
*/
private $contacts;
public function getPrimaryContact() { ... }
public function setPrimaryContact(Contact $contact) { //Set the type of $contact and add it $this->addContact($contact) }
public function getSecondaryContact() { ... }
public function setSecondaryContact(Contact $contact) { //Set the type of $contact and add it $this->addContact($contact) }
}`
And for the Form Type I have:
`
class CompanyType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('primaryContact', new ContactType())
->add('secondaryContact', new ContactType())
}
...
}`
With this set everything runs smoothly and I can CRUD without much struggle.
I have a View entity that references an associated entity called ViewVersion. But if I name the variable anything other than viewVersion, e.g. just simple version, then I get an error:
Neither the property "viewVersion" nor one of the methods "getViewVersion()", "isViewVersion()", "hasViewVersion()", "__get()" exist and have public access in class "Gutensite\CmsBundle\Entity\View\View".
All the getters and setters are created through php app/console doctrine:generate:entities but they are for getVersion() and not getViewVersion().
Question: So, is there some unspoken rule that associated entities MUST be named the same as their class name?
Entity Definition
/**
* #ORM\Entity
* #ORM\Table(name="view")
* #ORM\Entity(repositoryClass="Gutensite\CmsBundle\Entity\View\ViewRepository")
*/
class View extends Entity\Base {
/**
* #ORM\OneToOne(targetEntity="\Gutensite\CmsBundle\Entity\View\ViewVersion", inversedBy="view", cascade={"persist", "remove"}, orphanRemoval=true)
* #ORM\JoinColumn(name="versionId", referencedColumnName="id")
*/
protected $version;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $versionId = NULL;
}
FYI, the variables for associated entities can be whatever you want.
This was caused by a predefined formType still referencing "viewVersion". The first variable in a form $builder->add() is a reference to the specific variable in the entity. I had viewVersion listed there still, and when I audited my code, I assumed it was just a generic reference (without any requirement) or possibly a reference to the Entity class, so I didn't change it:
$builder->add('viewVersion', new ViewVersionType(), array(
'label' => false
));
The SOLUTION to this problem was to change viewVersion to version so that it references an actual variable on the entity. Obviously...
$builder->add('version', new ViewVersionType(), array(
'label' => false
));
To keep the field level constraints at a central place (not replicate it in each form), I added the constraints in the entity. Like below (lets say its one of the fields of a user entity):
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=255, nullable=false)
*
* #Constraints\NotBlank(
* groups={"register", "edit"},
* message="email cannot be blank."
* )
* #Constraints\Email(
* groups={"register", "edit"},
* message="Please enter a valid email address."
* )
*
* #Expose
* #Groups({"list", "details"})
*/
private $email;
Now I need a way to expose this validation constraints for each field which is an annotation of "Symfony\Component\Validator\Constraints". Is there a way that I can get all the constraints for all fields in the entity, like:
$em->getValidationConstraints('MyBundle:EntityUser'); //em is the entity manager
//and it returns me all the fields with its name, type and any constraints
//attached to it as any array
Thanks in advance.
Gather Information
Before fixing a problem, it's good to know what you are talking about and gather some information.
Doctrine is an ORM, something that does nice things between a database and an object. It has nothing to do with validation, that is done by the Symfony2 Validator Component. So you need something else than the $em.
All constraints of a class are called 'metadata' and they are usually stored in Symfony\Component\Validator\Mapping\ClassMetadata. We have to find a class which accepts the name of a class and returns a ClassMetadata instance.
To load the constraints, the Symfony2 Validator component uses loaders.
The Solution
We can see that there is a Symfony\Component\Validator\Mapping\ClassMetadataFactory. A factory is always used to build a class from a specific argument. In this case, we know it will create a ClassMetadata and we can see that it accepts a classname. We have to call ClassMetadataFactory::getMetadataFor.
But we see it needs some loaders. We aren't going to do the big job of initializing this factory, what about using the service container? We can see that the container has a validator.mapping.class_metadata_factory service, which is exactly the class we need.
Now we have all of that, let's use it:
// ... in a controller (maybe a seperated class is beter...)
public function someAction()
{
$metadataFactory = $this->get('validator.mapping.class_metadata_factory');
$metadata = $metadataFactory->getMetadataFor('Acme\DemoBundle\Entity\EntityUser');
}
Now we have the metadata, we only need to convert that to an array:
// ...
$propertiesMetadata = $metadata->properties;
$constraints = array();
foreach ($propertiesMetadata as $propertyMetadata) {
$constraints[$propertyMetadata->name] = $property->constraints;
}
Now, $constraints is an array with all fields and their constraint data, something like:
Array (
...
[email] => Array (
[0] => <instance of NotBlank>
[1] => <instance of Email>
),
)