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.
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...
I'm using Zend Framework 3 with Doctrine and I'm trying to save an Entity "Cidade" related to another Entity "Estado" witch is already stored in the database. However, Doctrine is trying to persist Entity "Estado", and the only attribute I have from Estado is the primary key in a HTML combo.
My view forms are built under Zend forms and fieldsets, which means POST data is automatically converted to the target entities using ClassMethods hydrator.
The problem is that if I set the attribute $estado with cascade={"persist"} in Cidade Entity, Doctrine tries to persist Estado Entity missing all required attributes but the primary key ID, which comes from POST request (HTML combo). I also considered using cascade={"detach"} ir order to Doctrine ignore Estado Entity in the EntityManager. But I get this error:
A new entity was found through the relationship 'Application\Entity\Cidade#estado' that was not configured to cascade persist operations for entity: Application\Entity\Estado#000000007598ee720000000027904e61.
I found a similar doubt here and the only way I could find for this matter was first retrieving Estado Entity and setting it on Cidade Entity before saving. If this is the only way, can I tell my form structure won't work unless I retrieve all relationships before saving the dependant entities?
In other words, what is the best way of doing such thing in Doctrine (for example):
<?php
/*I'm simulating the creation of Estado Entity representing an
existing Estado in database, so "3" is the ID rendered in HTML combo*/
$estado = new Entity\Estado();
$estado->setId(3);
$cidade = new Entity\Cidade();
$cidade->setNome("City Test");
$cidade->setEstado($estado); //relationship here
$entityManager->persist($cidade);
$entityManager->flush();
How to do that without having to retrieve an Estado all the time I need to save a Cidade? Wouldn't affect performance?
My Cidade Entity:
<?php
namespace Application\Entity;
use Zend\InputFilter\Factory;
use Zend\InputFilter\InputFilterInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Cidade
* #package Application\Entity
* #ORM\Entity
*/
class Cidade extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
private $nome;
/**
* #var Estado
* #ORM\ManyToOne(targetEntity="Estado", cascade={"detach"})
* #ORM\JoinColumn(name="id_estado", referencedColumnName="id")
*/
private $estado;
/**
* Retrieve input filter
*
* #return InputFilterInterface
*/
public function getInputFilter()
{
if (!$this->inputFilter) {
$factory = new Factory();
$this->inputFilter = $factory->createInputFilter([
"nome" => ["required" => true]
]);
}
return $this->inputFilter;
}
/**
* #return string
*/
public function getNome()
{
return $this->nome;
}
/**
* #param string $nome
*/
public function setNome($nome)
{
$this->nome = $nome;
}
/**
* #return Estado
*/
public function getEstado()
{
return $this->estado;
}
/**
* #param Estado $estado
*/
public function setEstado($estado)
{
$this->estado = $estado;
}
}
My Estado Entity:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\Factory;
use Zend\InputFilter\InputFilterInterface;
/**
* Class Estado
* #package Application\Entity
* #ORM\Entity
*/
class Estado extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
private $nome;
/**
* #var string
* #ORM\Column(length=3)
*/
private $sigla;
/**
* #return string
*/
public function getNome()
{
return $this->nome;
}
/**
* #param string $nome
*/
public function setNome($nome)
{
$this->nome = $nome;
}
/**
* #return string
*/
public function getSigla()
{
return $this->sigla;
}
/**
* #param string $sigla
*/
public function setSigla($sigla)
{
$this->sigla = $sigla;
}
/**
* Retrieve input filter
*
* #return InputFilterInterface
*/
public function getInputFilter()
{
if (!$this->inputFilter) {
$factory = new Factory();
$this->inputFilter = $factory->createInputFilter([
"nome" => ["required" => true],
"sigla" => ["required" => true]
]);
}
return $this->inputFilter;
}
}
Both entities extend my superclass AbstractEntity:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping\MappedSuperclass;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
/**
* Class AbstractEntity
* #package Application\Entity
* #MappedSuperClass
*/
abstract class AbstractEntity implements InputFilterAwareInterface
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var InputFilterAwareInterface
*/
protected $inputFilter;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #param InputFilterInterface $inputFilter
* #return InputFilterAwareInterface
* #throws \Exception
*/
public function setInputFilter(InputFilterInterface $inputFilter)
{
throw new \Exception("Método não utilizado");
}
}
My HTML inputs are rendered as it follows:
<input name="cidade[nome]" class="form-control" value="" type="text">
<select name="cidade[estado][id]" class="form-control">
<option value="3">Bahia</option>
<option value="2">Espírito Santo</option>
<option value="1">Minas Gerais</option>
<option value="9">Pará</option>
</select>
Each option above is an Estado Entity retrieved from database. My POST data comes as the following example:
[
"cidade" => [
"nome" => "Test",
"estado" => [
"id" => 3
]
]
]
On Zend Form's isValid() method, this POST data is automatically converted to the target Entities, which makes me crash on this Doctrine issue. How do I move on?
You should bind an object to your form and use the Doctrine Hydrator. In the form the field names should exactly match that of the Entity. So Entity#name is Form#name.
With Separation of Concerns I'm absolutely against placing the InputFilter for an Entity within the Entity itself. As such, I'll give you an example with everything separated, if you decide to mash it back together, that's up to you.
AbstractEntity for ID
/**
* #ORM\MappedSuperclass
*/
abstract class AbstractEntity
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
// getter/setter
}
Cicade Entity
/**
* #ORM\Entity
*/
class Cidade extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
protected $nome; // Changed to 'protected' so can be used in child classes - if any
/**
* #var Estado
* #ORM\ManyToOne(targetEntity="Estado", cascade={"persist", "detach"}) // persist added
* #ORM\JoinColumn(name="id_estado", referencedColumnName="id")
*/
protected $estado;
// getters/setters
}
Estado Entity
/**
* #ORM\Entity
*/
class Estado extends AbstractEntity
{
/**
* #var string
* #ORM\Column(length=50)
*/
protected $nome;
//getters/setters
}
So, above is the Entity setup for Many to One - Uni-direction relation.
You want to manage this, easily, with forms. So we need to create InputFilters for both.
Having InputFilters separately from the Entity allows us to nest them. This in turn allows us to create structured and nested forms.
For example, you could create a new Estado on-the-fly. If this were a bi-directional relation, you could create multiple Cicade Entity objects on-the-fly from/during the creation of Estado.
First: InputFilters. In the spirit of abstraction, which you started with your Entities, let's do that here as well:
AbstractDoctrineInputFilter
source AbstractDoctrineInputFilter & source AbstractDoctrineFormInputFilter
This gives a nice clean setup and a requirement to fulfill. I'm glossing over the more complex elements added in the source files, feel free to look those up though.
Both objects (Estado & Cicade) require an ObjectManager (they're Doctrine entities after all), so I'm assuming you might have more. The below should come in handy.
<?php
namespace Application\InputFilter;
use Doctrine\Common\Persistence\ObjectManager;
use Zend\InputFilter\InputFilter;
abstract class AbstractInputFilter extends InputFilter
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* AbstractFormInputFilter constructor.
*
* #param array $options
*/
public function __construct(array $options)
{
// Check if ObjectManager|EntityManager for FormInputFilter is set
if (isset($options['object_manager']) && $options['object_manager'] instanceof ObjectManager) {
$this->setObjectManager($options['object_manager']);
}
}
/**
* Init function
*/
public function init()
{
$this->add(
[
'name' => 'id',
'required' => false, // Not required when adding - should also be in route when editing and bound in controller, so just additional
'filters' => [
['name' => ToInt::class],
],
'validators' => [
['name' => IsInt::class],
],
]
);
// If CSRF validation has not been added, add it here
if ( ! $this->has('csrf')) {
$this->add(
[
'name' => 'csrf',
'required' => true,
'filters' => [],
'validators' => [
['name' => Csrf::class],
],
]
);
}
}
// getters/setters for ObjectManager
}
Estado InputFilter
class EstadoInputFilter extends AbstractInputFilter
{
public function init()
{
parent::init();
$this->add(
[
'name' => 'nome', // <-- important, name matches entity property
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
],
]
);
}
}
Cicade InputFilter
class EstadoInputFilter extends AbstractInputFilter
{
public function init()
{
parent::init(); // Adds the CSRF
$this->add(
[
'name' => 'nome', // <-- important, name matches entity property
'required' => true,
'allow_empty' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 2,
'max' => 255,
],
],
],
]
);
$this->add(
[
'name' => 'estado',
'required' => true,
]
);
}
}
So. Now we have 2 InputFilters, based on an AbstractInputFilter.
EstadoInputFilter filters just the nome property. Add additional if you want ;)
CicadeInputFilter filters the nome property and has a required estado field.
The names match those of the Entity definition in the respective Entity classes.
Just to be complete, below is the CicadeForm, take what you need to create the EstadoForm.
class CicadeForm extends Form
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* AbstractFieldset constructor.
*
* #param ObjectManager $objectManager
* #param string $name Lower case short class name
* #param array $options
*/
public function __construct(ObjectManager $objectManager, string $name, array $options = [])
{
parent::__construct($name, $options);
$this->setObjectManager($objectManager);
}
public function init()
{
$this->add(
[
'name' => 'nome',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Nome',
],
]
);
// #link: https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md
$this->add(
[
'type' => ObjectSelect::class,
'required' => true,
'name' => 'estado',
'options' => [
'object_manager' => $this->getObjectManager(),
'target_class' => Estado::class,
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label' => _('Estado'),
'label_attributes' => [
'title' => _('Estado'),
],
'label_generator' => function ($targetEntity) {
/** #var Estado $targetEntity */
return $targetEntity->getNome();
},
],
]
);
//Call parent initializer. Check in parent what it does.
parent::init();
}
/**
* #return ObjectManager
*/
public function getObjectManager() : ObjectManager
{
return $this->objectManager;
}
/**
* #param ObjectManager $objectManager
*
* #return AbstractDoctrineFieldset
*/
public function setObjectManager(ObjectManager $objectManager) : AbstractDoctrineFieldset
{
$this->objectManager = $objectManager;
return $this;
}
}
Config
Now that the classes are there, how to use them? Slap 'em together with module config!
In your module.config.php file, add this config:
'form_elements' => [
'factories' => [
CicadeForm::class => CicadeFormFactory::class,
EstadoForm::class => EstadoFormFactory::class,
// If you create separate Fieldset classes, this is where you register those
],
],
'input_filters' => [
'factories' => [
CicadeInputFilter::class => CicadeInputFilterFactory::class,
EstadoInputFilter::class => EstadoInputFilterFactory::class,
// If you register Fieldsets in form_elements, their InputFilter counterparts go here
],
],
From this config we read we need a Factory for both the Form and for the InputFilter of a set.
Below the CicadeInputFilterFactory
class CicadeInputFilterFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
*
* #return CicadeInputFilter
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager|EntityManager $objectManager */
$objectManager = $this->setObjectManager($container->get(EntityManager::class));
return new CicadeInputFilter(
[
'object_manager' => objectManager,
]
);
}
}
Matching CicadeFormFactory
class CicadeFormFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
*
* #return CicadeForm
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) : CicadeForm
{
$inputFilter = $container->get('InputFilterManager')->get(CicadeInputFilter::class);
// Here we creazte a new Form object. We set the InputFilter we created earlier and we set the DoctrineHydrator. This hydrator can work with Doctrine Entities and relations, so long as data is properly formatted when it comes in from front-end.
$form = $container->get(CicadeForm::class);
$form->setInputFilter($inputFilter);
$form->setHydrator(
new DoctrineObject($container->get(EntityManager::class))
);
$form->setObject(new Cicade());
return $form;
}
}
Massive preparation done, time to use it
Specific EditController to Edit an existing Cicade Entity
class EditController extends AbstractActionController // (Zend's AAC)
{
/**
* #var CicadeForm
*/
protected $cicadeForm;
/**
* #var ObjectManager|EntityManager
*/
protected $objectManager;
public function __construct(
ObjectManager $objectManager,
CicadeForm $cicadeForm
) {
$this->setObjectManager($objectManager);
$this->setCicadeForm($cicadeForm);
}
/**
* #return array|Response
* #throws ORMException|Exception
*/
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
if (is_null($id)) {
$this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of id received from route
}
/** #var Cicade $entity */
$entity = $this->getObjectManager()->getRepository(Cicade::class)->find($id);
if (is_null($entity)) {
$this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of not found entity
}
/** #var CicadeForm $form */
$form = $this->getCicadeForm();
$form->bind($entity); // <-- This here is magic. Because we overwrite the object from the Factory with an existing one. This pre-populates the form with value and allows us to modify existing one. Assumes we got an entity above.
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
/** #var Cicade $cicade */
$cicade = $form->getObject();
$this->getObjectManager()->persist($cicade);
try {
$this->getObjectManager()->flush();
} catch (Exception $e) {
throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
}
$this->redirect()->toRoute('cicade/view', ['id' => $entity->getId()]);
}
}
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
/**
* #return CicadeForm
*/
public function getCicadeForm() : CicadeForm
{
return $this->cicadeForm;
}
/**
* #param CicadeForm $cicadeForm
*
* #return EditController
*/
public function setCicadeForm(CicadeForm $cicadeForm) : EditController
{
$this->cicadeForm = $cicadeForm;
return $this;
}
/**
* #return ObjectManager|EntityManager
*/
public function getObjectManager() : ObjectManager
{
return $this->objectManager;
}
/**
* #param ObjectManager|EntityManager $objectManager
*
* #return EditController
*/
public function setObjectManager(ObjectManager $objectManager) : EditController
{
$this->objectManager = $objectManager;
return $this;
}
}
So, felt like giving a really expanded answer. Covers the whole thing really.
If you have any questions about the above, let me know ;-)
I have two classes. Lets says it is Album and AlbumManager. Album is a intake class which I am using for generating basic entity function. I do not want to touch Album for custom functions. All other custom function I wrote in AlbumManager class.
Now from controller, when I try to access Album class through AlbumManager, it through me an error:
Can not be mapped or accessed.
How can I handle this type of dependency? Here are the classes:
Album.php
<?php
namespace Album\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Album
*
* #ORM\Table(name="album")
* #ORM\Entity
*/
class Album
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", precision=0, scale=0, nullable=false, unique=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="artist", type="string", length=255, precision=0, scale=0, nullable=false, unique=false)
*/
private $artist;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set artist
*
* #param string $artist
* #return Album
*/
public function setArtist($artist)
{
$this->artist = $artist;
return $this;
}
/**
* Get artist
*
* #return string
*/
public function getArtist()
{
return $this->artist;
}
?>
AlbumManager.php
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
namespace Album\Manager;
use Album\Entity as Album;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Factory as InputFactory;
use Zend\InputFilter\InputFilterInterface;
class AlbumManager
{
private $album;
protected $inputFilter;
public function __construct(Album\Entity $album)
{
$this->album = $album;
}
public function getAlbum()
{
return $this->album;
}
/**
* Magic getter to expose protected properties.
*
* #param string $property
* #return mixed
*/
public function __get($property)
{
return $this->$property;
}
/**
* Magic setter to save protected properties.
*
* #param string $property
* #param mixed $value
*/
public function __set($property, $value)
{
$this->$property = $value;
}
/**
* Convert the object to an array.
*
* #return array
*/
public function getArrayCopy()
{
return get_object_vars($this);
}
/**
* Populate from an array.
*
* #param array $data
*/
public function populate($data = array())
{
$this->album->setId($data['id']);
$this->album->setArtist($data['artist']);
$this->album->setTitle($data['title']);
}
public function setInputFilter(InputFilterInterface $inputFilter)
{
throw new \Exception("Not used");
}
public function getInputFilter()
{
if (!$this->inputFilter) {
$inputFilter = new InputFilter();
$factory = new InputFactory();
$inputFilter->add($factory->createInput(array(
'name' => 'id',
'required' => true,
'filters' => array(
array('name' => 'Int'),
),
)));
$inputFilter->add($factory->createInput(array(
'name' => 'artist',
'required' => true,
'filters' => array(
array('name' => 'StripTags'),
array('name' => 'StringTrim'),
),
'validators' => array(
array(
'name' => 'StringLength',
'options' => array(
'encoding' => 'UTF-8',
'min' => 1,
'max' => 100,
),
),
),
)));
$inputFilter->add($factory->createInput(array(
'name' => 'title',
'required' => true,
'filters' => array(
array('name' => 'StripTags'),
array('name' => 'StringTrim'),
),
'validators' => array(
array(
'name' => 'StringLength',
'options' => array(
'encoding' => 'UTF-8',
'min' => 1,
'max' => 100,
),
),
),
)));
$this->inputFilter = $inputFilter;
}
return $this->inputFilter;
}
}
AlbumController.php
<?php
namespace Album\Controller;
use Zend\Mvc\Controller\AbstractActionController,
Zend\View\Model\ViewModel,
Album\Form\AlbumForm,
Doctrine\ORM\EntityManager,
Doctrine\ORM\EntityRepository,
Album\Entity\Album,
Album\Manager\AlbumManager,
class AlbumController extends AbstractActionController
{
public function addAction()
{
$form = new AlbumForm();
$form->get('submit')->setAttribute('label', 'Add');
$request = $this->getRequest();
if ($request->isPost()) {
$album = new Album();
$albumManager = new AlbumManager($album);
$form->setInputFilter($album->getInputFilter());
$form->setData($request->getPost());
if ($form->isValid()) {
$album->populate($form->getData());
$this->getEntityManager()->persist($album);
$this->getEntityManager()->flush();
// Redirect to list of albums
return $this->redirect()->toRoute('album');
}
}
return array('form' => $form);
}
?>
See the code at AlbumController
$album = new Album();
$albumManager = new AlbumManager($album);
$form->setInputFilter($album->getInputFilter());
$form->setData($request->getPost());
if ($form->isValid()) {
$album->populate($form->getData());
The album object can not get access to the getInputFilter and populate function which I write down in the Album Repository. I understood this happened because of the dependency injected. But what would be the solution so that I can access the album object, set the form data and also can use the album manger functions?
I have problem with fieldsets in ZF2, I show you my problem.
Here is my form (made by AngularJS, not by ZF2), where you can put a name, and select if you permit which page or which action (Pages are composed of actions).
The picture below partially show what I send to ZF2 :
Here is my data model of my CustomRole class :
class CustomRole
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Users\CustomRolePagePermission", mappedBy="customRole")
*/
protected $pagePermissions;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Users\CustomRoleActionPermission", mappedBy="customRole")
*/
protected $actionPermissions;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Users\GlobalRole")
*/
protected $globalRole;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Cie", inversedBy="customRoles")
* #ORM\JoinColumn(name="cie", referencedColumnName="id_cie")
*/
protected $cie;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Users\User", mappedBy="customRole")
*/
protected $users;
...
Here my class CustomRolePagePermission (which is near the same than CustomRoleActionPermission) :
class CustomRolePagePermission extends PagePermission
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="App\Entity\Users\CustomRole", inversedBy="pagePermissions")
* #ORM\JoinColumn(name="custom_role_id", referencedColumnName="id", nullable=false)
*/
protected $customRole;
...
And then the abstract class PagePermission :
abstract class PagePermission
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="App\Entity\Page")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id", nullable=false)
*/
protected $page;
/**
* #ORM\Column(type="boolean")
*/
protected $permission;
...
Now the Fieldset corresponding to CustomRole class (I have made it on each entities) :
class CustomRoleFieldset extends Fieldset implements InputFilterProviderInterface {
protected $serviceLocator;
public function __construct(EntityManager $entityManager) {
parent::__construct('role');
$this->setHydrator(new DoctrineHydrator($entityManager, 'App\Entity\Users\CustomRole'))
->setObject(new CustomRole());
$this->add(array('name' => 'name'));
$customRolePagePermissionFieldset = new CustomRolePagePermissionFieldset($entityManager);
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'pagePermission',
'options' => array(
'target_element' => $customRolePagePermissionFieldset
),
));
$customRoleActionPermissionFieldset = new CustomRoleActionPermissionFieldset($entityManager);
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'actionPermission',
'options' => array(
'target_element' => $customRoleActionPermissionFieldset
),
));
}
public function getInputFilterSpecification() {
return array(
'name' => array('required' => true),
'pagePermission' => array('required' => true),
'actionPermission' => array('required' => true),
);
}
}
...
Here my fieldset CustomRolePagePermissionFieldset :
class CustomRolePagePermissionFieldset extends Fieldset implements InputFilterProviderInterface {
protected $serviceManager;
public function __construct(EntityManager $entityManager) {
parent::__construct();
$this->setHydrator(new DoctrineHydrator($entityManager, 'App\Entity\Users\CustomRolePagePermission'))
->setObject(new CustomRolePagePermission());
$this->add(array('name' => 'permission'));
}
...
And then, my controller :
...
$customRoleForm = new CustomRoleForm($em);
$customRole = new CustomRole();
$formData = $request->getPost();
$customRoleForm->bind($customRole);
$customRoleForm->setData($formData);
if ($customRoleForm->isValid()) {
$customRole->setCie($cie);
$customRole->setGlobalRole($globalRole);
$em->persist($customRole);
$em->flush();
return $this->ok($customRole->getId());
}
...
Problem
When I send the form, the CustomRole is created, but pages and actions checked previously are not linked with the CustomRole created, as if I had never checked any checkbox.
I don't understand why it doesn't have effects, do you have any ideas ?
Thanks a lot in advance ! :)
Your collection should be "pagePermissions" for the hydrator to call setPagePermissions.
I am trying to implement doctrine hydrator in my zend 2 project. I am using doctrine's official documentation
I am getting two warning and one error. Following is the warning at top of page.
Warning: Missing argument 2 for DoctrineModule\Stdlib\Hydrator\DoctrineObject::__construct(), called in /path/to/my/project/module/Notes/src/Notes/Form/AssignForm.php on line 16 and defined in /path/to/my/project/vendor/doctrine/doctrine-module/src/DoctrineModule/Stdlib/Hydrator/DoctrineObject.php on line 71
Notice: Undefined variable: targetClass in /path/to/my/project/vendor/doctrine/doctrine-module/src/DoctrineModule/Stdlib/Hydrator/DoctrineObject.php on line 76
Here is the error:
An error occurred
An error occurred during execution; please try again later.
Additional information:
Doctrine\Common\Persistence\Mapping\MappingException
File:
/path/to/my/project/vendor/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/MappingException.php:96
Message:
Class '' does not exist
Here is my entity:
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Entity */
class Assigned{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/** #ORM\Column(type="string")
* #access protected
*/
protected $loan_number;
/** #ORM\Column(type="string")
* #access protected
*/
protected $claim_status;
/** #ORM\Column(type="datetime")
* #access protected
*/
protected $hold_date;
/** #ORM\Column(type="datetime")
* #access protected
*/
protected $vsi_date;
/** #ORM\Column(type="integer")
* #access protected
*/
protected $hold_status;
/** #ORM\Column(type="integer")
* #access protected
*/
protected $vsi_status;
/**
* #param string $id
* #return Assign
*/
// id should be auto incremental in database.
/*
public function setId($id)
{
$this->id = $id;
return $this;
}
*/
/**
* #return string $id;
*/
public function getId()
{
return $this->id;
}
/**
* #param string $loan_number
* #access public
* #return Assigned
*/
public function setLoanNumber($loan_number)
{
$this->loan_number = $loan_number;
return $this;
}
/**
* #return string $loan_number
*/
public function getLoanNumber()
{
return $this->loan_number;
}
/**
* #param string $claim_status
* #access public
* #return Assigned
*/
public function setClaimStatus($claim_status)
{
$this->claim_status = $claim_status;
return $this;
}
/**
* #return string $claim_status;
*/
public function getClaimStatus()
{
return $this->claim_status;
}
/**
* #param datetime $hold_date
* #access public
* #return Assigned
*/
public function setHoldDate($hold_date)
{
$this->hold_date = new \DateTime($hold_date);
return $this;
}
/**
* #return datetime $hold_date;
*/
public function getHoldDate()
{
return $this->hold_date;
}
/**
* #param datetime $vsi_date
* #access public
* #return Assigned
*/
public function setVsiDate($vsi_date)
{
$this->vsi_date = new \DateTime($vsi_date);
return $this;
}
/**
* #return datetime $vsi_date;
*/
public function getVsiDate()
{
return $this->vsi_date;
}
/**
* #param integer $hold_status
* #access public
* #return Assigned
*/
public function setHoldStatus($hold_status)
{
$this->hold_status = $hold_status;
return $this;
}
/**
* #return integer $hold_status;
*/
public function getHoldStatus($hold_status)
{
return $this->hold_status;
}
/**
* #param integer $vsi_status
* #access public
* #return Assigned
*/
public function setVsiStatus($vsi_status)
{
$this->vsi_status = $vsi_status;
return $this;
}
/**
* #return integer $vsi_status;
*/
public function getVsiStatus()
{
return $this->vsi_status;
}
}
Here is my Controller
<?php
namespace Notes\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Doctrine\ORM\EntityManager;
use Application\Entity\Assigned;
use Notes\Form\AssignForm;
use Notes\Form\NotesFieldset;
class NotesController extends AbstractActionController
{
protected $objectManager;
public function indexAction()
{
return new ViewModel();
}
public function addAction()
{
// Get your ObjectManager from the ServiceManager
$objectManager = $this->getOBjectManager();
$form = new AssignForm($objectManager);
// Create a new, empty entity and bind it to the form
$assigned = new Assigned();
$form->bind($assigned);
if ($this->request->isPost()) {
$form->setData($this->request->getPost());
if ($form->isValid()) {
$objectManager->persist($assigned);
$objectManager->flush();
}
}
return array('form' => $form);
}
public function getOBjectManager(){
if(!$this->objectManager){
$this->objectManager = $this->getServiceLocator()
->get('Doctrine\ORM\EntityManager');
}
return $this->objectManager;
}
}
Here is my form class:
<?php
namespace Notes\Form;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Form;
use Notes\Form\NotesFieldset;
class AssignForm extends Form
{
public function __construct(ObjectManager $objectManager)
{
parent::__construct('assigned');
// The form will hydrate an object of type "BlogPost"
$this->setHydrator(new DoctrineHydrator($objectManager));
// Add the user fieldset, and set it as the base fieldset
$notesFieldset = new NotesFieldset($objectManager);
$notesFieldset->setUseAsBaseFieldset(true);
$this->add($notesFieldset);
// … add CSRF and submit elements …
// Optionally set your validation group here
}
}
Here is the Fieldset class
<?php
namespace Notes\Form;
use Application\Entity\Assigned;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
class NotesFieldset extends Fieldset implements InputFilterProviderInterface
{
protected $inputFilter;
public function __construct(ObjectManager $objectManager)
{
parent::__construct('assigned');
$this->setHydrator(new DoctrineHydrator($objectManager))
->setObject(new Assigned());
$this->add(array(
'type' => 'Zend\Form\Element\Hidden',
'name' => 'id'
));
$this->add(array(
'type' => 'Zend\Form\Element\Text',
'name' => 'loan_number',
'options' => array(
'label' => 'Loan Number'
)
));
$this->add(array(
'type' => 'Zend\Form\Element\Text',
'name' => 'claim_status',
'options' => array(
'label' => 'Claim Status'
)
));
$this->add(array(
'type' => 'Zend\Form\Element\Text',
'name' => 'hold_date',
'options' => array(
'label' => 'Hold Date'
)
));
$this->add(array(
'type' => 'Zend\Form\Element\Text',
'name' => 'vsi_date',
'options' => array(
'label' => 'VSI Date'
)
));
$this->add(array(
'type' => 'Zend\Form\Element\Text',
'name' => 'hold_status',
'options' => array(
'label' => 'Hold Status'
)
));
$this->add(array(
'name' => 'vsi_status',
'attributes' => array(
'type' => 'text',
),
'options' => array(
'label' => 'VSI Status',
),
));
$this->add(array(
'name' => 'submit',
'attributes' => array(
'type' => 'submit',
'value' => 'Go',
'id' => 'submitbutton',
),
));
}
/**
* Define InputFilterSpecifications
*
* #access public
* #return array
*/
public function getInputFilterSpecification()
{
return array(
'id' => array(
'required' => false
),
'name' => array(
'required' => true
),
'loan_number' => array(
'required' => true
),
'claim_status' => array(
'required' => true
),
'hold_date' => array(
'required' => true
),
'hold_status' => array(
'required' => true
),
'vsi_date' => array(
'required' => true
),
'hold_status' => array(
'required' => true
),
);
}
}
It is saying : Class '' does not exist. But the class name is not given in the message.
Beside this
I am using these library for doctrine in my composer.
"doctrine/doctrine-orm-module": "0.7.*",
"doctrine/migrations": "dev-master",
"doctrine/common": "2.4.*#dev",
"doctrine/annotations": "1.0.*#dev",
"doctrine/data-fixtures": "1.0.*#dev",
"doctrine/cache": "1.0.*#dev",
"zf-commons/zfc-user-doctrine-orm": "dev-master",
Please tell me what is missing in my implementation. I have got one open issue in github for doctrine with incomplete example docs. Here is link of issue. If this is the case, then please suggest me ho to implement correctly.
Your missing the target class parameter:
$this->setHydrator(new DoctrineHydrator($objectManager));
Should be:
$this->setHydrator(
new DoctrineHydrator(
$objectManager,
'the\target\entity' // <-- the target entity class name
)
);
The targetClass is used within the DoctrineObject to fetch the metadata of the hydrated entity. You can see this in DoctrineModule\Stdlib\Hydrator\DoctrineObject::__construct():
$this->metadata = $objectManager->getClassMetadata($targetClass);