Symfony Entity check for ArrayCollection if already exists, then do update - symfony

For a Collection, I want to check if a new added Part already exists in the database. And if so, that it'll be overwritten with the new value.
/**
* Add Part
*/
public function addPart(\MyBundle\Entity\FooTypePart $Part)
{
$part->setProduct($this);
$this->part[] = $part;
return $this;
}
/**
*/
public function removePart(\MyBundle\Entity\FooTypePart $part)
{
$this->part->removeElement($part);
}
/**
* Get Part
* #return \Doctrine\Common\Collections\Collection
*/
public function getPart()
{
return $this->part;
}
/**
* Set Part
*/
public function setPart($part)
{
$this->part = $part;
return $this;
}
The Part Entity has: ID, Category_id (FK), Product_id (FK), Part (Collection)
It is possible at the moment to add a new Part with the same name, also when there is already a Part with the same Product_id AND Category_id.
Making Part unique isn't the fix, because Part can be used for many Products/Categories.
The following example already exists in the database, with a different 'Part'. So it should do a update command.
<?php
$part = new FooTypePart();
$part->setCategory($specification);
$part->setProduct($product);
$part->setPart('DifferentNamingThenCurrentOne');
$xx->addSpecificationValue($part);
How? :-)

Just use the UniqueEntity validator to find an already existant Item in the collection.
You can specify uniqueness using multiple properties or a repository method. This way you can search for items only having a unique combination of name, product-id and category-id.
Create a validation group i.e. "unique" and find the non-unique/existant entity in your collection by looking for invalid entities.
... then update the existing entity with your new value. You will probably need some extra logic because there might be multiple fields with the same name added to your form collection.

Related

Symfony 3 - Update Many-To-Many

I have been looking around for a clean solution on how to update (keep in sync) a many to many relationship?
I have the following scenario:
A Sprint Entity owns the Many To Many relationship towards the Ticket entity.
When editing a Ticket (or Sprint, but I am not there yet), I want to be able to select (checkboxes) the Sprints that this ticket belongs to.
Upon persistance (save), I want to update my join table tickets_sprint (which is just a join table on ticket_id, sprint_id).
Adding Sprints to the Ticket seems easy enough, but removing Sprints from the Ticket is not reflected at all.
Code
Ticket Entity contains this method for adding a Ticket to a Sprint:
public function setSprints($sprints) {
/**
* #var $sprint \AppBundle\Entity\Sprint
*/
foreach ($sprints as $sprint) {
$this->sprints[] = $sprint;
$sprint->addTicket($this);
}
}
I have read here that the only way to go would be to remove all relations and re-save them upon persistance.
Coming from the Laravel world, this hardly feels like a good idea :)
This is how it is done in Laravel:
/**
* #param \App\User $user
* #param \App\Http\Requests\StoreUserRequest $request
* #return \Illuminate\Http\RedirectResponse
* Update the specified resource in storage.
*/
public function update(User $user, StoreUserRequest $request)
{
$user->fill($request->input());
$user->employee_code = strtolower($user->employee_code);
$user->roles()->sync($request->role ? : []);
$user->save();
\Session::flash('flash_message_success', 'The user was successfully updated.');
return redirect()->route('frontend::users.show', [$user]);
}
All suggestions are welcome!
The EntityType that you may use to create a multiple selectbox doesn't have a by_reference option like CollectionType.
If your Ticket Entity use the "inversedBy" side, you don't need to add the reference in the other object. So you can symply do this :
public function setSprints($sprints) {
$this->sprints = $sprints;
}
Maybe this will be enough to add and remove your elements automatically (Sorry didn't try).
Otherwise you have to do it manually and you can create a new method to remove elements returns by the difference between your new ArrayCollection and the old one.

Symfony - access object of non related tables

How can i access the object of second table when joined (non-related tables)?
I have two table which are not related and I want to get the object of the second class (from below dump output)
My repository with dump
For example:
my controller:
$ProductSet_Repo = $em->getRepository('MyTestBundle:Product\ProductSet')->FindProductSet($productid);
Normally when the tables are related I can simple do
$productSet = $ProductSet_Repo->getproductid()->getProduct(); to get the object of Product class From ProductSet Class.
See My Dump
However since the tables are not in relationship and when i dump the data i get the objects of two classes is there a way I can access the Object My\TestBundle:Products\Entity\Product\ProductSet and \My\TestBundle\Entity\Product\Product?
Note: i don't want to do establish relationship between the two tables as I am working on already existing table for which i don't want to make any changes
Also I know I can select the fields which i want to retrieve. (I dont want to do that)
You write:
i don't want to do establish relationship between the two tables as I am working on already existing table for which i don't want to make any changes.
But with doctrine you are very well able to make a association between two entities without changing the tables. As far as I can see from your query you have a product_id column in your product_set table. That is all you need to make an association between Product and ProductSet.
In your ProductSet class you can do:
<?php
namespace My\TestBundle\Entity\Product;
class ProductSet
{
//... other properties
/**
* #var Product
* #ORM\Id
* #ORM\ManyToOne(targetEntity="My\TestBundle\Entity\Product\Product")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
/**
* Set the product.
*
* #param Product $product
* #return ProductSet
*/
public function setProduct(Product $product)
{
$this->product = $product;
return $this;
}
/**
* Get the product.
*
* #return Product
*/
public function getProduct()
{
return $this->product;
}
//... other setters and getters
}
Now you can do:
$repository = $em->getRepository('MyTestBundle:Product\ProductSet')
$productSets = $repository->findBy(array('product' => $productid));
foreach($productSets as $productSet){
$productSet->getProduct()->getId() === $productId; // true
}
You can still join them (despite of strange naming convention you have id of corresponding object in the other entity) using query builder or native sql, but it's a really bad way.
it was developed by previous webdeveloper and i dont want to spend more time as i work as free lancer
That's not an excuse. You should create a relation and migration for these data. Getting money for a poorly designed and developed app is not cool.
Probably additional work when working with that poor design will take your more time than doing it in a proper way.

Update a field after Linking / Unlinking Many-Many records in SilverStripe

I have created a Customer DataObject by extending Member. Customer has a many_many data relation with a Package DataObject.
I would like increment/decrement a Credits field in the Customer DataObject when a Package is linked / unlinked through the CMS based on the Limit field in the Package table.
Customer
class Customer extends Member {
private static $db = array(
'Gender' => 'Varchar(2)',
'DateOfBirth' => 'Date',
'Featured' => 'Boolean',
'Credits' => 'Int'
);
private static $many_many = array(
'Packages' => 'Package'
);
public function getCMSFields() {
$fields = new FieldList();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = new GridField(
'Packages',
'Package',
$this->Packages(),
$config
);
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
Package
class Package extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)',
'Limit' => 'Int'
);
private static $belongs_many_many = array(
'Customers' => 'Customer'
);
}
When you create or delete many to many relationship just one record is modified in your database - the one in table which joins elements of both sides of the relationship. Therefore neither object the relationship is based on is updated. This is why methods like: onBeforeWrite, onAfterWrite, onBeforeDelete and onAfterDelete will not be called at all and you cannot use them to detect such change.
However, Silverstripe provides class ManyManyList which is responsible for all operations connected to many to many relationships. There are two methods which are of your interest: add and remove. You can override them and put inside action to do what you need. These methods are obviously called on each link or unlink operation no matter object types are, so you should make some filtering on classes you are particularly interested in.
The proper way to override the ManyManyList class is to use Injector mechanism, so as not to modify anything inside the framework or cms folder. The example below uses relationship between Members and Groups in Silverstripe but you can easily adopt it to your need (Customer -> Member; Package -> Group).
app.yml
Injector:
ManyManyList:
class: ManyManyListExtended
ManyManyListExtended.php
/**
* When adding or removing elements on a many to many relationship
* neither side of the relationship is updated (written or deleted).
* SilverStripe does not provide any built-in actions to get information
* that such event occurs. This is why this class is created.
*
* When it is uses together with SilverStripe Injector mechanism it can provide
* additional actions to run on many-to-many relations (see: class ManyManyList).
*/
class ManyManyListExtended extends ManyManyList {
/**
* Overwritten method for adding new element to many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param mixed $item
* #param null $extraFields
* #throws Exception
*/
public function add($item, $extraFields = null) {
parent::add($item, $extraFields);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) added to Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Overwritten method for removing item from many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param DataObject $item
*/
public function remove($item) {
parent::remove($item);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) removed from Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Check if relationship is of Group-Member type.
*
* #return bool
*/
private function isGroupMembershipChange() {
return $this->getJoinTable() === 'Group_Members';
}
/**
* Get the actual ID for many-to-many relationship part - local or foreign key value.
*
* This works both ways: make action on a Member being element of a Group OR
* make action on a Group being part of a Member.
*
* #param DataObject|int $item
* #param string $keyName
* #return bool|null
*/
private function getMembershipID($item, $keyName) {
if ($this->getLocalKey() === $keyName)
return is_object($item) ? $item->ID : $item;
if ($this->getForeignKey() === $keyName)
return $this->getForeignID();
return false;
}
}
The solution provided by 3dgoo should also work fine but IMO that code does much more "hacking" and that's why it is much less maintainable. It demands more modifications (in both classes) and needs to be multiplied if you would like to do any additional link/unlink managing, like adding custom admin module or some forms.
The problem is when adding or removing items on a many to many relationship neither side of the relationship is written. Therefore onAfterWrite and onBeforeWrite is not called on either object.
I've come across this problem before. The solution I used isn't great but it was the only thing that worked for me.
What we can do is set an ID list of Packages to a session variable when getCMSFields is called. Then when an item is added or removed on the grid field we refresh the CMS panel to call getCMSFields again. We then retrieve the previous list and compare it to the current list. If the lists are different we can do something.
Customer
class Customer extends Member {
// ...
public function getCMSFields() {
// Some JavaScript to reload the panel each time a package is added or removed
Requirements::javascript('/mysite/javascript/cms-customer.js');
// This is the code block that saves the package id list and checks if any changes have been made
if ($this->ID) {
if (Session::get($this->ID . 'CustomerPackages')) {
$initialCustomerPackages = json_decode(Session::get($this->ID . 'CustomerPackages'), true);
$currentCustomerPackages = $this->Packages()->getIDList();
// Check if the package list has changed
if($initialCustomerPackages != $currentCustomerPackages) {
// In here is where you put your code to do what you need
}
}
Session::set($this->ID . 'CustomerPackages', json_encode($this->Packages()->getIDList()));
}
$fields = parent::getCMSFields();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = GridField::create(
'Packages',
'Package',
$this->Packages(),
$config
);
// This class needs to be added so our javascript gets called
$packageField->addExtraClass('refresh-on-reload');
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
The if ($this->ID) { ... } code block is where all our session code happens. Also note we add a class to our grid field so our JavaScript refresh works $packageField->addExtraClass('refresh-on-reload');
As mentioned before, we need to add some JavaScript to reload the panel each time a package is added or removed from the list.
cms-customer.js
(function($) {
$.entwine('ss', function($){
$('.ss-gridfield.refresh-on-reload').entwine({
reload: function(e) {
this._super(e);
$('.cms-content').addClass('loading');
$('.cms-container').loadPanel(location.href, null, null, true);
}
});
});
})(jQuery);
Inside the if($initialCustomerPackages != $currentCustomerPackages) { ... } code block there are a number of things you can do.
You could use $this->Packages() to fetch all the current packages associated to this customer.
You could call array_diff and array_merge to get just the packages that have been added and removed:
$changedPackageIDs = array_merge(array_diff($initialCustomerPackages, $currentCustomerPackages), array_diff($currentCustomerPackages, $initialCustomerPackages));
$changedPackages = Package::get()->byIDs($changedPackageIDs);
The above code will add this functionality to the Customer side of the relationship. If you also want to manage the many to many relationship on the Package side of the relationship you will need to add similar code to the Package getCMSFields function.
Hopefully someone can come up with a nicer solution. If not, I hope this works for you.
note: Didn't actually check does the model work but by visually checking this should help you:
On the link you provided you are using
$customer = Customer::get()->Filter...
That returns a DataList of objects, not a singular object unless you specify what is the object you want from the DataList.
If you are filtering the Customers then you want to get a SPECIFIC customer from the DataList, e.g. the first one in this case.
$customer = Customer::get()->filter(array('ID' => $this->CustomerID))->first();
But You should be able to get the singular DataObject with:
$customer = $this->Customer();
As you are defining the Customer as "has_one". If the relation was a Has many, using () would get you a DataList of objects.
Protip:
You don't need to write our own debug files in SilverStripe. It has own functions for it. For example Debug::log("yay"); what writes the output to a file and Debug::dump("yay") that dumps it directly out.
Tip is that you can check what is the object that you accessing right. Debug::dump(get_class($customer)); would output only the class of the object.

Symfony2 remove and save many to many relations

I need your help today. I'm working on a small application using Symfony 2.1 but I have a base problem, I have to tables with a many to many relation which creates a third table:
class Usuario implements UserInterface {
/**
* #ORM\ManyToMany(targetEntity="Alood\BackBundle\Entity\Alergeno", inversedBy="usuarios")
* #ORM\JoinTable(name="UsuariosProductos",
* joinColumns={#ORM\JoinColumn(name="usuario_user", referencedColumnName="user")},
* inverseJoinColumns={#ORM\JoinColumn(name="alergeno_id", referencedColumnName="id")}
* )
**/
protected $alergenos;
}
public function __construct(){
$this->alergenos = new ArrayCollection();
}
public function getAlergenos() { return $this->alergenos; }
and:
/**
* #ORM\ManyToMany(targetEntity="Alood\BackBundle\Entity\Usuario", mappedBy="alergenos")
**/
protected $usuarios;
Then I need to remove the non selected Alergenos, this is my controller:
$alergenosUser = $em->getRepository("BackBundle:Usuario")->find($usuario);
$resultSym = array_diff($alergenosUsuarioIds, $alergen);
foreach($resultSym as $result) {
$alergenosUser->getAlergenos()->remove($result);
}
$em->persist($alergenosUser);
$em->flush();
Could you please help me to figure out what I'm doing wrong? Thanks you so much!
In order to remove an item from a collection use the following:
$collection->removeElement($item);
The remove($key) function will remove by key while removeElement($item) removes the item from the collection if found. Have a look at the ArrayCollection code here.
Be aware that doctrine will only check the owning side of a relation for changes.
It is not clear what the $alergenosUsuarioIds and $alergen variables represent but you might be mistaken about the usage of the remove() method of ArrayCollection.
You need to give it an index, not the id of the entity you want to remove. You can also use the removeElement() method and pass it the entity.
For instance you can do something like this :
$elements = $alergenosUser->getAlergenos();
foreach ($elements as $element) {
if ($element->getId() == $id_from_array_diff_or_whatever) {
$elements->removeElement($element);
}
}
or
$elements = $alergenosUser->getAlergenos();
foreach ($elements as $key => $element) {
if ($element->getId() == $id_from_array_diff_or_whatever) {
$elements->remove($key);
// or
unset($elements[$key]);
}
}
You can also use the matching() but I'm not sure it's available with the version shipped with symfony2 2.1.
So your problem can be solved doing the relation yourself.
ManyToMany doesn't really exist because as you say a third table is created. You want to delete elements only in this third table.
So you have to build the relation yourself to delete directly an element in the third table.
So first create the third entity.
Do two relation ManyToOne from the third entity to the two others entities.
Then you just have to simply remove an element of the third entity you just created.

Composite key and form

I have the following associations in my database (simplified version):
This is a Many-To-Many association but with an attribute on the joining table, so I have to use One-To-Many/Many-To-One associations.
I have a form where I can add as many relations as I want to one order item and create it at the same time (mainly inspired by the How to Embed a Collection of Forms tutorial from the documentation.
When I post the form, I get the following error:
Entity of type TEST\MyBundle\Entity\Relation has identity through
a foreign entity TEST\MyBundle\Entity\Order, however this entity
has no identity itself. You have to call EntityManager#persist() on
the related entity and make sure that an identifier was generated
before trying to persist 'TEST\MyBundle\Entity\Relation'. In case
of Post Insert ID Generation (such as MySQL Auto-Increment or
PostgreSQL SERIAL) this means you have to call EntityManager#flush()
between both persist operations.
I understand this error because Doctrine tries to persist the Relation object(s) related to the order since I have the cascade={"persist"} option on the OneToMany relation. But how can I avoid this behavior?
I have tried to remove cascade={"persist"} and manually persist the entity, but I get the same error (because I need to flush() order to get the ID and when I do so, I have the same error message).
I also tried to detach() the Relation objects before the flush() but with no luck.
This problem seems unique if 1) you are using a join table with composite keys, 2) forms component, and 3) the join table is an entity that is being built by the form component's 'collection' field. I saw a lot of people having problems but not a lot of solutions, so I thought I'd share mine.
I wanted to keep my composite primary key, as I wanted to ensure that only one instance of the two foreign keys would persist in the database. Using
this entity setup as an example
/** #Entity */
class Order
{
/** #OneToMany(targetEntity="OrderItem", mappedBy="order") */
private $items;
public function __construct(Customer $customer)
{
$this->items = new Doctrine\Common\Collections\ArrayCollection();
}
}
/** #Entity */
class Product
{
/** #OneToMany(targetEntity="OrderItem", mappedBy="product") */
private $orders;
.....
public function __construct(Customer $customer)
{
$this->orders = new Doctrine\Common\Collections\ArrayCollection();
}
}
/** #Entity */
class OrderItem
{
/** #Id #ManyToOne(targetEntity="Order") */
private $order;
/** #Id #ManyToOne(targetEntity="Product") */
private $product;
/** #Column(type="integer") */
private $amount = 1;
}
The problem I was facing, if I were building an Order object in a form, that had a collection field of OrderItems, I wouldn't be able to save OrderItem entity without having saved the Order Entity first (as doctrine/SQL needs the order id for the composite key), but the Doctrine EntityManager wasn't allowing me to save the Order object that has OrderItem attributes (because it insists on saving them en mass together). You can't turn off cascade as it will complain that you haven't saved the associated entities first, and you cant save the associated entities before saving Order. What a conundrum. My solution was to remove the associated entities, save Order and then reintroduce the associated entities to the Order object and save it again. So first I created a mass assignment function of the ArrayCollection attribute $items
class Order
{
.....
public function setItemsArray(Doctrine\Common\Collections\ArrayCollection $itemsArray = null){
if(null){
$this->items->clear();
}else{
$this->items = $itemsArray;
}
....
}
And then in my Controller where I process the form for Order.
//get entity manager
$em = $this->getDoctrine()->getManager();
//get order information (with items)
$order = $form->getData();
//pull out items array from order
$items = $order->getItems();
//clear the items from the order
$order->setItemsArray(null);
//persist and flush the Order object
$em->persist($order);
$em->flush();
//reintroduce the order items to the order object
$order->setItemsArray($items);
//persist and flush the Order object again ):
$em->persist($order);
$em->flush();
It sucks that you have to persist and flush twice (see more here Persist object with two foreign identities in doctrine). But that is doctrine for you, with all of it's power, it sure can put you in a bind. But thankfully you will only have to do this when creating a new object, not editing, because the object is already in the database.
You need to persist and flush the original before you can persist and flush the relationship records. You are 100% correct in the reason for the error.
I assume from the diagram that you are trying to add and order and the relation to the contact at the same time? If so you need to persist and flush the order before you can persist and flush the relationship. Or you can add a primary key to the Relation table.
I ended up creating a separated primary key on my Relation table (instead of having the composite one).
It looks like it is a dirty fix, and I am sure there is a better way to handle this situation but it works for now.
Here is my Relations entity:
/**
* Relation
*
* #ORM\Entity
*/
class Relation
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Contact", inversedBy="relation")
*/
protected $contact;
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="relation")
*/
protected $order;
/**
* #var integer
*
* #ORM\Column(name="invoice", type="integer", nullable=true)
*/
private $invoice;
//Rest of the entity...
I then added the cascade={"persist"} option on the OneToMany relation with Order:
/**
* Orders
*
* #ORM\Entity
*/
class Order
{
/**
* #ORM\OneToMany(targetEntity="Relation", mappedBy="order", cascade={"persist"})
*/
protected $relation;
//Rest of the entity...
Et voilĂ !

Resources