Akeneo 2.2.8 : How can I get the original attribute data in the akeneo.storage.pre_save event? - symfony

I'm using Akeneo 2.2.8 and I'm trying to use the akeneo.storage.pre_save-event to compare the original product data with the new data provided. I do this by subscribing to the akeneo.storage.pre_save-event:
In event_subscribers.yml:
parameters:
vendor.bundle.event_subscriber.product_save.class: Vendor\Bundle\CustomBundle\EventSubscriber\ProductSaveSubscriber
services:
vendor.bundle.event_subscriber.product_save:
class: '%vendor.bundle.event_subscriber.product_save.class%'
arguments:
- '#pim_catalog.repository.product'
tags:
- { name: kernel.event_listener, event: akeneo.storage.pre_save, method: onPreSave, priority: 255 }
In ProductSaveSubscriber.php:
/**
* #var ProductRepositoryInterface
*/
protected $productRepository;
public function __construct(ProductRepositoryInterface $productRepository)
{
$this->productRepository = $productRepository;
}
public function onPreSave(GenericEvent $event)
{
/** #var Product $subject */
$subject = $event->getSubject();
if ($subject instanceof Product) {
$originalProduct = $this->productRepository->findOneByIdentifier($subject->getIdentifier());
foreach ($subject->getAttributes() as $attribute) {
if ($attribute->getReadOnly()) {
echo "{$attribute->getCode()} : {$subject->getValue($attribute->getCode())}\n";
echo "{$attribute->getCode()} : {$originalProduct->getValue($attribute->getCode())}\n";
}
}
}
}
Now when I run this code, I expect the second echo-statement to give the original data (since I've loaded that anew). However, the original product I load from the repository also has the new data.
Another thing to note here is that if I add a die()-statement, the data is not stored in the database. So it seems that the repository returns the in-memory model or something like that.
Can anyone point me in the right direction? Or am I using the wrong approach to compare newly-entered data with already existing data?

Now when I run this code, I expect the second echo-statement to give
the original data (since I've loaded that anew). However, the original
product I load from the repository also has the new data.
This might be because the object is referenced in doctrine's unit of work. So when you use the repository to fetch what you think is the original object, might actually be the same object you've updated.
Another thing to note here is that if I add a die()-statement, the
data is not stored in the database. So it seems that the repository
returns the in-memory model or something like that.
That's because since your subscriber is listening on the PRE_SAVE event, the updated product has not been flushed in the database yet. Saving a product goes this way:
PRE_SAVE event thrown
COMMIT / FLUSH
POST_SAVE event thrown
So if you call die during the PRE_SAVE event, the COMMIT / FLUSH won't be called.
Can anyone point me in the right direction? Or am I using the wrong
approach to compare newly-entered data with already existing data?
I don't know your particular use case but you might want to use a Query function (see https://github.com/akeneo/pim-community-dev/blob/2.2/src/Pim/Bundle/CatalogBundle/Doctrine/ORM/Query/FindAttributesForFamily.php). It's purpose is to directly fetch in the database the data you need (it will be the original value since the product hasn't been flushed in DB on PRE_SAVE)
I hope this helps.

Related

Loggable capability for entity with trace of persistance place (location) - doctrine

Hi I am using Doctrine with DoctrineBehaviors bundle in order to log history of changes for entities. Everything is working fine except one thing. I will need information where is the location of change that is going to be saved. For example if that change happened in cron command or in front end controller by some user etc.
Logging class is looking like this (similar to this) :
/** service: "entity_logger" **/
class EntityLogger
{
/**
* #var EntityManager
*/
protected $em;
public function __construct(EntityManager $em = null)
{
$this->em = $em;
}
function __invoke($message)
{
$this->em->persist(new LogMessage($message));
$this->em->flush();
}
}
and it's set as service. So my question is how to manage information of location of change? I will need somehow to send beside entity that is going to be persist one more thing: location of that change. As I see it there are at least two options :
A) store value of location to that service "entity_logger" before persist/flush happens. Have property "location" inside service and put it into LogMessage obj. Something like:
$this->get('entity_logger')->setCurrentLocation("Cron for sync");
$this->em->persist($entityOne);
$this->em->flush();
B) get stack trace and see what class has call persist/flush and then see how to handle that information
First option requires editing of all current locations of persist and second one don't have that overhead but what is the better approach?
Is there even better way of doing that?
Thanks

Add data when running Symfony migrations

I have a Symfony project that is using the DoctrineMigrations bundle, and I have a really simple question: When I run a migration (e.g., when I'm pushing an update to production), how can I insert data to the database?
For example: I have an Entity which is the type of an add. The entity is:
private $addType; // String
private $type1; // Boolean
private $type2; // Boolean
private $type3; // Boolean
I add another field ($type4), and I want to add a new record to the database, with this values:
$addType = 'Type number 4';
$type1 = false;
$type2 = false;
$type3 = false;
$type4 = true;
How can this be done with DoctrineMigrations? Is it possible?
Using the Entity Manager as suggested in another answer is not a good idea, as it leads to troubles later.
In the first migration, I created a table with users and populated some users via $em->persist($user); which seemed fine at the beginning.
But after a month, I added a phone column to my User model. And Doctrine generates INSERT statements with this column within the first migration, which fails due to the non-existing column phone. Of course it doesn't exist yet in the first migration. So it is better to go with pure SQL INSERTs.
I just asked a related related question.
It is possible to use the migrations bundle to add data to the database. If you add a new property and use the doctrine mapping then the
php app/console doctrine:migrations:diff
command will generate a new migration file. You can just put your insert statements inside this file using the syntax:
$this->addSql('INSERT INTO your_table (name) VALUES ("foo")');
Make sure you put it after the auto-generated schema changes though. If you want to separate your schema changes and your data changes then you can use
php app/console doctrine:migrations:generate
to create an empty migrations file to put your insert statements in.
Like I said in my related question, this is one way to do it, but it requires manually creating these if you want to change this data in the database.
Edit:
Since this answer seems to get a few views I think it's worth adding that to more clearly separate the data changes from the schema changes there is a postUp method that can be overridden and that will be called after the up method.
https://www.doctrine-project.org/projects/doctrine-migrations/en/3.0/reference/migration-classes.html#postup
I've "found" the correct way to solve my problem (insert data after running migrations, using my entity classes).
Here is: https://stackoverflow.com/a/25960400
The idea is to declare the migration as ContainerAware, and then, from the postUp function, call the DI to get the EntityManager. It's really easy, and you can use all your entities and repositories.
// ...
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Version20130326212938 extends AbstractMigration implements ContainerAwareInterface
{
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function up(Schema $schema)
{
// ... migration content
}
public function postUp(Schema $schema)
{
$em = $this->container->get('doctrine.orm.entity_manager');
// ... update the entities
}
}
when you make the new field you need to enter this annotation "options={"default":1}" and it should work.
/**
* #var boolean
* #ORM\Column(name="type4", type="boolean", options={"default":1})
*/
private $type4 = true;
Took me some time to figure this out :)
It does, if you know how to format the array;
$this->connection->insert('user', ['id' => 1, 'gender' => 'Male']);
this is good solution for me. Just use bin/console make:migration and when migration is generated just edit if and add "DEFAULT TRUE":
$this->addSql('ALTER TABLE event ADD active TINYINT(1) NOT NULL DEFAULT TRUE');
It doesn't sound a good idea to fill date in migration, not its responsibility, symfony has a way of doing that. https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html

Updating an entity through Doctrine while it's persisted by a backend process

I've got an issue with Doctrine being somehow nasty with automagical tracking of entities and changes. I've got a UserManager which gets data for a new user from a form and sends the data to the backend which creates the corresponding database entries. As the backend is only inserting some basic data like username, I want to persist everything else like a collection of user roles given through the form.
So my method should look like this:
public function create(User $user)
{
$this->createUserInBackend($user);
$this->em->merge($user);
$this->em->persist($user);
$this->em->flush();
}
My issue now is that Doctrine either drops the data from the given $user and replaces it with the database content. This way I lose the chosen user roles. Without the merge() Doctrine tries an INSERT which is clearly not what I want.
I tried everything coming to my mind from fetching a managed copy before the merge, cloning or whatever. In all cases the objects are linked so I lose the data from the form (although their spl_object_hash differ).
Some more simplified details as requested:
class User
{
// Username is tracked by backend
private $username;
// Fullname is only tracked by frontend/Doctrine
private $fullname;
}
Variant 1:
public function create(User $user)
{
// The user entity gets passed to the backend, which does some stuff
// and also inserts the entity in the database.
$this->createUserInBackend($user);
// The entitiy is in the database, but not managed by the EM.
// Therefore Doctrine does an INSERT.
$this->em->persist($user);
$this->em->flush();
}
Variant 2:
public function create(User $user)
{
// The user entity gets passed to the backend, which does some stuff
// and also inserts the entity in the database.
$this->createUserInBackend($user);
// Now there's an entity in the db with ID and username, but not the fullname
$user = $this->em->merge($user);
// The merge finds the entry in the database and refreshes its data.
// This leads to $user->fullname which was given in the form to be emptied. :(
$this->em->persist($user);
$this->em->flush();
}
I also tried to put the return value from merge into an $otherUser variable, but the objects are linked and fullname still gets dropped.
I'd just need something to tell Doctrine that the new entity is managed, but it should not touch its data. I've looked into the underlying UnitOfWork, but couldn't find a trick to solve this.

Symfony/Doctrine entity leads to dirty entity association when merged multiple times with entity manager

I am using Symfony2 and Doctrine
I have a doctrine entity which is serialized/unserialized to a session and used in multiple screens. This entity has a number of one to many associations.
The doctrine entity has the following one to many, for example:
class Article {
...
/**
* #ORM\OneToMany(targetEntity="image", mappedBy="article", cascade= {"merge","detach","persist"})
*/
protected $images;
public function __construct()
{
$this->images = new ArrayCollection();
}
.....
}
The article entity is saved and retrieved as follows:
public function saveArticle($article)
{
$articleSerialized = serialize($article);
$this->session->set('currentArticle',$articleSerialized);
}
public function getArticle()
{
$articleSerialized = $this->session->get('currentArticle');
$article = unserialize($articleSerialized);
$article = $this->em->merge($article);
return $article;
}
I am able to save and load the entity to and from the session any number of times, and then merge it back to the entity manager and save. This is only if it is a new entity.
However, once I load an entity from the db and then save it to session, I get problems.
I know, from other posts, that after you unserialise a saved entity, you have to run $em->merge($entity);
I am able to merge the entity, add a new sub-entity (one to many) and then save:
$article = $this->getArticle(); //Declared above, gets article from session
$image = new Image();
$image->setFilename('image.jpeg');
$article->addImage($image);
$this->saveArticle($article); //Declared above, returns the article to session
However, after the first merge and image add, I can't add any more sub-entities. If i try to add a second image, It returns the following error:
A managed+dirty entity <<namespace of entity>>
image#0000000067078d7400000000221d7e02 can not
be scheduled for insertion.
So in summary, i can make any number of changes to an entity and save it to session, but if I run $em->merge more than once while adding sub-entities, the new sub-entities are marked as dirty.
Does anybody know why an entity would be marked as dirty? Do I need to reset the entity itself, and if so, how could I do that?
Got it.
For anyone who might run into this problem in the future:
You cannot merge an entity which has unpersisted sub-entities. They become marked as dirty.
I.E
You may have an article with two images already saved to DB.
ARTICLE (ID 1) -> IMAGE (ID 1)
-> IMAGE (ID 2)
If you save serialise the article to session and then unserialize and merge it. It's ok.
If you add a new image, then serialize it to session you will have problems. This is because you cannot merge an unpersisted entity.
ARTICLE (ID 1) -> IMAGE (ID 1)
-> IMAGE (ID 2)
-> IMAGE (NOT YET PERSISTED)
What I had to do was:
After I unserialize the article, I remove the unpersisted images and store them in a temporary array (I check for ID). THEN i merge the article and re-add the unpersisted image(s).
$article = unserialize($this->session->get('currentArticle'));
$tempImageList = array();
foreach($article->getImages() as $image)
{
if(!$image->getId()) //If image is new, move it to a temporary array
{
$tempImageList[] = $image;
$article->removeImage($image);
}
}
$plug = $this->em->merge($article); //It is now safe to merge the entity
foreach($tempImageList as $image)
{
$article->addImage($image); //Add the image back into the newly merged plug
}
return $article;
I can then add more images if need be, and repeat the process until I finally persist the article back to DB.
This is handy to know in the event you need to do a multiple pages creation process or adding images via AJAX.

Symfony 2 - flush in postUpdate fire preUpdate event

I detected this problem "thanks" to an exception I got:
Catchable Fatal Error: Argument 3 passed to
Doctrine\ORM\Event\PreUpdateEventArgs::__construct()
must be an array, null given, called in
/.../vendor/doctrine/lib/Doctrine/ORM/UnitOfWork.php on line 804
and defined in
/.../vendor/doctrine/lib/Doctrine/ORM/Event/PreUpdateEventArgs.php line 28
I am working on a project that requieres a specific logic:
When the order field in entity book is modified, I need to update field books_order_modified_at in the parent entity bookstore (this field allows me to know whether the order of books in a bookstore was changed).
I decided to do it in an event listener since there are many places in the code that might change the order of books.
I didn't find any way to update a related entity from preUpdate event, so I have a private field in the listener class which I use to tell the postUpdate event to update the relevant bookstore entity.
My problem is that when I do so the preUpdate event of the book entity is fired.
When I check the change-set it contains only the modified_at field, but it has the same value before and after.
If someone has another idea how to solve the problem - great.
If not - any idea how can I prevent the preUpdate event from being fired when the flush is called in teh postUpdate event??
Actually, this is a problem from doctrine Doctrine Issue DDC-2726. Solved it by adding a clear call on the entity manager after the flush in the listener so the 3-rd argument to that constructor, which is actually the entityChangeSets, will be re-written.
What about updating the modified_at within your entities and let doctrine handle it? You would change your setOrder method in your book to update the BookOrder entity like this:
class Book {
public function setOrder($order) {
// modify book
$this->bookOrder->updateModifiedAt();
}
}
Of course your BookOrder would have to implement modifiedAt:
class BookOrder {
public function updateModifiedAt() {
$this->modifiedAt = new \DateTime();
}
}
If you use other classes for your Datetime, you of course have to change this code!
Doctrine should recognize that BookOrder has changed and should update it without any need to use a event listener.
I can suggest you to use Timestampable extension for Doctrine from DoctrineExtensionsBundle.
By using it you don't need to set created_at or modified_at values. This extension does it automatically. Even it can set modified_at only when specific fields were modified. See example.
I think you are writing something like this extension. So, you don't need to do that because this is already done :)
I had a similar problem to this. Trying to use preupdate to modify child elements caused the same error. In the end, my solution to simply update the children belonging to the parent. No explicit call to flush required.
/**
* Update expiry dates for all runners belonging to a campaign
*
* #param $runners
* #param $expiryDate
*/
private function updateCampaignRunners($runners, $expiryDate){
foreach($runners as $runner){
$runner->setExpiresAt($expiryDate);
$this->getModelManager()->update($runner);
}
}
/**
* Post update and persist lifecycle callback
*
* #param Campaign $campaign
*/
private function postAction(Campaign $campaign)
{
$runnerExpire = $this->getForm()->get("runnerExpire")->getData();
if($runnerExpiryDate && $campaign->getRunners()){
$this->updateCampaignRunners($campaign->getRunners(), $runnersExpiryDate);
}
}

Resources