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/
Related
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');
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.
I can't help myself and it's currently annoying, and yes, I used google a lot.
What I need:
A twitterlike follow button with the action to follow user.
What I already did:
Database
users table: id, username, password, ...
users_users table: id, user_id, follower_id
Code
In model User.php
public $hasAndBelongsToMany = array(
'Follower' => array(
'className' => 'User',
'joinTable' => 'users_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'follower_id',
'unique' => 'keepExisting',
)
);
In UsersController.php
public function follow() {
/*need help here*/
}
In Users\index.ctp
<?php if ($current_user['id'] != $user['User']['id']) echo $this->Html->link('Follow', array('action' => 'follow', $user['User']['id'])); ?>
Personally, I don't find hasAndBelongsToMany to be a good fit for situations like this. It's a good fit for when you want to display a list of checkboxes, or a select list, and allow the user to select/manage all their followings (or whatever the relationships might be) in one form.
It might just be my personal preference, but in situations like yours, where you're adding/deleting single links without worrying about any of the other links related to that user, I prefer to just create a separate 'Relationships' (or similarly named) Model / Controller, and consider the records as things in their own right, as opposed to just hasAndBelongsToMany links that are all sort of 'automagically' managed.
Here's how I'd do it:
Name your users_users table 'relationships'. And name the columns 'followed_by_id' and 'following_id' (or similar) to avoid any ambiguity as to which user is the follower / followee (if that was a word!).
In your users Model, you'd have these relationships:
var $hasMany = array(
'Followers' => array(
'className' => 'Relationship',
'foreignKey' => 'following_id',
'dependent'=> true
),
'FollowingUsers' => array(
'className' => 'Relationship',
'foreignKey' => 'followed_by_id',
'dependent'=> true
),
);
Then you'd have a Relationships model that looks something like this (the $belongsTo relationships are the important part):
<?php
class Relationship extends AppModel {
var $name = 'Relationship';
var $validate = array(
'followed_by_id' => array(
'numeric' => array(
'rule' => array('numeric'),
),
),
'following_id' => array(
'numeric' => array(
'rule' => array('numeric'),
),
),
);
var $belongsTo = array(
'FollowedBy' => array(
'className' => 'User',
'foreignKey' => 'followed_by_id'
),
'Following' => array(
'className' => 'User',
'foreignKey' => 'following_id'
)
);
}
?>
And then in your Relationships controller, you'd have something like this:
function add($following_id = null) {
$this->Relationship->create();
$this->Relationship->set('followed_by_id',$this->Auth->User('id'));
$this->Relationship->set('following_id',$following_id);
if ($this->Relationship->save($this->data)) {
// all good
} else {
// You could throw an error here if you want
$this->Session->setFlash(__('Error. Please, try again.', true));
}
$this->redirect($this->referer());
}
Then to add relationships, you obviously just call the add method of your relationships controller.
NOTE: Ideally, since adding a relationship is changing the database, it ideally shouldn't be done with a GET request accessed by a regular URL. It should be done via submitting a form via POST. I know that seems overkill when it's so easy to just do it via a regular link with GET. I haven't bothered to use forms/POST in this example - but if you want to stick to best practices, that's what you should do. See this for more info: https://softwareengineering.stackexchange.com/questions/188860/why-shouldnt-a-get-request-change-data-on-the-server
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.
I have a Recipe, Item, and Units table/model. I have a HABTM relationship with Recipe and Item, and I get the default multiple-select box when adding/editing Recipe. (am using Bake for everything for the most part). The problem is I need to associate quantities and units with each Item.
Sample of UI I'm hoping for:
A big component of it is the ability to add/delete/edit the individual items. I imagine looking at the submitted form data, and using some jquery and clone would work. But I was wondering if someone already created a Behavior perhaps for this already?
Current Models (shortened to the relevant stuff, ie removed users/notes/etc):
class Item extends AppModel {
var $name = 'Item';
// id : int
// name : varchar
// unit_id : int
var $belongsTo = array(
'Unit' => array(
'className' => 'Unit',
'foreignKey' => 'unit_id'
),
);
var $hasAndBelongsToMany = array(
'Recipe' => array(
'className' => 'Recipe',
'joinTable' => 'recipes_items',
'foreignKey' => 'item_id',
'associationForeignKey' => 'recipe_id',
)
);
}
.
class Recipe extends AppModel {
var $name = 'recipe';
var $displayField = "name";
// id : int
// name : varchar
var $hasAndBelongsToMany = array(
'Item' => array(
'className' => 'Item',
'joinTable' => 'recipes_items',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'item_id',
)
);
}
.
class RecipesItem extends AppModel {
var $name = 'RecipesItem';
// id : int
// quantity : int
// unit_id : int
// recipe_id : int
// item_id : int
var $belongsTo = array(
'Unit' => array(
'className' => 'Unit',
'foreignKey' => 'unit_id'
),
'Recipe' => array(
'className' => 'Recipe',
'foreignKey' => 'recipe_id'
),
'Item' => array(
'className' => 'Item',
'foreignKey' => 'item_id'
)
);
}
Not quite sure what you're asking. For adding, editing and deleting items you would need create actions in your items controller. Saving association data (ie which Items a Recipe has) should be handled more-or-less automatically by the save() method in your controller action, assuming you have your forms set up correctly.
Out of curiosity, where did the RecipesItem model come from? What does that represent? If I am understanding you correctly, you have a Recipe model, and an Item model, with HABTM relationship. You shouldn't need a model for their join table, the recipes_items table just relates items from the two models.
that's not something Cake can do for you. Maybe there's some js that can helps you a bit, but you'll pretty much have to write your own javascript for that.
You have to use javascript to "transform" the select tag into something "cooler".
Here is the jquery-multiselect plugin which I use quite a bit. You can easily set it up to replace all of your multi selects with 1 line of code.
More info here:
http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/