Doctrine and ZF2: Store ManyToOne relation without cascading - doctrine-orm

I´m struggling with setting up the entities correctly at the moment.
Here´s the situation:
"Country" has one or many "Customers"
I´ve a country table with all countries and I want to save a reference to a country with each customer. Quite simple and often required.
But I´m unable to configure the entities properly. If I don´t define a cascading method in the "Customer" class, I get an exception. If I add a cascading method then the country objects are added as a new record also to the country table but I only want to have a reference to this object in the Customer table.
Customer Class
/**
* #ORM\Entity
* #ORM\Table(name="PS_Customer")
*/
class Customer {
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Country",inversedBy="id")
* #ORM\JoinColumn(name="country_id", referencedColumnName="id")
*/
protected $country;
}
Country Class
/**
* #ORM\Entity
* #ORM\Table(name="PS_Country")
*/
class Country {
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
* #ORM\OneToMany(targetEntity="Customer", mappedBy="country")
*/
protected $id;
/** #ORM\Column(type="string") */
protected $name;
/** #ORM\Column(type="string") */
protected $iso2;
}
If I want to store a Customer object with this definition, I receive the following error:
A new entity was found through the relationship 'Photoshop\Entity\Customer#country' that was not configured to cascade persist operations for entity: Photoshop\Entity\Country#000000004c6f8efb00000000b695d273. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Photoshop\Entity\Country#__toString()' to get a clue.
ActionController (extracted):
$forms = $this->getServiceLocator()->get('FormElementManager');
$form = $forms->get('Photoshop\Form\CheckoutForm');
$customer = new Customer;
$form->bind($customer);
$order = new Order;
$order->setCustomer($customer);
// Order object is put into a session during checkout process here...
/**
* Commit Order to database
*/
$em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$em->persist($sessionCheckout->order); // Fetch the order object from session
$em->flush();
Checkout Form
class CheckoutForm extends Form implements ObjectManagerAwareInterface {
protected $objectManager;
public function __construct() {
parent::__construct('checkout');
}
public function init() {
$this->setAttribute('action', 'checkout');
$this->setAttribute('method', 'post');
$this->setHydrator(new DoctrineHydrator($this->getObjectManager()));
$this->setInputFilter(new \Photoshop\Form\CheckoutFilter());
$this->add(array(
'name' => 'country',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'label' => 'Country:',
'empty_option' => 'Please choose...',
'object_manager' => $this->getObjectManager(),
'target_class' => 'Photoshop\Entity\Country',
'property' => 'name',
),
));
$this->add(array(
'type' => 'Zend\Form\Element\Select',
'name' => 'gender',
'options' => array(
'label' => 'Title*:',
'empty_option' => 'Please choose...',
'value_options' => array(
'f' => 'Mrs.',
'm' => 'Mr.'
),
)
));
$this->add(array(
'name' => 'firstName',
'attributes' => array(
'type' => 'text',
'id' => 'firstName'
),
'options' => array(
'label' => 'First name*:'
),
));
$this->add(array(
'name' => 'lastName',
'attributes' => array(
'type' => 'text',
'id' => 'lastName'
),
'options' => array(
'label' => 'Last name*:'
),
));
$this->add(array(
'name' => 'submit',
'attributes' => array(
'type' => 'submit',
'value' => 'Pay with PayPal or Credit Card now',
'class' => 'btn btn-primary btn-lg btn-block'
)
));
}
public function setObjectManager(ObjectManager $objectManager) {
$this->objectManager = $objectManager;
}
/**
* Get the object manager
*
* #return ObjectManager
*/
public function getObjectManager() {
return $this->objectManager;
}
}
I´m quite sure that it will be simple to solve. But I can´t see the solution at the moment :)
Maybe somebody can give me a hint?!? Would appreciate that...
Thanks,
Michael

// Order object is put into a session during checkout process here... ---- thats the important part
So if I understand, you create order and customer in one request, then transfer it through session to some other request and persist it there. What really happens is that you have object graph like order->customer->country, where first two are new entities, so serializing unserializing does nothing wrong with them, but country is managed entity already existing in DB. By serializing it into session it is detached from entity manager and after unserializing it, it is presented to new entity manager instance, which does not know that it was once managed, so decides to persist is as new one.
Usually you need to merge unserialized entity to current entity manager
$managedOrder = $em->merge($sessionCheckout->order);
and work with $managedOrder. For this to work you might need to set cascade={"merge"} on Customer::country and on Order::customer.
Doctrine has Entities in session documentation page on this subject.

Related

How to instantiate new objects from Table Inhertitance in Laminas API?

I am using Doctrine2 within a Laminas API project to create a table inheritance like this:
Content:
/**
* #ORM\Entity()
* #ORM\Table(name="content")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({"content" = "Content", "image"="Image", "video"="Video"})
*/
class Content {
/**
* #ORM\Id
* #ORM\Column(name="id")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\Column(name="name", type="string")
*/
protected $name;
[... some other fields and getters and setters]
}
Image:
/**
* #ORM\Entity()
* #ORM\Table(name="image")
*/
class Image extends Content {
/**
* #ORM\Column(name="filesize", type="integer")
*/
protected $filesize;
[... some other fields and getters and setters]
}
I want to create new Images and Videos by posting data to the /content endpoint in my app. Based on the type of the file I create either a new Video() or a new Image(). But when I call new Image() in my Mapper, I get the following error:
Unable to determine entity identifier for object of type
"App\V1\Entity\Image"; no fields matching "id"
I tried to duplicate the fields and Getters and Setters of content including the id to the image object with the same result. I also created a new endpoint /images and tried posting image data to that endpoint, also with the same result.
I use the same setup in the Laminas-MVC variant of this app, which I now want to migrate to Laminas-API. In the MVC app, this works.
How can I create new images?
[UPDATE]
Here is the API tools hal config:
'api-tools-hal' => [
'metadata_map' => [
DNSad\V1\Entity\Content::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.content',
'route_identifier_name' => 'content_id',
'hydrator' => \Doctrine\Laminas\Hydrator\DoctrineObject::class,
],
DNSad\V1\Collection\Content::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.content',
'route_identifier_name' => 'content_id',
'is_collection' => true,
],
DNSad\V1\Entity\Image::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.image',
'route_identifier_name' => 'image_id',
'hydrator' => \Doctrine\Laminas\Hydrator\DoctrineObject::class,
],
DNSad\V1\Collection\Images::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.image',
'route_identifier_name' => 'image_id',
'is_collection' => true,
],
],
I tried different hydrators and configs for the image object, but the problem still persists. Is there a special configuration necessary to set up the table inheritance here ?
[EDIT]
Removing 'entity_identifier_name' => 'id', from the image entity config solves the issue

How to reference child elements in a Many-to-One relationship in ZF2 and Doctrine

If I develop a project using ZF2 and Doctrine that hydrates an object with a Many-to-Many relationship similar this Doctrine hydrator tutorial, the parent fieldset would look like this:
namespace Application\Form;
use Application\Entity\BlogPost;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
class BlogPostFieldset extends Fieldset implements InputFilterProviderInterface
{
public function __construct(ObjectManager $objectManager)
{
parent::__construct('blog-post');
$this->setHydrator(new DoctrineHydrator($objectManager))
->setObject(new BlogPost());
$this->add(array(
'type' => 'Zend\Form\Element\Text',
'name' => 'title'
));
$tagFieldset = new TagFieldset($objectManager);
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'tags',
'options' => array(
'count' => 2,
'target_element' => $tagFieldset
)
));
}
public function getInputFilterSpecification()
{
return array(
'title' => array(
'required' => true
),
);
}
}
and the form elements could be accessed in the view like this:
// edit.phtml:
// ...
$bpfs=$form->get('blog-post');
$tfss=$bpfs->get('tags')->getFieldsets();
$tfs=$tfss[0];
$tagName = $tfs->get('name');
// ...
However, if I want to use Many-to-One relationship, I'm not sure how to code the child elements. In the BlogPost Fieldset, I assume that the tag element is no longer a collection because there will only be one of them. Yet the tag is still a fieldset, so I guess that it goes into the BlogPost Fieldset like this:
$tagFieldset = new TagFieldset($objectManager);
$this->add(array(
'name' => 'tag',
'options' => array(
'target_element' => $tagFieldset
)
));
(It's a single record, so I've changed the name to tag. It's not a collection, nor does it seem to be any other ZF2 form elements, so I've dropped the type attribute statement.)
Then in the view, I attempt to access the form elements like this:
// edit.phtml:
// ...
$bpfs=$form->get('blog-post');
$tfs=$bpfs->get('tag')->getFieldsets();
$tagName = $tfs->get('name');
// ...
but this gives the error,
Fatal error: Call to undefined method Zend\Form\Element::getFieldsets() in …
How should this be coded properly?
Since tag is just one fieldset, you should do this:
$tfs=$bpfs->get('tag');
$tagName = $tfs->get('name');

How to pass ArrayCollection to DoctrineModule\Form\Element\ObjectSelect

I tried pretty much everything I found by searching here and at Google too but still no luck.
I have User entity with ManytoMany relation with Countries, here is it:
/**
* #var \Doctrine\Common\Collections\Collection
* #ORM\ManyToMany(targetEntity="Admin\Entity\Country", cascade={"persist", "remove"})
* #ORM\JoinTable(name="user_country_linker",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="country_id", referencedColumnName="id")}
* )
*/
protected $countries;
Now I'm trying to display DoctrineModule\Form\Element\ObjectSelect with allowed/ assigned countries only. I do have this list available by calling $this->zfcUserAuthentication()->getIdentity()->getCountries().
Is there any way to pass this ArrayCollection to ObjectSelect form element?
$this->add(array(
'name' => 'country',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'label' => 'Country',
'object_manager' => $em,
'target_class' => '\Admin\Entity\Country',
'property' => 'code',
'find_method' => array(
'name' => 'findBy',
'params' => array(
'criteria' => array(),
'orderBy' => array('id' => 'asc'),
),
),
'column-size' => 'sm-10',
'label_attributes' => array('class' => 'col-sm-2'),
'help-block' => 'Select country where the entity is present'
),
'attributes' => array(
'required' => false
)
));
Many thanks for the help, I really appreciate it!
How to fill a Dropdown in your controller is best described here: zf2 create select/drop down box and populate options in controller?. This is basically AlexP's solution.
If this is not what you are looking for, maybe the method described by this post can help you. At least it could help others like me that were looking for a solution like this: http://samsonasik.wordpress.com/2014/05/22/zend-framework-2-using-doctrinemoduleformelementobjectselect-and-custom-repository/
You basically create a custom reposity which holds a custom query to retrieve possible solutions:
namespace Your\Repository;
use Doctrine\ORM\EntityRepository;
class CountriesRepository extends EntityRepository
{
public function getPossibleCountries()
{
$querybuilder = $this->_em
->getRepository($this->getEntityName())
->createQueryBuilder('c');
return $querybuilder->select('c')//... define your query here
->getQuery()->getResult();
}
}
You can then refer to that method in your ObjectSelect:
$this->add(array(
'name' => 'continent',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'object_manager' => $this->entityManager,
'target_class' => 'Your\Entity\User',
'property' => 'contries',
'is_method' => true,
'find_method' => array(
'name' => 'getCountries',
),
),
));

Doctrine 2 ORM Zend 2 Form Many to Many Example

Anyone have a good and complete example for many to many relation using ZF2 and Doctrine 2, especially when using ObjectMultiCheckBox ?
I found this tutorial - https://github.com/doctrine/DoctrineModule/blob/master/docs/hydrator.md but it don't explain how to do a many to many relation.
K so I figured out how todo this eventually after realizing the hydrator wasn't binding associations and I had to create the link. I'm going to put a full blog post up explaining it but in case you're still stuck. If you want to look at all the code together I've got it up on github (link) mind you I'm actively learning/developing/cleaning it up so its a bit messy
Basically you need to collect the selected models, create the association link by adding them to the entity, then persist the entity WITH the cascade add/del active on Doctrine (or manually do it by persisting the link's before the entity).
Below example is a Many-to-Many between my Posts and Categories
You need to have cascade active on your entity's property
/**
* #ORM\OneToMany(targetEntity="CategoryPostAssociation", mappedBy="category", cascade={"persist", "remove"})
*/
protected $category_post_associations;
You need to push the object manager from your Form to your fieldsets
PostFieldSet
$categoryFieldset = new CategoryFieldset($objectManager);
$this->add(array(
'type' => 'DoctrineModule\Form\Element\ObjectMultiCheckbox',
'name' => 'categories',
'options' => array(
'label' => 'Select Categories',
'object_manager' => $objectManager,
'should_create_template' => true,
'target_class' => 'OmniBlog\Entity\Category',
'property' => 'title',
'target_element' => $categoryFieldset,
),
));
and categoryfieldset just has a textbox of the Title.
In my PostController's AddAction
public function addAction() {
// Get your ObjectManager
$objectManager = $this->getEntityManager();
//Create the form and inject the ObjectManager
//Bind the entity to the form
$form = new PostForm($objectManager);
$post = new Post();
$form->bind($post);
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
/*
* Get IDs from form element
* Get categories from the IDs
* add entities to $post's categories list
*/
$element = $form->getBaseFieldset()->get('categories'); //Object of: DoctrineModule\\Form\\Element\\ObjectMultiCheckbox
$values = $element->getValue();
foreach($values as $catID){
$results = $objectManager->getRepository('OmniBlog\Entity\Category')->findBy(array('id' => $catID));
$catEntity = array_pop($results);
$link = $post->addCategory($catEntity);
//Entity/Post 's association table cascades persists and removes so don't need to persist($link), but would be done here
}
$objectManager->persist($post);
$objectManager->flush();
return $this->redirect()->toRoute(
static::ROUTE_CHILD,
array('controller' => static::CONTROLLER_NAME
));
}
}
return array('form' => $form);
}
Above if you see $post->addCategory($catEntity); this leads to my Entity or Model managing the linking (I was passing back the link encase I want to handle the cascading manually)
Post
/**
* Returns the created $link
*/
public function addCategory(Category $category){
$link = new CategoryPostAssociation();
$link->setCategory($category);
$link->setPost($this);
$this->addCategoryPostAssociations($link);
return $link;
}
Found an good blog where it is explained step by step what is need to be done - http://laundry.unixslayer.pl/2013/zf2-quest-zendform-many-to-many/

ZF2 tutorial with SpiffyDoctrine

As a New Year's Day hackathon thought I would take Rob Allen's great zend framework 2 beta tutorial and substitute doctrine2 instead of Zend\Db\Table using the ZF2 modules SpiffyDoctrine and SpiffyDoctrineORM.
Everything was going pretty well, got the entity manager going and set up my entity:
<?php
namespace AlbumDoc\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="album")
*/
class Album {
/**
* #ORM\Id
* #ORM\Column(type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #ORM\Column(type="string")
*/
public $artist;
/**
* #ORM\Column(type="string")
*/
public $title;
/**
* global getter
*
* #param type $property
* #return type
*/
public function __get($property) {
return $this->$property;
}
/**
* Global setter
*
* #param type $property
* #param type $value
*/
public function __set($property, $value) {
$this->$property = $value;
}
}
As I'm new to Doctrine and Zend Framework for that mater, I thought I would do a test to see if I could make the Entity Manager save to the database. I setup my code in the indexController as follows:
$em = $this->getLocator()->get('doctrine_em');
$album = new \Application\Entity\Album();
$album->artist = 'Art Ist';
$album->title = 'Cool Title';
$em->persist($album);
$em->flush();
The problem I'm having it that when this code runs I get the following error:
Class Application\Entity\Album is not a valid entity or mapped super class.
From the limited stuff I have found I think the problem has something to do with Doctrine not knowing the entity path or something to do with the AnnotationDriver.
Guessing there is something that needs to be added to the Album module's config file but can't find what.
Updated: Since I don't have enough reputation points to post the answer formally I'll add the answer here.
Found the solution. There were actually two parts to what was going wrong.
First was a stupid mistake, I forgot to take the .dist off the end of the module.spiffy_doctrine_orm.config.php file that gets dropped into the applications config/autoload directory.
Second part was in this file, I didn't alter the driver settings of the settings array to point to:
'driver' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'namespace' => 'AlbumDoc\Entity',
'paths' => array('module/AlbumDoc/src/AlbumDoc/Entity')
)
This does beg the question of whether it would be possible for each module to hold it's own entities, if you are setting a global entity path for your application. But that can wait for another day.
(Anwering the second question)
Yes this is possible and (in my oppinion) the way to go. Your module's config just has to return a subset of the orm-configuration you want to change/extend.
Unfortunatelly Doctrine's drivers don't seem to support multiple namespaces. Therefore you'll have to add a new driver for each namespace (not sure about this, correct me if i'm wrong :)
To add a new driver, let you module's config contain something like:
return array(
'di' => array(
'instance' => array(
'orm_driver_chain' => array(
'parameters' => array(
'drivers' => array(
'mymodule' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'namespace' => __NAMESPACE__ . '\Entity',
'paths' => array(
__DIR__ . '/../src/' . __NAMESPACE__ . '/Entity'
)
)
)
)
)
),
));
This will add a new driver to the configuration without touching any non-module related config-files.