How can I clone all data from my database with Symfony doctrine? - php

I try to clone all records in my data entity that have the item value cf7c1ae00f
$dataEntity= new Data();
$cloneArray = $this->em->getRepository(Data::class)->findBy(['item' => 'cf7c1ae00f']);
foreach ($cloneArray as $cloneItem) {
$fieldClone = clone $cloneItem;
$dataEntity->setItem($fieldClone);
$this->em->persist($dataEntity);
}
$this->em->flush();
In my database there are 5 records. So I expect that another 5 records are added. But only one record is added.

You are writing the same $dataEntity 5 times with different contents. You could construct that object in the loop to solve your problem but you could also persist $fieldClone directly instead and skip the $dataEntity variable completely.
However, entities have unique ids and that will lead to errors when you try to persist a cloned entity. You would have to empty the id and other attributes that have to be unique in the collection / database.
You can easily set some initial values on a new object when the clone keyword is used, using the __clone() method of the class that the object belongs to.
So if you only need to empty the id, you would add a clone method to the Data class and change the loop to:
Data class:
public function __clone() {
$this->id = null;
}
Cloning code:
$cloneArray = $this->em->getRepository(Data::class)->findBy(['item' => 'cf7c1ae00f']);
foreach ($cloneArray as $cloneItem) {
# Clone the object and automatically run the `__clone()` method of the class
$fieldClone = clone $cloneItem;
$this->em->persist($fieldClone);
}
$this->em->flush();

Related

How to set doctrine associations?

I know that association property in entity is implements \Doctrine\Common\Collections\Collection. I know that in constructor such properties should be initialized:
$this->collection = new \Doctrine\Common\Collections\ArrayCollection()
I know that I can modify collections using ArrayCollection#add() and ArrayCollection#remove(). However I have a different case.
Suppose I have a new simple array of associative entities. Using existing methods I need to check every element in array: if entity collection has it. If no - add array element to entity collection. In addition to this, I need to check every element in entity collection. If any collection element is absent in new array, then I need to remove it from collection. So much work to do trivial thing.
What I want? To have the setProducts method implemented:
class Entity {
private $products;
// ... constructor
public function setProducts(array $products)
{
// synchronize $products with $this->products
}
}
I tried: $this->products = new ArrayCollection($products). However this makes doctrine remove all products and add those ones from $products parameter. I want similar result but without database queries.
Is there any built in solution in Doctrine for such case?
Edit:
I would like to have a method in ArrayCollection like fromArray which would merge elements in collections removing unneeded. This would just duplicate using add/remove calls for each element in collection argumen manually.
Doctrine collections do not have a "merge"-feature that will add/remove entities from an array or Collection in another Collection.
If you want to "simplify" the manual merge process you describe using add/remove, you could use array_merge assuming both arrays are not numeric, but instead have some kind of unique key, e.g. the entity's spl_object_hash:
public function setProducts(array $products)
{
$this->products = new ArrayCollection(
array_merge(
array_combine(
array_map('spl_object_hash', $this->products->toArray()),
$this->products->toArray()
),
array_combine(
array_map('spl_object_hash', $products),
$products->toArray()
)
)
);
}
You might want to use the product id instead of spl_object_hash as 2 products with the same id, but created as separate entities - e.g. one through findBy() in Doctrine and one manually created with new Product() - will be recognized as 2 distinct products and might cause another insert-attempt.
Since you replace the original PersistentCollection holding your previously fetched products with a new ArrayCollection this might still result in unneeded queries or yield unexpected results when flushing the EntityManager, though. Not to mention, that this approach might be harder to read than explicitly calling addElement/removeElement on the original Collection instead.
I would approach it by creating my own collection class that extends Doctrine array collection class:
use Doctrine\Common\Collections\ArrayCollection;
class ProductCollection extends ArrayCollection
{
}
In the entity itself you would initialise it in the __constructor:
public function __construct()
{
$this->products = new ProductCollection();
}
Here, Doctrine will you use your collection class for product results. After this you could add your own function to deal with your special merge, perhaps something:
public function mergeProducts(ProductCollection $products): ProductCollection
{
$result = new ProductCollection();
foreach($products as $product) {
$add = true;
foreach($this->getIterator() as $p) {
if($product->getId() === $p->getId()) {
$result->add($product);
$add = false;
}
}
if($add) {
$result->add($product);
}
}
return $result;
}
It will return a brand new product collection, that you can replace your other collection in the entity. However, if the entity is attached and under doctrine control, this will render SQL at the other end, if you want to play with the entity without risking database updates you need to detach the entity:
$entityManager->detach($productEntity);
Hopes this helps

How to copy a CakePHP Entity

I'm writing a method that copies an object. Instead of manually setting each property manually, it would be more robust to just loop over the original object's properties...
//Booo
$new->name = $old->name;
$new->color = $old->color;
...
//Oh yeah...
foreach ($old as $prop=>$val){
$new->$prop = $val;
}
unset $new->id;
It appears that CakePHP entities cannot be iterated over in this way. I tried using $old->toArray(), which basically works... but has the drawback of converting all the associations to arrays also, which is screwing this up for me down stream.
How do I loop over the $old properties without converting all the data types?
Update:
Mark brought to my attention the existence of a __clone() method. Sounds like it does exactly what I need but I'm still figuring out how to use it.
You can use $entity->visualProperties()
foreach($old->visualProperties() as $property) {
if($new->has($property))
$new->set($property, $old->get($property));
After looking at this for a while, and discovering there is no __clone() function for entities, at least in 3.8, I have worked out how to do it, with the hint from DouglasSantos :
//Find out the entity classname
$classname = get_class($entity);
//Instanciate a new object of that class
$clone = new $classname;
//Use visibleProperties to clone it
foreach($entity->visibleProperties() as $property)
if($clone->has($property))
$clone->set($property, $entity->get($property));
Of course you could combine the first 2 lines into one line, but I have split it out for clarity.
UPDATE: I have discovered if you use the has->($property) check it will skip many of the fields. So the corrected answer is :
//Find out the entity classname
$classname = get_class($entity);
//Instanciate a new object of that class
$clone = new $classname;
//Use visibleProperties to clone it
foreach($entity->visibleProperties() as $property)
$clone->$property = $entity->$property;
It is actually much easier to use the Table Object:
// Assuming your model is called "Documents"
// If you are in the Controller, you can just use `$this->Documents`
instead of fetching the Table from the Registry
use Cake\ORM\TableRegistry;
$table = TableRegistry::getTableLocator()->get('Documents');
// newEntity() creates a new Entity from an array of data
$documentCopy = $table->newEntity(
// extract() extracts the given properties as an associative array
$document->extract(
// getVisible() will get all visible properties as an array
$document->getVisible()
)
);

Doctrine Events, update persisted entity

i wrote an Importer script, which read entries from an csv file,
and iterate the rows. To handle big files without performance loss,
i insert new data within doctrine batch(bulks).
My problem at the moment is, i have an "Category" entity, which should be expanded
only within new entries. So i have to check if entries are available given category names.
My first question is, i've read that doctrines "prePersist" event will be called on call
"$entityManager->persist()" and inside the "flush" method (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#prepersist).
So how can i check if the event was inside an flush?
The next thing, how can i update the actually entity within the identity datas?
I try to set the id, but without any effect.
<?php
/**
* #return \Doctrine\Commong\Collections\ArrayCollection
*/
public function getCategories()
{
if (null === $this->categories) {
$this->categories = $this->getServiceCategory()->findAll();
}
return $this->categories;
}
public function prePersist(LifecycleEventArgs $event)
{
$entity = $event->getEntity();
$objectManager = $event->getObjectManager();
if ($entity instanceof \Application\Entity\Category) {
$categories = $this->getCategories();
$entityCriteria = // buildCriteria from Entity;
$matched = $categories->matching($entityCriteria);
if ($matched->count() > 0) {
$entity->setId($matched->first()->getId();
}
}
}
So, here i dont know how to update the persisted categorie entity?
Is this the right event, or should be an other event a better solution for my situation?
I developed the import within zf2 and doctrine2.
regards
First I would recommend to use DQL instead of ORM entities within you import script, because it makes you code much more simple.
Your import process increases the performance, if you first (1.) read all existing "Categories" from yor database, keep them within a member variable and second (2.) iterate each csv row and compare its category foreign key with the initially read set of categories.
If the category already exists in you database, create a new entity row with the existing corresponding foreign key, else create a new category and create a new entity row associated to the new category.
<?php
// read categories from database
$categories = "..."; /* e.g. array('A' => 1,
'B' => 2, ...); */
// ...
// Iterate each csv row
foreach($csvRows as $csvRow) {
// check category name
if(!array_key_exists($csvRow['category']), $categories) {
// Create new Category
// Remember id of the new created category
$categoryId = "...";
} else {
// Use already existing category id
$categoryId = $categories[$csvRow['category']];
}
// Create new csv row entity with category foreign key
// ...
}
?>

PHP - Editing object clone affects original object

So I have an object, let's call it Example.
class Example {
public function __construct ($id) {
$this->Id = $id;
}
}
$objectA = new Example(10);
It has an id (it pulls this from somewhere), the goal of the object is to overwrite a similar object with this object's properties (post to an external service). This is done by simply changing the ID of the object, and running an ->update() method. However, it must first change its ID (among other properties) to match the ids of object B.
So what I do is clone the current object, reassign the needed properties, and then pass that cloned object to the update method, so that the update method uses the $post values for the update.
public function createPost ($id) {
$post = clone $this;
$post->Id = $id;
return $post;
}
$objectA->update($objectA->createPost(12));
$objectA->update($objectA->createPost(16));
$objectA->update($objectA->createPost(21));
The issue I'm having is this object A needs to be used for multiple different updates, and it uses the ID it is originally assigned as a pointer to what IDs it must later use for $post, and in this scenario, the value of $this->ID is getting reassigned to the $id that is passed in ->setParameters(), even though I'm trying to assign it to a clone of $this, rather than $this itself.
My impression is that $objectA = $objectB assigns ObjectB to the same pointer that points to ObjectA, but that "clone" was supposed to actually make a copy of that object, so that if properties of the clone are changed, the original object remains unaffected, but that doesn't seem to be the case here. Is there a particular method I should instead be using to ensure that the original object's value aren't ever changed when a clone of it is?
I think what you want is a deep clone. Take a look at this link.
The issue I ran into here was a shallow cloning issue it would appear.
Forget what I referenced above, the problem was more so this: -
class Example {
public function __construct ($id) {
$this->ObjId = (object)array("Id" => 5);
$this->Id = $id;
}
}
$example = new Example (5);
$clone = clone $example;
$clone->ObjId->Id = 10;
In this example, I was trying to change the ->Id of a standard php object that was stored within my main object (I had used json_encode to pull a whole structure of objects) that was called "Id". The result of the above was this: -
echo $example->ObjId->Id; //10
echo $example->Id; //5
echo $clone->ObjId->Id; //10
echo $clone->Id; //5
So I had to write this method as part of the Example class: -
public function __clone () {
foreach ($this as $key => $val) {
if (is_object($val) || (is_array($val))) {
$this->{$key} = unserialize(serialize($val));
}
}
}
So now, the result was this: -
echo $example->ObjId->Id; //5
echo $example->Id; //5
echo $clone->ObjId->Id; //10
echo $clone->Id; //5
This is because when I cloned the main Example object, it was creating an new variable for it's properties, but if one of it's properties was an object or array, it was copying the address of that object or array for the new cloned object. The above method prevents that, and does a "deep" copy.

Yii on update, detect if a specific AR property has been changed on beforeSave()

I am raising a Yii event on beforeSave of the model, which should only be fired if a specific property of the model is changed.
The only way I can think of how to do this at the moment is by creating a new AR object and querying the DB for the old model using the current PK, but this is not very well optimized.
Here's what I have right now (note that my table doesn't have a PK, that's why I query by all attributes, besides the one I am comparing against - hence the unset function):
public function beforeSave()
{
if(!$this->isNewRecord){ // only when a record is modified
$newAttributes = $this->attributes;
unset($newAttributes['level']);
$oldModel = self::model()->findByAttributes($newAttributes);
if($oldModel->level != $this->level)
// Raising event here
}
return parent::beforeSave();
}
Is there a better approach? Maybe storing the old properties in a new local property in afterFind()?
You need to store the old attributes in a local property in the AR class so that you can compare the current attributes to those old ones at any time.
Step 1. Add a new property to the AR class:
// Stores old attributes on afterFind() so we can compare
// against them before/after save
protected $oldAttributes;
Step 2. Override Yii's afterFind() and store the original attributes immediately after they are retrieved.
public function afterFind(){
$this->oldAttributes = $this->attributes;
return parent::afterFind();
}
Step 3. Compare the old and new attributes in beforeSave/afterSave or anywhere else you like inside the AR class. In the example below we are checking if the property called 'level' is changed.
public function beforeSave()
{
if(isset($this->oldAttributes['level']) && $this->level != $this->oldAttributes['level']){
// The attribute is changed. Do something here...
}
return parent::beforeSave();
}
Just in one line
$changedArray = array_diff_assoc($this->attributes,
$this->oldAttributes);
foreach($changedArray as $key => $value){
//What ever you want
//For attribute use $key
//For value use $value
}
In your case you want to use if($key=='level') inside of foreach
Yii 1.1: mod-active-record at yiiframework.com
or Yii Active Record instance with "ifModified then ..." logic and dependencies clearing at gist.github.com
You can store old properties with hidden fields inside update form instead of loading model again.

Categories