in ZF2 I've got a form which edits the data of a table in the database. I'd like to add a field from another table to the form, how can I do it?
The table is called UserProfile, the entity Class goes like this:
namespace UpvUser\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\Factory as InputFactory;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
/**
* User Profile
*
* #ORM\Entity
* #ORM\Table(name="userprofile")
* #property int $userId
* #property string $title
* #property string $firstName
* #property string $lastName
and so on...
*/
class UserProfile
{
/**
* #ORM\Id
* #ORM\Column(type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $userId;
/**
* #ORM\Column(type="string")
*/
public $title;
/**
* #ORM\Column(type="string")
*/
public $firstName;
/**
* #ORM\Column(type="string")
*/
and so on including getters/setters and input filters...
The UserProfileForm class goes like this:
namespace UpvUser\Form;
use Zend\Form\Form;
use Zend\I18n\Translator\Translator;
class UserProfileForm extends Form
{
public function __construct($name = null)
{
parent::__construct('userprofile'); // The name of the form
$this->setAttribute('method', 'post');
$this->add(array(
'name' => 'userId',
'attributes' => array(
'type' => 'hidden',
),
));
$this->add(array(
'name' => 'title',
'attributes' => array(
'type' => 'text',
),
));
$this->add(array(
'name' => 'firstName',
'attributes' => array(
'type' => 'text',
),
));
// and so on...
and in the editAction() of the controller I have:
$user = $this->getEntityManager()->find('UpvUser\Entity\UserProfile', $userId);
$form = new UserProfileForm();
$form->bind($user);
$form->get('submit')->setAttribute('value', 'LANG_BTN_EDIT');
$request = $this->getRequest();
if ($request->isPost()) {
$form->setInputFilter($user->getInputFilter());
$form->setData($request->getPost());
if ($form->isValid()) {
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('upv-user', array('action' => 'overview')); // Redirect to list of albums
}
}
return array(
'userId' => $userId,
'form' => $form,
);
I guess the $form->bind($user) populates the form with the data from $user object, but how can I append other data, e.g. user's e-mail address, which is stored in another table called User (which has the corresponding structure and Entity class as well), in a column e-mail, with the same id as the userprofile.userid? I tried to make a getter to another table from UserProfile entity class, but the Entity manager is only available in the controller and not in Entity classes! Or should I do it in the controller and somehow append the missing data to the $user object (but I guess I should make it in the model and not in the controller)? Or do I have to define a foreign key between the two tables, even if the user id is the same (but I'm not allowed to change the User table, it's used as a shared resource for the other ones as well)?
Thanks a lot for info
Jan
you can do that with Fieldsets - just make an own Fieldset for the second Entity (note the setObject() call) and be sure to set the name the same as it is defined in your entity class (parent::__construct()). Example: 'user' -> is your referencing/relating entity property (from where you need the 'external' data) - sorry I can't explain it better.
class UserDataFieldset extends Fieldset {
public function __construct(ObjectManager $objectManager){
parent::__construct('user');
$this->setHydrator(new DoctrineHydrator($objectManager))
->setObject(new UserData());
$this->add(array(
'name' => 'email',
'attributes' => array(
'type' => 'text',
),
));
// etc....
}
}
In your Form you can add it like a normal form-element:
class UserProfileForm extends Form
{
public function __construct(ObjectManager $objectManager, $name = null)
{
// etc..
$this->add(new UserDataFieldset($objectManager));
}
}
that should be enough.
Related
I'm really confused about my Form Filter.
My Test-Project contains 2 Models.
class Category extends AbstractEntity
{
use Nameable; // just property name and getter and setter
/**
* #var boolean
* #ORM\Column(name="issue", type="boolean")
*/
private $issue;
/**
* #var Collection|ArrayCollection|Entry[]
*
* #ORM\OneToMany(targetEntity="CashJournal\Model\Entry", mappedBy="category", fetch="EAGER", orphanRemoval=true, cascade={"persist", "remove"})
*/
private $entries;
}
the entry
class Entry extends AbstractEntity
{
use Nameable;
/**
* #var null|float
*
* #ORM\Column(name="amount", type="decimal")
*/
private $amount;
/**
* #var null|Category
*
* #ORM\ManyToOne(targetEntity="CashJournal\Model\Category", inversedBy="entries", fetch="EAGER")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*/
protected $category;
/**
* #var null|DateTime
*
* #ORM\Column(name="date_of_entry", type="datetime")
*/
private $dateOfEntry;
}
And if someone needed the AbstractEntity
abstract class AbstractEntity implements EntityInterface
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
}
Every Category can have many Entries. I'm using Doctrine for this relation. And this works fine.
I have a Form based on this FieldSet:
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
]
]);
So my Form displays a dropdown with my categories. Yeah fine.
To load the Category for my Entry Entity i use a filter.
$this->add([
'name' => 'category',
'required' => true,
'filters' => [
[
'name' => Callback::class,
'options' => [
'callback' => [$this, 'loadCategory']
]
]
]
]);
And the callback:
public function loadCategory(string $categoryId)
{
return $this->mapper->find($categoryId);
}
The mapper loads the category fine. great. But the form is invalid because:
Object of class CashJournal\Model\Category could not be converted to int
Ok, so i'm removing the Filter, but now it failed to set the attributes to the Entry Entity, because the setter needs a Category. The Form error says:
The input is not a valid step
In Symfony i can create a ParamConverter, which converts the category_id to an valid Category Entity.
Question
How i can use the filter as my ParamConver?
Update
Also when i cast the category_id to int, i will get the error from the form.
Update 2
I changed my FieldSet to:
class EntryFieldSet extends Fieldset implements ObjectManagerAwareInterface
{
use ObjectManagerTrait;
/**
* {#inheritDoc}
*/
public function init()
{
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'required' => false,
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
'object_manager' => $this->getObjectManager(),
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label_generator' => function ($targetEntity) {
return $targetEntity->getName();
},
]
]);
parent::init();
}
}
But this will be quit with the error message:
Entry::setDateOfEntry() must be an instance of DateTime, string given
Have you checked the documentation for ObjectSelect? You appear to be missing a few options, namely which hydrator (EntityManager) and identifying property (id) to use. Have a look here.
Example:
$this->add([
'type' => ObjectSelect::class,
'name' => 'category', // Name of property, 'category' in your question
'options' => [
'object_manager' => $this->getObjectManager(), // Make sure you provided the EntityManager to this Fieldset/Form
'target_class' => Category::class, // Entity to target
'property' => 'id', // Identifying property
],
]);
To validate selected Element, add in your InputFilter:
$this->add([
'name' => 'category',
'required' => true,
]);
No more is needed for the InputFilter. A Category already exist and as such has been validated before. So, you should just be able to select it.
You'd only need additional filters/validators if you have special requirements, for example: "A Category may only be used once in Entries", making it so that you need to use a NoObjectExists validator. But that does not seem to be the case here.
UPDATE BASED ON COMMENTS & PAST QUESTIONS
I think you're over complicating a lot of things in what you're trying to do. It seems you want to simply populate a Form before you load it client-side. On receiving a POST (from client) you wish to put the received data in the Form, validate it and store it. Correct?
Based on that, please find a complete controller for User that I have in one of my projects. Hope you find it helpful. Providing it because updates are veering away from your original question and this might help you out.
I've removed some additional checking and error throwing, but otherwise is in complete working fashion.
(Please note that I'm using my own abstract controller, make sure to replace it with your own and/or recreate and match requirements)
I've also placed additional comments throughout this code to help you out
<?php
namespace User\Controller\User;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\ORMException;
use Exception;
use Keet\Mvc\Controller\AbstractDoctrineActionController;
use User\Entity\User;
use User\Form\UserForm;
use Zend\Http\Request;
use Zend\Http\Response;
class EditController extends AbstractDoctrineActionController
{
/**
* #var UserForm
*/
protected $userEditForm; // Provide this
public function __construct(ObjectManager $objectManager, UserForm $userEditForm)
{
parent::__construct($objectManager); // Require this in this class or your own abstract class
$this->setUserEditForm($userEditForm);
}
/**
* #return array|Response
* #throws ORMException|Exception
*/
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
// check if id set -> else error/redirect
/** #var User $entity */
$entity = $this->getObjectManager()->getRepository(User::class)->find($id);
// check if entity -> else error/redirect
/** #var UserForm $form */
$form = $this->getUserEditForm(); // GET THE FORM
$form->bind($entity); // Bind the Entity (object) on the Form
// Only go into the belof if() on POST, else return Form. Above the data is set on the Form, so good to go (pre-filled with existing data)
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost()); // Set received POST data on Form
if ($form->isValid()) { // Validates Form. This also updates the Entity (object) with the received POST data
/** #var User $user */
$user = $form->getObject(); // Gets updated Entity (User object)
$this->getObjectManager()->persist($user); // Persist it
try {
$this->getObjectManager()->flush(); // Store in DB
} catch (Exception $e) {
throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
}
return $this->redirectToRoute('users/view', ['id' => $user->getId()]);
}
}
// Returns the Form with bound Entity (object).
// Print magically in view with `<?= $this->form($form) ?>` (prints whole Form!!!)
return [
'form' => $form,
];
}
/**
* #return UserForm
*/
public function getUserEditForm() : UserForm
{
return $this->userEditForm;
}
/**
* #param UserForm $userEditForm
*
* #return EditController
*/
public function setUserEditForm(UserForm $userEditForm) : EditController
{
$this->userEditForm = $userEditForm;
return $this;
}
}
Hope that helps...
Extending on 2 previous questions about form structure and validating collections I've run into the next issue.
My form validates properly. Including included collections by way of Fieldsets. But the innermost Fieldset should not result in an Entity and a FK association to the parent if its values are not set.
An Address may or may not have linked Coordinates. It's possible to create all of these in the same Form.
However, the Coordinates should not be created and should not be linked from the Address if no coordinates have been given in the Form. They're not required in the Form and the Entity Coordinates itself requires both properties of Latitude and Longitude to be set.
Below, first the Entities. Following are the Fieldsets used for the AddressForm. I've removed stuff from both unrelated to the chain of Address -> Coordinates.
Address.php
class Address extends AbstractEntity
{
// Properties
/**
* #var Coordinates
* #ORM\OneToOne(targetEntity="Country\Entity\Coordinates", cascade={"persist"}, fetch="EAGER", orphanRemoval=true)
* #ORM\JoinColumn(name="coordinates_id", referencedColumnName="id", nullable=true)
*/
protected $coordinates;
// Getters/Setters
}
Coordinates.php
class Coordinates extends AbstractEntity
{
/**
* #var string
* #ORM\Column(name="latitude", type="string", nullable=false)
*/
protected $latitude;
/**
* #var string
* #ORM\Column(name="longitude", type="string", nullable=false)
*/
protected $longitude;
// Getters/Setters
}
As is seen in the Entities above. An Address has a OneToOne uni-directional relationship to Coordinates. The Coordinates entity requires both latitude and longitude properties, as seen with the nullable=false.
It's there that it goes wrong. If an Address is created, but no Coordinates's properties are set in the form, it still creates a Coordinates Entity, but leaves the latitude and longitude properties empty, even though they're required.
So, in short:
A Coordinates Entity is created where non should exist
A link to Coordinates is created from Address where non should exist
Below the Fieldsets and InputFilters to clarify things further.
AddressFieldset.php
class AddressFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
// Other properties
$this->add([
'type' => CoordinatesFieldset::class,
'required' => false,
'name' => 'coordinates',
'options' => [
'use_as_base_fieldset' => false,
],
]);
}
}
CoordinatesFieldset.php
class CoordinatesFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'latitude',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Latitude'),
],
]);
$this->add([
'name' => 'longitude',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Longitude'),
],
]);
}
}
AddressFieldsetInputFilter.php
class AddressFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/** #var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
protected $coordinatesFieldsetInputFilter;
public function __construct(
CoordinatesFieldsetInputFilter $filter,
EntityManager $objectManager,
Translator $translator
) {
$this->coordinatesFieldsetInputFilter = $filter;
parent::__construct([
'object_manager' => $objectManager,
'object_repository' => $objectManager->getRepository(Address::class),
'translator' => $translator,
]);
}
/**
* Sets AddressFieldset Element validation
*/
public function init()
{
parent::init();
$this->add($this->coordinatesFieldsetInputFilter, 'coordinates');
// Other filters/validators
}
}
CoordinatesFieldsetInputFilter.php
class CoordinatesFieldsetInputFilter extends AbstractFieldsetInputFilter
{
public function init()
{
parent::init();
$this->add([
'name' => 'latitude',
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
[
'name' => Callback::class,
'options' => [
'callback' => function($value, $context) {
//If longitude has a value, mark required
if(empty($context['longitude']) && strlen($value) > 0) {
$validatorChain = $this->getInputs()['longitude']->getValidatorChain();
$validatorChain->attach(new NotEmpty(['type' => NotEmpty::NULL]));
$this->getInputs()['longitude']->setValidatorChain($validatorChain);
return false;
}
return true;
},
'messages' => [
'callbackValue' => _('Longitude is required when setting Latitude. Give both or neither.'),
],
],
],
],
]);
// Another, pretty much identical function for longitude (reverse some params and you're there...)
}
}
EDIT: Adding a DB Dump image. Shows empty latitude, longitude.
EDIT2: When I remove 'allow_empty' => true, from the AddressFieldsetInputFilter inputs and fill a single input (latitude or longitude), then it validates correctly, unless you leave both inputs empty, then it breaks off immediately to return that the input is required. (Value is required and can't be empty).
By chance did I stumple upon this answer, which was for allowing a Fieldset to be empty but validate it if at least a single input was filled in.
By extending my own AbstractFormInputFilter and AbstractFieldsetInputFilter classes from an AbstractInputFilter class, which incorporates the answer, I'm now able to supply FielsetInputFilters, such as the AddressFieldsetInputFilter, with an additional ->setRequired(false). Which is then validated in the AbstractInputFilter, if it actually is empty.
The linked answer gives this code:
<?php
namespace Application\InputFilter;
use Zend\InputFilter as ZFI;
class InputFilter extends ZFI\InputFilter
{
private $required = true;
/**
* #return boolean
*/
public function isRequired()
{
return $this->required;
}
/**
* #param boolean $required
*
* #return $this
*/
public function setRequired($required)
{
$this->required = (bool) $required;
return $this;
}
/**
* #return bool
*/
public function isValid()
{
if (!$this->isRequired() && empty(array_filter($this->getRawValues()))) {
return true;
}
return parent::isValid();
}
}
As I mentioned I used this code to extend my own AbstractInputFilter, allowing small changes in *FieldsetInputFilterFactory classes.
AddressFieldsetInputFilterFactory.php
class AddressFieldsetInputFilterFactory extends AbstractFieldsetInputFilterFactory
{
/**
* #param ServiceLocatorInterface|ControllerManager $serviceLocator
* #return InputFilter
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
parent::setupRequirements($serviceLocator, Address::class);
/** #var CoordinatesFieldsetInputFilter $coordinatesFieldsetInputFilter */
$coordinatesFieldsetInputFilter = $this->getServiceManager()->get('InputFilterManager')
->get(CoordinatesFieldsetInputFilter::class);
$coordinatesFieldsetInputFilter->setRequired(false); // <-- Added option
return new AddressFieldsetInputFilter(
$coordinatesFieldsetInputFilter,
$this->getEntityManager(),
$this->getTranslator()
);
}
}
Might not be a good idea for everybody's projects, but it solves my problem of not always wanting to validate a Fieldset and it definitely solves the original issue of not creating an Entity with just an ID, as shown in the screenshot in the question.
I need to create ZF2 form for a Doctrine translatable Entity (I use https://github.com/Atlantic18/DoctrineExtensions Translatable Extension), which should provide fields for all translatable properties(columns) of the entity in each available language.
So far I have the following:
1) Article Entity
namespace TestModule\Entity;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #Doctrine\ORM\Mapping\Entity(repositoryClass="TestModule\Entity\ArticleRepository")
* #Doctrine\ORM\Mapping\Table(name="test_module_articles")
* #Gedmo\Mapping\Annotation\TranslationEntity(class="TestModule\Entity\ArticleTranslation")
*/
class Article
{
/**
* #var int Auto-Incremented Primary Key
*
* #Doctrine\ORM\Mapping\Id
* #Doctrine\ORM\Mapping\Column(type="integer")
* #Doctrine\ORM\Mapping\GeneratedValue
*/
protected $id;
/**
* #Doctrine\ORM\Mapping\Column(type="string")
* #Gedmo\Mapping\Annotation\Translatable
*/
protected $name;
/**
* #Doctrine\ORM\Mapping\Column(type="text", length=65535)
* #Gedmo\Mapping\Annotation\Translatable
*/
protected $description;
/**
* #Gedmo\Mapping\Annotation\Locale
* Used locale to override Translation listener`s locale
* this is not a mapped field of entity metadata, just a simple property
* and it is not necessary because globally locale can be set in listener
*/
private $locale;
/**
* #Doctrine\ORM\Mapping\OneToMany(
* targetEntity="TestModule\Entity\ArticleTranslation",
* mappedBy="object",
* cascade={"persist", "remove"}
* )
*/
private $translations;
public function __construct()
{
$this->translations = new ArrayCollection();
}
public function getTranslations()
{
return $this->translations;
}
public function addTranslation(\TestModule\Entity\ArticleTranslation $t)
{
if (!$this->translations->contains($t)) {
$this->translations[] = $t;
$t->setObject($this);
}
}
public function addTranslations($translations)
{
foreach ($translations as $translation) {
$this->addTranslation($translation);
}
}
public function removeTranslations($translations)
{
foreach ($translations as $translation) {
$this->translations->removeElement($translation);
$translation->setObject(null);
}
}
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
2) ArticleTranslation Entity
namespace TestModule\Entity;
use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation;
/**
* #Doctrine\ORM\Mapping\Entity
* #Doctrine\ORM\Mapping\Table(name="test_module_articles_translations",
* uniqueConstraints={#Doctrine\ORM\Mapping\UniqueConstraint(name="lookup_unique_idx", columns={
* "locale", "object_id", "field"
* })}
* )
*/
class ArticleTranslation extends AbstractPersonalTranslation
{
/**
* Convinient constructor
*
* #param string $locale
* #param string $field
* #param string $value
*/
public function __construct($locale, $field, $value)
{
$this->setLocale($locale);
$this->setField($field);
$this->setContent($value);
}
/**
* #Doctrine\ORM\Mapping\ManyToOne(targetEntity="TestModule\Entity\Article", inversedBy="translations")
* #Doctrine\ORM\Mapping\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $object;
}
3) The Form
namespace TestModule\Form;
use Zend\Form\Form;
use Doctrine\ORM\EntityManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
use TestModule\Form\ArticleTranslationsFieldset;
use TestModule\Entity\ArticleTranslation;
class ArticleForm extends Form
{
protected $entityManager;
public function __construct(EntityManager $entityManager,$name = null)
{
parent::__construct($name);
$this->entityManager = $entityManager;
$hydrator = new DoctrineHydrator($this->entityManager, 'TestModule\Entity\Article');
$this->setAttribute('method', 'post')
->setHydrator($hydrator)
//->setInputFilter($inputFilter)
;
$this->add(array(
'name' => 'id',
'type' => 'Hidden',
));
$articleFieldset = new ArticleTranslationsFieldset($entityManager);
$fieldsetHydrator = new DoctrineHydrator($entityManager, 'TestModule\Entity\ArticleTranslation');
$articleFieldset->setHydrator($fieldsetHydrator)->setObject(new ArticleTranslation('en','name',''));
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'translations',
'allow_empty' => true,
'options' => array(
'label' => '',
'count' => 0,
'allow_add' => true,
'allow_remove' => true,
'target_element' => $articleFieldset,
),
));
$this->add(array(
'name' => 'submit',
'attributes' => array(
'type' => 'submit',
'value' => 'Submit'
),
));
}
}
4) And the Translations Fieldset:
namespace TestModule\Form;
use Zend\Form\Fieldset;
class ArticleTranslationsFieldset extends Fieldset
{
public function __construct()
{
parent::__construct('translations');
$this->add(array(
'name' => 'locale',
'type' => 'Hidden',
));
$this->add(array(
'name' => 'field',
'type' => 'Hidden',
));
$this->add(array(
'name' => 'content',
'type' => 'Zend\Form\Element\Text',
'options' => array(
'label' => _(''),
),
'attributes' => array(
'type' => 'text',
),
));
}
}
With this set-up I can save both the name and the description properties for each language, but I cannot manage the content field type - it is Text element for either the name and the description and cannot set the proper field label. I also cannot group the elements by language so that the form presented to the user is well organized.
Do you have any other suggestions how to solve this problem?
What I want to achieve is something like this:
I couldn't find a solution with the Translatable Doctrine Extension that I used in the question. So I search for another one and finally I end up using the Prezent Extension(https://github.com/Prezent/doctrine-translatable).
With this extension the translation entity contains the translatable fields, which makes it easy to map the translation entity with the translations fieldset. Each translation entity has a locale property which I map to a hidden field in the fieldset and use it to present the form in the desired way.
Haven't found a solution to this problem through research yet but I am trying to save into the database with two forms in one (embedded/collection). I have entities that are related to each other and I want the form to submit and persist both entities into the database.
Main entity:
/**
* #var integer
* #ORM\OneToMany(targetEntity="Sub", mappedBy="mainId", cascade={"persist"})
*/
protected $sub;
public function __construct() {
$this->sub = new ArrayCollection();
}
Sub entity:
/**
* #var integer
*
* #ORM\Column(name="main_id", type="integer")
*/
protected $mainId;
.......
/**
* #ORM\ManyToOne(targetEntity="Main", inversedBy="sub")
* #ORM\JoinColumn(name="main_id", referencedColumnName="id")
*/
protected $main;
Here's my MainType form:
class MainType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('dano', 'text', array(
'label' => 'DA: ',
'disabled' => true
))
->add('partno','text', array(
'label' => 'Part: ',
'disabled' => true
))
->add('batchno', 'text', array(
'label' => 'Batch: ',
'disabled' => true
))
->add('sub', 'collection', array('type' => new SubType()))
->add('submit', 'submit');
}......
And my SubType form:
class SubType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('main_id','hidden')
->add('rackno','text', array(
'label' => 'Rack No(s) '
))
->add('diecode','text', array(
'label' => 'Die Code '
))
->add('heatcode','text', array(
'label' => 'Heat Code '
))
->add('inqty','integer', array(
'label' => 'Qty In '
))
->add('onhold','choice', array(
'label' => 'Hold',
'choices' => array(
'1' => 'On Hold',
'0' => 'Released'
),
'multiple' => false,
'expanded' => true
));
And my controller:
/**
* #param Request $request
* #Route("/{dano}", name="subpart_part")
*/
public function submitPartByDAAction(Request $request, $dano) {
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('Bundle:Main')
->findOneByDano($dano);
$partno = $entity->getPartno();
$batchno = $entity->getBatchno();
$mainid = $entity->getId();
$main1 = new Main();
$main1->setDano($dano);
$main1->setPartno($partno);
$main1->setBatchno($batchno);
$sub1 = new Sub();
$sub1->setMainId($mainid);
$main1->getSub()->add($sub1);
$form = $this->createForm(new MainType(), $main1, array(
'method' => 'POST'
));
$form->handleRequest($request);
if($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($main1);
$em->flush();
return $this->redirect($this->generateUrl('subpart_home'));
}
return $this->render('Bundle:Parts:addparts.html.twig', array(
'form' => $form->createView()
));
}
Let me explain what I did here, at first I did not have the Sub's "main_id" field (which is related to Main's id) on but when I tried persisting the data it gave me the error:
An exception occurred while executing 'INSERT INTO sub
(main_id, rackno, heatcode, diecode, inqty, onhold) VALUES
(?, ?, ?, ?, ?, ?)' with params [null, "46", "eterte", "seteter", 3, 0]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column
'main_id' cannot be null
So then I made a field "main_id" with it being hidden, grabbed the id from Main by getId(); and passed it into the Sub's form's setMainId(); to persist and it still gives me the same error that "main_id" cannot be null.
What am I missing? Thanks!
You are defining your entities wrong. First understand the concept of ORM and relations. Your Sub entity does not need to have the integer main_id. Simply map it to Main entity. Your Main entity should look like
/**
* #var Sub
* this value is just integer in database, but doc should point it to Sub
* #ORM\OneToMany(targetEntity="Sub", mappedBy="mainId", cascade={"persist"})
*/
protected $sub;
public function __construct() {
$this->sub = new ArrayCollection();
}
And your Sub entity
/**
* #ORM\ManyToOne(targetEntity="Main", inversedBy="sub")
* #ORM\JoinColumn(name="main_id", referencedColumnName="id")
*/
protected $main;
You dont need main_id. The ORM will handle that for you. The MainType form is good. Just get rid of the main_id in SubType form.
You should reference to entities by object rather than their IDs. In your controller also rather than using
$sub1->setMainId($mainid);
You should set the object.
$sub1->setMain($main1);
Your main form is also a little weird. I do not say it is not valid, but you should consider replacing this line:
->add('sub', 'collection', array('type' => new SubType()))
With something like this:
->add('sub', new SubType(), array())
I think it is way more appropriate if you have only "ONE" item. You use collection when you want many items.
I would suggest you look into the form component... how the form is represented as a tree...
Also never make fields like "main_id", unless it is necessary. Try not to work id's and work with associations.
I have a zf2 application that works with doctrine.
I have the following entity:
class Role
{
/**
* #var int
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=255, unique=true, nullable=true)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="YrmUser\Entity\Role", mappedBy="parent")
*/
protected $children;
/**
* #var Role
* #ORM\ManyToOne(targetEntity="YrmUser\Entity\Role", inversedBy="children", cascade={"persist"})
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
protected $parent;
}
for this entity i have a form:
class RoleForm extends Form
{
/**
* [init description]
*
* #return void
*/
public function init()
{
$this->setHydrator(
new DoctrineHydrator($this->objectManager, 'YrmUser\Entity\Role')
)->setObject(new Role());
$this->setAttribute('method', 'post');
$this->add(
array(
'name' => 'name',
'attributes' => array(
'type' => 'text',
'placeholder' =>'Name',
),
'options' => array(
'label' => 'Name',
),
)
);
$this->add(
array(
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'name' => 'parent',
'attributes' => array(
'id' => 'parent_id',
),
'options' => array(
'label' => 'Parent',
'object_manager' => $this->objectManager,
'property' => 'name',
'is_method' => true,
'empty_option' => '-- none --',
'target_class' => 'YrmUser\Entity\Role',
'is_method' => true,
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array('parent' => null),
),
),
),
)
);
}
}
The hydration for the select in the form works as it only shows other roles that don't have a parent.
But when editing a existing entity it shows itself in the select so i can select itself as its parent.
I figured if i would have the id of current entity inside the form i can create a custom repo with a method that retrieves all roles without a parent and does not have the current entity id.
But i cant figure out how to get the id of the currently edited entity from inside the form.
Any help is appreciated.
Cheers,
Yrm
You can fetch the bound entity within the form using $this->getObject().
You have actually already set this with setObject(new Role());. Unfortunately this means that it was not loaded via Doctine and you will have the same issue, no $id to work with.
Therefore you will need to add the 'parent role' options (value_options) after you have bound the role loaded via doctrine.
From within the controller, I normally request the 'edit' form from a service class and pass in the entity instance or id that is being edited. Once set you can then modify existing form elements before passing it back to the controller.
// Controller
class RoleController
{
public function editAction()
{
$id = $this->params('id'); // assumed id passed as param
$service = $this->getRoleService();
$form = $service->getRoleEditForm($id); // Pass the id into the getter
// rest of the controller...
}
}
By passing in the $id when you fetch the form you can then, within a service, modify the form elements for that specific role.
class RoleService implements ObjectManagerAwareInterface, ServiceLocatorAwareInterface
{
protected function loadParentRolesWithoutThisRole(Role $role);
public function getRoleEditForm($id)
{
$form = $this->getServiceLocator()->get('Role\Form\RoleEditForm');
if ($id) {
$role = $this->getObjectManager()->find('Role', $id);
$form->bind($role); // calls $form->setObject() internally
// Now the correct entity is attached to the form
// Load the roles excluding the current
$roles = $this->loadParentRolesWithoutThisRole($role);
// Find the parent select element and set the options
$form->get('parent')->setValueOptions($roles);
}
// Pass form back to the controller
return $form;
}
}
By loading the options after the form has initialized you do not need the current DoctrineModule\Form\Element\ObjectSelect. A normal Select element that has no default value_options defined should be fine.