ZF2 + Doctrine 2 - Entity created when requirements not met and values empty - php

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.

Related

Zend3 Form Filter - ParamConverter could not generate valid form

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...

Use same entity field multiple times with different admin code in sonata form

I'm using Sonata admin in my Symfony project. I have 2 entities like Parent and Child. Parent entity is connected to child by one-to-many relationship.
I have created 2 admin classes for child entity with different baseRoutName. I need to use Child entity fields in Parent entity sonata form for 2 times.
//ParentAdmin.php
$formMapper
->with('Child 1', ['class' => 'col-md-4'])
->add('child', CollectionType::class, [], [
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'admin_code' => 'admin.child1'
])
->end()
->with('Child 2', ['class' => 'col-md-4'])
->add('child', CollectionType::class, [], [
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'admin_code' => 'admin.child2'
])
->end();
The problem here is that I need to use child field for multiple times. But the child field within Child 2 is overriding the child field in Child 1. As you can see I have used different admin_code for these 2 fields.
My expected output is,
But the actual output I'm getting is,
I know the problem here is duplicate entity fields. Is it possible to display same field for multiple times?
Does anyone have solution/suggestion? Thanks in advance!!
maybe it's late but i had the same problem, found this post with no answer, and finally found a solution, so here it is for future purpose.
i have a Request class with generated document, get an eye on get and add functions:
class Request
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="GeneratedDocument", mappedBy="request", cascade={"all"}, orphanRemoval=true)
*/
protected $generatedDocuments;
/**
* #var ArrayCollection
* each of this attributes will get one type of $generatedDocuments
*/
protected $generatedAttestationDocuments;
protected $generatedCertificationDocuments;
public function __construct()
{
$this->generatedDocuments = new ArrayCollection();
}
/**
* Set the value of id.
*
* #param integer $id
* #return \App\Entity\Request
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get the value of id.
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedDocuments(): Collection
{
return $this->generatedDocuments;
}
public function addGeneratedDocument(GeneratedDocument $generatedDocument): self
{
if (!$this->generatedDocuments->contains($generatedDocument)) {
$this->generatedDocuments[] = $generatedDocument;
$generatedDocument->setRequest($this);
}
return $this;
}
public function removeGeneratedDocument(GeneratedDocument $generatedDocument): self
{
if ($this->generatedDocuments->contains($generatedDocument)) {
$this->generatedDocuments->removeElement($generatedDocument);
// set the owning side to null (unless already changed)
if ($generatedDocument->getRequest() === $this) {
$generatedDocument->setRequest(null);
}
}
return $this;
}
/**
* #return Collection|GeneratedDocument[]
*
* #param int $type
*/
protected function getTypedGeneratedDocuments(int $type): Collection
{
return $this->getGeneratedDocuments()->filter(function (GeneratedDocument $gd) use ($type) {
if ($gd->getGeneratedDocumentModel()) {
return $type === $gd->getGeneratedDocumentModel()->getType();
}
return false;
});
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedAttestationDocuments(): Collection
{
if (empty($this->generatedAttestationDocuments)) {
$this->generatedAttestationDocuments =
$this->getTypedGeneratedDocuments(GeneratedDocumentModel::TYPE_ATTESTATION);
}
return $this->generatedAttestationDocuments;
}
public function addGeneratedAttestationDocument(GeneratedDocument $generatedDocument): self
{
$this->generatedAttestationDocuments[] = $generatedDocument;
return $this->addGeneratedDocument($generatedDocument);
}
public function removeGeneratedAttestationDocument(GeneratedDocument $generatedDocument): self
{
return $this->removeGeneratedDocument($generatedDocument);
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedCertificationDocuments(): Collection
{
if (empty($this->generatedCertificationDocuments)) {
$this->generatedCertificationDocuments =
$this->getTypedGeneratedDocuments(GeneratedDocumentModel::TYPE_CERTIFICATE);
}
return $this->generatedCertificationDocuments;
}
public function addGeneratedCertificationDocument(GeneratedDocument $generatedDocument): self
{
$this->generatedCertificationDocuments[] = $generatedDocument;
return $this->addGeneratedDocument($generatedDocument);
}
public function removeGeneratedCertificationDocument(GeneratedDocument $generatedDocument): self
{
return $this->removeGeneratedDocument($generatedDocument);
}
}
Then in admin you have to give a different name in each add, here I use my typed generated documents so there's no duplication problem, but they are not mapped and symfony complain about it.
Trying to use 'mapped' => false resolved nothing, the simplest way I found was a 'virtual mapping', based on the original mapped attribute 'generatedDocuments, just the time to fool symfony when building the form.
class RequestAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper): void
{
/** #var Request $createdRequest */
$createdRequest = $this->getSubject();
$metaData = $this->getModelManager()->getMetadata($this->getClass());
//We need many CollectionType based on 'generatedDocuments', and we need an ArrayCollection for each of them
//so here is a virtual mapping to make symfony accept the persistence of our CollectionType
//then setter should fill 'generatedDocuments'
$mapping = $metaData->getAssociationMappings()['generatedDocuments'];
$mapping['fieldName'] = 'generatedAttestationDocuments';
$metaData->mapOneToMany($mapping);
$mapping['fieldName'] = 'generatedCertificationDocuments';
$metaData->mapOneToMany($mapping);
$formMapper
->with(('Attestation Deposit'))
->add('generatedAttestationDocuments', CollectionType::class, [
'label' => false,
'by_reference' => false,
'btn_add' => 'Add Attestation',
'data' => $createdRequest->getGeneratedAttestationDocuments(),
], [
'edit' => 'inline',
'inline' => 'table',
'admin_code' => 'admin.generated_document_attestation',
])
->end()
->with(('Certificate'))
->add('generatedCertificationDocuments', CollectionType::class, [
'label' => false,
'by_reference' => false,
'btn_add' => 'Add Certification',
'data' => $createdRequest->getGeneratedCertificationDocuments(),
], [
'edit' => 'inline',
'inline' => 'table',
'admin_code' => 'admin.generated_document_certification',
])
->end()
//delete virtual mapping to avoid it to get handle like a real mapping
unset($metaData->associationMappings['generatedAttestationDocuments']);
unset($metaData->associationMappings['generatedCertificationDocuments']);
}
}
I would like to know a simplest way, but It really work like individual CollectionType for me.
Hope it will help!

ZF2 injecting InputFilter into Fieldset not working automatically

I'm building a small application with ZF2 and Doctrine2. Setting it up in such a way as to have a lot of reusable code and technique. However, getting stumped by the fact that my InputFilter is not automatically injected into the Fieldset that it should get associated to.
I've confirmed that the Form that uses the Fieldset works (without the InputFilter). The InputFilter is also visible as present during debugging.
The question then, what am I doing wrong and how to fix having a separate InputFilter, coupled to a Fieldset in ZF2?
Sidenotes:
1 - I am aware that by using the InputFilterInterface I could have the InputFilter inside of the Fieldset class with the getInputFilterSpecification() function. However, as I'm trying to keep it DRY and reusable, it wouldn't do to have to copy it if I were to create an API that needs to use the Entity and InputFilter, but can only have the latter coupled with a Fieldset.
2 - A lot of Abstract classes are used, where used I'll indicate in the snippets what they have that's relevant
3 - The problem line is in CustomerFieldsetFactory.php
=========================================================================
Entity: Customer.php
/**
* Class Customer
* #package Customer\Entity
*
* #ORM\Entity
* #ORM\Table(name="customers")
*/
class Customer extends AbstractEntity //Contains $id
{
/**
* #var string
* #ORM\Column(name="name", type="string", length=255, nullable=false)
*/
protected $name;
}
Form: CustomerForm.php
class CustomerForm extends AbstractForm
{
public function __construct($name = null, array $options)
{
parent::__construct($name, $options); // Adds CSRF
}
public function init()
{
$this->add([
'name' => 'customer',
'type' => CustomerFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
//Call parent initializer. Check in parent what it does.
parent::init(); //Adds submit button if not in form
}
}
Fieldset: CustomerFieldset.php
class CustomerFieldset extends AbstractFieldset //Contains EntityManager property and constructor requirement (as we're managing Doctrine Entities here)
{
public function init()
{
$this->add([ //For now id field is here, until InputFilter injection works
'name' => 'id',
'type' => Hidden::class,
'attributes' => [
'id' => 'entityId',
],
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => _('Name'),
],
]);
}
}
InputFilter: CustomerInputFilter.php
class CustomerInputFilter extends AbstractInputFilter
{
public function init()
{
parent::init();
$this->add([
'name' => 'name',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]);
}
}
Above the classes. Below the Factories
FormFactory: CustomerFormFactory.php
class CustomerFormFactory implements FactoryInterface, MutableCreationOptionsInterface
{
/**
* #var array
*/
protected $options;
/**
* #param array $options
*/
public function setCreationOptions(array $options)
{
//Arguments checking removed
$this->options = $options;
}
/**
* #param ServiceLocatorInterface|ControllerManager $serviceLocator
* #return CustomerForm
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$serviceManager = $serviceLocator->getServiceLocator();
$form = new CustomerForm($this->options['name'], $this->options['options']);
$form->setTranslator($serviceManager->get('translator'));
return $form;
}
}
FieldsetFactory: CustomerFieldsetFactory.php
class CustomerFieldsetFactory implements FactoryInterface, MutableCreationOptionsInterface
{
/**
* #var string
*/
protected $name;
public function setCreationOptions(array $options)
{
//Argument checking removed
$this->name = $options['name'];
}
public function createService(ServiceLocatorInterface $serviceLocator)
{
$serviceManager = $serviceLocator->getServiceLocator();
$fieldset = new CustomerFieldset($serviceManager->get('Doctrine\ORM\EntityManager'), $this->name);
$fieldset->setHydrator(new DoctrineObject($serviceManager->get('doctrine.entitymanager.orm_default'), false));
$fieldset->setObject(new Customer());
$fieldset->setInputFilter($serviceManager->get('InputFilterManager')->get(CustomerInputFilter::class));
//RIGHT HERE! THE LINE ABOVE IS THE ONE THAT DOES NOT WORK!!!
return $fieldset;
}
}
InputFilterFactory: CustomerInputFilterFactory.php
class CustomerInputFilterFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$repository = $serviceLocator->getServiceLocator()
->get('Doctrine\ORM\EntityManager')
->getRepository(Customer::class);
return new CustomerInputFilter($repository);
}
}
Config: module.config.php
'controllers' => [
'factories' => [
CustomerController::class => CustomerControllerFactory::class,
],
],
'form_elements' => [
'factories' => [
CustomerForm::class => CustomerFormFactory::class,
CustomerFieldset::class => CustomerFieldsetFactory::class,
],
],
'input_filters' => [
'factories' => [
CustomerInputFilter::class => CustomerInputFilterFactory::class,
],
],
'service_manager' => [
'invokables' => [
CustomerControllerService::class => CustomerControllerService::class,
],
],
I am hoping one of you can help me out here.
EDIT: Update with actual error
The following line in the CustomerFieldset.php (above) triggers the error.
$fieldset->setInputFilter($serviceManager->get('InputFilterManager')->get(CustomerInputFilter::class));
The error:
Fatal error: Call to undefined method Customer\Fieldset\CustomerFieldset::setInputFilter() in D:\htdocs\server-manager\module\Customer\src\Customer\Factory\CustomerFieldsetFactory.php on line 57
As seen in the above snippet, the InputFilter (and it's Factory) are known the the InputFilterManager.
The error states it does not know the getInputFilter() function on the Fieldset. Which is correct in a way, it doesn't exist. The question then is, how to have the function exist so that injecting the InputFilter will work, or how to bind this InputFilter to the Fieldset?
EDIT 2: Update based on Wilt's answer
Added use InputFilterAwareTrait to Abstract class AbstractInputFilter to create following (from answer):
use Zend\InputFilter\InputFilterAwareTrait;
abstract class AbstractFieldset extends Fieldset
{
use InputFilterAwareTrait;
// ... Other code
}
Turns out that I had another mistake in the (original) code above as well:
In file module.config.php the input_filters should've been input_filter_specs. This was (after using the Trait) a Invalid Factory registered error (titled ServiceNotCreatedException).
The following might be of use to someone, the Factory to create a Fieldset with Hydrator, Object and Inputfilter has the following createService() function:
public function createService(ServiceLocatorInterface $serviceLocator)
{
/** #var ServiceLocator $serviceManager */
$serviceManager = $serviceLocator->getServiceLocator();
/** #var CustomerRepository $customerRepository */
$customerRepository = $serviceManager->get('Doctrine\ORM\EntityManager')->getRepository(Customer::class);
$fieldset = new CustomerFieldset($serviceManager->get('Doctrine\ORM\EntityManager'), $this->name);
$fieldset->setHydrator(new DoctrineObject($serviceManager->get('doctrine.entitymanager.orm_default'), false));
$fieldset->setObject(new Customer());
$fieldset->setInputFilter($serviceManager->get('InputFilterManager')->get(CustomerInputFilter::class, $customerRepository));
return $fieldset;
}
There is lots of information added to your question. I'd suggest you try to narrow down your question in the future. Read more on the guidelines for a good question here: How to create a Minimal, Complete, and Verifiable example.
Zend framework provides a InputFilterAwareTrait with both the setInputFilter and getInputFilter methods. You can easily implement/use this trait inside your CustomerFieldset class:
use Zend\InputFilter\InputFilterAwareTrait;
class CustomerFieldset extends AbstractFieldset
{
use InputFilterAwareTrait;
//...remaining code
}
In case you want the inputfilter in all classes that extend your abstract AbstractFieldset class you could also decide to add the trait there:
use Zend\InputFilter\InputFilterAwareTrait;
class AbstractFieldset
{
use InputFilterAwareTrait;
//...remaining code
}
See the following question: How to validate nested fieldsets. Fieldsets don't contain InputFilters but you should extend your base InputFilter of your form.
Create an InputFilter for each Fieldset and add them, with the same name as your fieldset, to your InputFilter of your form. As seen within the answer of the other question I linked.
If you don't want to do this you might consider working with InputSpecification.

ZF2 and Doctrine 2: Translation entity form

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.

form select parent hydration

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.

Categories