Doctrine persisting Many-To-One entities - php

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 ;-)

Related

Symfony - link one main entity to a second one through a form

I am currently using doctrine wrong as when I have a choiceType in my entity, I use this:
/**
* #ORM\Column(type="integer")
*/
private $type;
plus in my builder:
->add('type', ChoiceType::class, ['choices' => ['Pattern' => 0, 'Image' => 1]])
I would like now to have something cleaner and have another entity in my database that would be linked to this main entity. Here is what I did.
Created a "category" entity (and created in DB):
/**
* #ORM\Entity(repositoryClass="App\Repository\CategoryRepository")
* #ORM\Table(name="vipbox_mep_category")
*/
class Category {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $category;
/**
* #return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* #param mixed $category
*/
public function setCategory($category): void
{
$this->category = $category;
}
}
linked my main entity to the second one (with the getter and setter):
/**
* #ORM\OneToOne(targetEntity="App\Entity\Category")
*/
private $category;
Added my category to my form:
->add('category', CategoryType::class, ['required' => true, 'label' => 'Category'])
Extended AbstractType form for my category:
class CategoryType extends AbstractType
{
/**
* #var RouterInterface $route
*/
private $router;
/**
* #param RouterInterface $router
*/
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'required' => true,
'label' => 'Category'
]);
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
}
Populated my Category table with text sample data (2 rows)
Tested to show my form, I does compute, but I have an empty choiceType:
I know I missed one/multiple things on my way, any clues ?
Assuming you have saved some data in your Category table to populate your drop-down list just change your step 3 from this:
->add('category', CategoryType::class, ['required' => true, 'label' => 'Category'])
to this:
->add('category', EntityType::class, [
'class' => Category::class,
'required' => true,
'label' => 'Category',
])
There are lots of other options available for EntityType, see the docs.

Php Dependency Injecttion in Zend 2

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?

Adding choices in entityType

I'm learning by myself the symfony framework (my job is not about developing, I'm not a developer) and I find out most of case the solution but here, is one what I didn't know how to manage.
I have 2 entity :
Product:
/**
* Product
*
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\ProductRepository")
* #UniqueEntity("productNumber")
*/
class Product
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="productNumber", type="string", length=255, unique=true)
* #Assert\Regex(
* pattern="/[0-9][.][0-9]{3}/",
* message="It should be like 1.234"
* )
*/
private $productNumber;
/**
* #ORM\ManyToOne(targetEntity="ProductGroup")
*/
private $productGroup;
/**
* Constructor
*/
public function __construct()
{
}
}
Camera :
/**
* Camera
*
* #ORM\Table(name="camera")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\CameraRepository")
*/
class Camera
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
*/
private $description;
/**
*
* #ORM\ManyToOne(targetEntity="Product")
*/
private $product;
/**
* #ORM\ManyToMany(targetEntity="CustomField", inversedBy="camera", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $customFields;
/**
* Constructor
*/
public function __construct()
{
$this->customFields = new ArrayCollection();
}
}
My form :
namespace ProductBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
class CameraType extends AbstractType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('product', EntityType::class, [
'class' => 'ProductBundle:Product',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
},
'attr' => [
'required' => true,
],
'choice_label' => 'productNumber',
])
->add('modele', TextType::class, [
'label' => "Modele",
])
->add('description', TextType::class, [
'label' => "Description",
])
->add('customFields', CollectionType::class, [
'entry_type' => CustomFieldType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'required' => false,
'attr' => [
'class' => 'customfield'
]
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ProductBundle\Entity\Camera'
));
}
}
When I add a camera, I would like only the Product:productNumber where are available (not take by a camera), the querybuilder is working but my issue concern the edit form, it show only available productNumber so it's changing every time I need to edit this camera.
What can I handle this ? Should I try to found another way to add a productNumber ? do you have a "trick" ?
I hope you will understand the problem and my english because it's not my first language.
Have a nice day.
Edit : I'm on Symfony 3.1.4
I presume on new form your choice field shows only unused ProductBundle:Camera entity, and on edit form it should show saved ProductBundle:Camera entity and all unused ones.
You should look into Form Event Subscribers
You need to implement two event listeners PRE_SET_DATA and PRE_SUBMIT.
Here is one way to do it. Something like this works on SF 2.8
First you will have to create product entity form from custom ProductFieldSubscriber which becomes EventSubscriberInterface:
$builder->addEventSubscriber(new ProductFieldSubscriber('product', [])
Now ProductFieldSubscriber should look something like this (untested)
namespace ProductBundle\Form\EventListener;
use Symfony\Component\Form\FormInterface,
Symfony\Component\Form\FormEvent,
Symfony\Component\EventDispatcher\EventSubscriberInterface,
Symfony\Component\Form\FormEvents,
Doctrine\ORM\EntityRepository,
Symfony\Bridge\Doctrine\Form\Type as DoctrineTypes
;
class ProductFieldSubscriber implements EventSubscriberInterface
{
private $propertyPathToSelf;
public function __construct($propertyPathToSelf, array $formOptions=[]) {
$this->propertyPathToSelf = $propertyPathToSelf;
$this->formOptions = $formOptions;
}
public static function getSubscribedEvents() {
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit',
];
}
private function addForm(FormInterface $form, $selfId = null) {
$formOptions = array_replace_recursive ([
'class' => 'ProductBundle:Product',
'placeholder' => null,
'compound' => false,
'query_builder' => function (EntityRepository $er) use ($selfId) {
$qb = $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
if (null !== $selfId) {
$qb
->orWhere($qb->expr()->eq('p.product', ':existingId'))
->setParameter('existingId', $selfId->getId())
;
}
return $qb;
},
],
$this->formOptions
);
if ($selfId) {
$formOptions['data'] = $selfId;
}
$form->add($this->propertyPathToSelf, DoctrineTypes\EntityType::class, $formOptions);
}
public function onPreSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$selfIdTypeMethod = "get{$this->propertyPathToSelf}";
$selfId = $data->$selfIdTypeMethod();
$this->addForm($form, $selfId);
}
public function onPreSubmit(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
$selfId = array_key_exists($this->propertyPathToSelf, $data) ? $data[$this->propertyPathToSelf] : null;
$this->addForm($form, $selfId);
}
}
Query builder would be simpler if you had mapped entity relations.
Bonus update:
form option 'placeholder' => null, takes care that no default 'empty' option is available.
form option 'required' => true, forces html5 form popup validation.
Then you should use something like entity #assert notations and use validator constraints on entity attribute:
use Symfony\Component\Validator\Constraints as Assert;
/**
* #var string
*
* #Assert\NotNull()
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
You could also disallow edit form from opening via controller editAction (maybe some redirect) and twig, where you could hide edit button.

Fieldset validation data

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.

Doctrine Mapping Exception in Zend Framework 2's Project while using hydrator

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);

Categories