Doctrine 2 ManyToMany and preUpdate - php

I'm using Symfony2.5 and Doctrine 2.4.2.
I have two entities, PageImages which extends Page
Page:
/**
* #ORM\ManyToMany(targetEntity="PageImage",cascade={"persist"})
*/
private $images;
Using form collection to create add images to the Page entity
When i create a new entry for PageImage it works just fine, PrePersist,PreUpdate,PostPersist,PostUpdate all run,
But when I try to change the image for an existing PageImage the event doesn't fire and doesn't upload new images.
Update:
It doesn’t updating because the $file field is just a virtual and doctrine doesn’t see new data, and events aren't fired, so the easiest solution to do:
public function setFile(UploadedFile $file = null)
{
$this->temp = $this->getPath();
$this->file = $file;
$this->preUpload();
}

I found a solution, maybe its not very beautiful and someone can suggest any other way to solve the problem.
I've figured out, what’s the problem, because the $file is virtual field doctrine thinks there’s no new data in the entity and no need to persist or update it and the events doesn’t fired so I need to run it myself ot setFile():
public function setFile(UploadedFile $file = null)
{
$this->temp = $this->getPath();
$this->file = $file;
$this->preUpload();
}

In your Edit action you will have to manually exploit the 'upload' method to current image like this
$currentImage->upload();
$entityManager->persist($currentImage);
$entityManager->flush();

Related

Eloquent - How to wait until after create() without Observer

The Error
Call to a member function media() on null
The Scenario
I have several related models. I have an "Event" model. Attached to this (via 1 to 1) is a Gallery model. Attached to that (via one to many) is a "Media" model. In the "created" observer for the Event, I'm trying to attach its gallery. I can create the gallery no problem, but when I then try and attach media to it I get the above error.
My code
if ($model->gallery === null) {
$this->createGallery($model);
}
// product images or banner images
$file = Storage::put("public/event_images/", $image);
$file = str_replace("public/event_images/", "", $file);
$file = "/" . $file;
$model->gallery->media()->create([
"path" => $file,
"type" => "image"
]);
// The createGallery() function
private function createGallery($model)
{
$model->gallery()->create();
}
So I know to fix this, I have to "wait" until the gallery has been created before I try to access its relationships. But I can't figure out how to do this. This code works the second time it's run, suggesting that the gallery is indeed created - just not quickly enough before the code hits media().
PHP is a synchronous programming language, so it is impossible that you have to wait for something to complete.
The problem is that you loaded the relation already and this loaded relation won't re-validate until you load it again. which can be done using the load() function.
Change your code to create the gallery like this:
if ($model->gallery === null) {
// Create the related model
$this->createGallery($model);
// Load the relation again
$model->load('gallery');
}
I think you might need to refresh the model before trying to access its relationship. Try $model->refresh() before you attempt to attach the image to the gallery. Something like
if ($model->gallery === null) {
$this->createGallery($model);
$model->refresh();
}
The model won't be aware of the newly created gallery otherwise.

Make model dynamically using user supplied model name in laravel

I am stuck with it and i couldn't find any appropriate solution for this. what i want to achieve is in my admin panel based upon check box value i want to change active status of specific control and update database value using ajax. I want to make a common ajax function in controller to avoid repeatedly writing same ajax function for other controls like menu manager content manager, documents manager etc.So i want to send Model name to the ajax controller so that same function can be used. Ajax call is working perfectly but couldn't make appropriate models. for example:
$m = new App\Model.$request->model OR $m = 'App\Model\'.$request->model (adding last \ gives an error) or answer provided in Dynamically use model in laravel is not helping either. Is there any better ways if yes please suggest. I can do this below but it is hardcoded so i want to make model dynamic models
if($request->model ==='Menu')
$model = new \App\Http\Models\Menu;
else if($request->model === 'News')
$this->model = new \App\Http\Models\News;
else if($request->model === 'Document')
$this->model = new \App\Http\Models\Document;
Thankyou !!!
You can just use:
$modelName = '\\App\\Http\\Models\\'.$request->model;
$this->model = new $modelName;
However you should add validation to make sure only some allowed models would be used or at least do something like this:
if (! in_array($request->model, ['Menu', 'News', 'Document']))
{
throw new \Exception('Invalid model');
}
$modelName = '\\App\\Http\\Models\\'.$request->model;
$this->model = new $modelName;
This is because you don't want to expose probably all the models data for security reasons.
Try the below code
if($request->model){
$m = '\App'. '\' .$request->model;
//other code
}

Get auto generated ID right after flush() Symfony3

I have a Project entity and when creating a new project I need to be able to upload files. For this reason I have created Files entity which saves the path and projectID. My problem is that I don't know how to retrieve it after creating it. Here is what I am trying to do
$projectService->createProject($project,$user,$isWithoutTerm,self::NO_TERM_DEFAULT_VALUE);
$filesService = $this->get('app.service.files_service');
foreach ($managerFiles as $managerFile){
$fileName = $filesService->uploadFileAndReturnName($managerFile);
$filesService->createFile($fileName,$project->getId(),$user);
}
Currently my $project doesn't have ID which means that I cant create a new file. I heard that I could use $em->retrieve(object), but the actual flushing is not done in the controller. If i try to use it in my createProject function and return it for some reason I can't. PHPStorm says that it is a void function. Here is the code in createProject
$project->setFromUser($user->getFullName());
$project->setDepartment($user->getDepartment());
$project->setIsOver(false);
$project->setDate(new \DateTime());
if($isWithoutTerm){
$project->setTerm(\DateTime::createFromFormat('Y-m-d', $noTermDefaultValue));
}
$this->entityManager->persist($project);
$this->entityManager->flush();
Is there a way to retrieve the projectID after flushing and being able to use it in my controller?
It could be a race condition where the $project is not available yet and so the service cannot load it and thus getId() fails.
First, create your project entity:
//build your $project
$this->entityManager->flush();
Second, send this $project to your "service"
$projectService->createProject($project,$user,$isWithoutTerm,self::NO_TERM_DEFAULT_VALUE);
$filesService = $this->get('app.service.files_service');
foreach ($managerFiles as $managerFile){
$fileName = $filesService->uploadFileAndReturnName($managerFile);
$filesService->createFile($fileName,$project->getId(),$user);
}
More specifically if you wrap this in a try/catch you can identify exactly what is happening:
try {
$project = new Project();
//build project
$this->entityManager->flush();
//load $projectService..
$projectService->createProject($project,$user,$isWithoutTerm,self::NO_TERM_DEFAULT_VALUE);
$filesService = $this->get('app.service.files_service');
foreach ($managerFiles as $managerFile){
$fileName = $filesService->uploadFileAndReturnName($managerFile);
$filesService->createFile($fileName,$project->getId(),$user);
}
}
catch(\Exception $e) {
//$e->getMessage() will tell you if you're good to go, or if there
//is actually an issue
}
However, it could also be as simple as PHPStorm not being able to introspect your getId() method and thus doesn't understand it. OR getId() is not actually returning the id.
Upgrading the Symfony from 3.2 and 3.3 fixed the issue. I don't think it was due to the version itself, but rather removing depricated stuff and changing a lot of things and something fixed it.

Check if a Symfony Doctrine Entity has Changed from Form Submission

Question
Can I use the Doctrine entity manager (or some other Symfony function) to check if an entity has been updated?
Background
I am building a CMS with the ability to save "versions" of each page. So I have a Doctrine annotated entity $view (which is basically the "page), and this entity has nested associated entities like $view->version (which contain the majority of the information that can be updated in different revisions). This entity is edited with a standard Symfony form in the CMS. When the form is submitted, it does a $em->persist($view) and the Entity Manager detects if any of the fields have been changed. If there are changes, the changes are persisted. If there are no changes, the entity manager ignores the persist and saves itself a database call to update. Great.
But before the entity is saved, my versioning system checks if it's been more than 30 minutes since the current version was last save, or if the user submitting the form is different than the user who saved the current version, and if so it clones the $viewVersion. So the main record for $view remains the same id, but it works from an updated revision. This works great.
HOWEVER... If it's been a while since the last save, and someone just looks at the record without changing anything, and hits save, I don't want the version system to clone a new version automatically. I want to check and confirm that the entity has actually changed. The Entity Manager does this before persisting an entity. But I can't rely on it because before I call $em->persist($view) I have to clone $view->version. But before I clone $view->version I need to check if any of the fields in the entity or it's nested entities have been updated.
Basic Solution
The solution is to calculate the change set:
$form = $this->createForm(new ViewType(), $view);
if ($request->isMethod( 'POST' )) {
$form->handleRequest($request);
if( $form->isValid() ) {
$changesFound = array();
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
// The Version (hard coded because it's dynamically associated)
$changeSet = $uow->getEntityChangeSet($view->getVersion());
if(!empty($changeSet)) {
$changesFound = array_merge($changesFound, $changeSet);
}
// Cycle through Each Association
$metadata = $em->getClassMetadata("GutensiteCmsBundle:View\ViewVersion");
$associations = $metadata->getAssociationMappings();
foreach($associations AS $k => $v) {
if(!empty($v['cascade'])
&& in_array('persist', $v['cascade'])
){
$fn = 'get'.ucwords($v['fieldName']);
$changeSet = $uow->getEntityChangeSet($view->getVersion()->{$fn}());
if(!empty($changeSet)) {
$changesFound = array_merge($changesFound, $changeSet);
}
}
}
}
}
The Complication
But I read that you shouldn't use this $uow->computerChangeSets() outside of a the lifecycle events listener. They say you should do a manual diff of the objects, e.g. $version !== $versionOriginal. But that doesn't work because some fields like timePublish always get updated, so they are always different. So is it really not possible to use this to getEntityChangeSets() in the context of a controller (outside of an event listener)?
How should I use an Event Listener? I don't know how to put all the pieces together.
UPDATE 1
I followed the advice and created an onFlush event listener, and presumably that should load automatically. But now the page has a big error which happens when my service definition for gutensite_cms.listener.is_versionable passes in another service of mine arguments: [ "#gutensite_cms.entity_helper" ]:
Fatal error: Uncaught exception 'Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException' with message 'Circular reference detected for service "doctrine.dbal.cms_connection", path: "doctrine.dbal.cms_connection".' in /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php:456 Stack trace: #0 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(604): Symfony\Component\DependencyInjection\Dumper\PhpDumper->addServiceInlinedDefinitionsSetup('doctrine.dbal.c...', Object(Symfony\Component\DependencyInjection\Definition)) #1 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(630): Symfony\Component\DependencyInjection\Dumper\PhpDumper->addService('doctrine.dbal.c...', Object(Symfony\Component\DependencyInjection\Definition)) #2 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(117): Symfony\Componen in /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php on line 456
My Service Definition
# This is the helper class for all entities (included because we reference it in the listener and it breaks it)
gutensite_cms.entity_helper:
class: Gutensite\CmsBundle\Service\EntityHelper
arguments: [ "#doctrine.orm.cms_entity_manager" ]
gutensite_cms.listener.is_versionable:
class: Gutensite\CmsBundle\EventListener\IsVersionableListener
#only pass in the services we need
# ALERT!!! passing this service actually causes a giant symfony fatal error
arguments: [ "#gutensite_cms.entity_helper" ]
tags:
- {name: doctrine.event_listener, event: onFlush }
My Event Listener: Gutensite\CmsBundle\EventListener\isVersionableListener
class IsVersionableListener
{
private $entityHelper;
public function __construct(EntityHelper $entityHelper) {
$this->entityHelper = $entityHelper;
}
public function onFlush(OnFlushEventArgs $eventArgs)
{
// this never executes... and without it, the rest doesn't work either
print('ON FLUSH EXECUTING');
exit;
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$updatedEntities = $uow->getScheduledEntityUpdates();
foreach($updatedEntities AS $entity) {
// This is generic listener for all entities that have an isVersionable method (e.g. ViewVersion)
// TODO: at the moment, we only want to do the following code for the viewVersion entity
if (method_exists($entity, 'isVersionable') && $entity->isVersionable()) {
// Get the Correct Repo for this entity (this will return a shortcut
// string for the repo, e.g. GutensiteCmsBundle:View\ViewVersion
$entityShortcut = $this->entityHelper->getEntityBundleShortcut($entity);
$repo = $em->getRepository($entityShortcut);
// If the repo for this entity has an onFlush method, use it.
// This allows us to keep the functionality in the entity repo
if(method_exists($repo, 'onFlush')) {
$repo->onFlush($em, $entity);
}
}
}
}
}
ViewVersion Repo with onFlush Event: Gutensite\CmsBundle\Entity\View\ViewVersionRepository
/**
* This is referenced by the onFlush event for this entity.
*
* #param $em
* #param $entity
*/
public function onFlush($em, $entity) {
/**
* Find if there have been any changes to this version (or it's associated entities). If so, clone the version
* which will reset associations and force a new version to be persisted to the database. Detach the original
* version from the view and the entity manager so it is not persisted.
*/
$changesFound = $this->getChanges($em, $entity);
$timeModMin = (time() - $this->newVersionSeconds);
// TODO: remove test
print("\n newVersionSeconds: ".$this->newVersionSeconds);
//exit;
/**
* Create Cloned Version if Necessary
* If it has been more than 30 minutes since last version entity was save, it's probably a new session.
* If it is a new user, it is a new session
* NOTE: If nothing has changed, nothing will persist in doctrine normally and we also won't find changes.
*/
if($changesFound
/**
* Make sure it's been more than default time.
* NOTE: the timeMod field (for View) will NOT get updated with the PreUpdate annotation
* (in /Entity/Base.php) if nothing has changed in the entity (it's not updated).
* So the timeMod on the $view entity may not get updated when you update other entities.
* So here we reference the version's timeMod.
*/
&& $entity->getTimeMod() < $timeModMin
// TODO: check if it is a new user editing
// && $entity->getUserMod() ....
) {
$this->iterateVersion($em, $entity);
}
}
public function getChanges($em, $entity) {
$changesFound = array();
$uow = $em->getUnitOfWork();
$changes = $uow->getEntityChangeSet($entity);
// Remove the timePublish as a valid field to compare changes. Since if they publish an existing version, we
// don't need to iterate a version.
if(!empty($changes) && !empty($changes['timePublish'])) unset($changes['timePublish']);
if(!empty($changes)) $changesFound = array_merge($changesFound, $changes);
// The Content is hard coded because it's dynamically associated (and won't be found by the generic method below)
$changes = $uow->getEntityChangeSet($entity->getContent());
if(!empty($changes)) $changesFound = array_merge($changesFound, $changes);
// Check Additional Dynamically Associated Entities
// right now it's just settings, but if we add more in the future, this will catch any that are
// set to cascade = persist
$metadata = $em->getClassMetadata("GutensiteCmsBundle:View\ViewVersion");
$associations = $metadata->getAssociationMappings();
foreach($associations AS $k => $v) {
if(!empty($v['cascade'])
&& in_array('persist', $v['cascade'])
){
$fn = 'get'.ucwords($v['fieldName']);
$changes = $uow->getEntityChangeSet($entity->{$fn}());
if(!empty($changeSet)) $changesFound = array_merge($changesFound, $changes);
}
}
if(!$changesFound) $changesFound = NULL;
return $changesFound;
}
/**
* NOTE: This function gets called onFlush, before the entity is persisted to the database.
*
* VERSIONING:
* In order to calculate a changeSet, we have to compare the original entity with the form submission.
* This is accomplished with a global onFlush event listener that automatically checks if the entity is versionable,
* and if it is, checks if an onFlush method exists on the entity repository. $this->onFlush compares the unitOfWork
* changeSet and then calls this function to iterate the version.
*
* In order for versioning to work, we must
*
*
*/
public function iterateVersion($em, $entity) {
$persistType = 'version';
// We use a custom __clone() function in viewVersion, viewSettings, and ViewVersionTrait (which is on each content type)
// It ALSO sets the viewVersion of the cloned version, so that when the entity is persisted it can properly set the settings
// Clone the version
// this clones the $view->version, and the associated entities, and resets the associated ids to null
// NOTE: The clone will remove the versionNotes, so if we decide we actually want to keep them
// We should fetch them before the clone and then add them back in manually.
$version = clone $entity();
// TODO: Get the changeset for the original notes and add the versionNotes back
//$version->setVersionNotes($versionModified->getVersionNotes());
/**
* Detach original entities from Entity Manager
*/
// VERSION:
// $view->version is not an associated entity with cascade=detach, it's just an object container that we
// manually add the current "version" to. But it is being managed by the Entity Manager, so
// it needs to be detached
// TODO: this can probably detach ($entity) was originally $view->getVersion()
$em->detach($entity);
// SETTINGS: The settings should cascade detach.
// CONTENT:
// $view->getVersion()->content is also not an associated entity, so we need to manually
// detach the content as well, since we don't want the changes to be saved
$em->detach($entity->getContent());
// Cloning removes the viewID from this cloned version, so we need to add the new cloned version
// to the $view as another version
$entity->getView()->addVersion($version);
// TODO: If this has been published as well, we need to mark the new version as the view version,
// e.g. $view->setVersionId($version->getId())
// This is just for reference, but should be maintained in case we need to utilize it
// But how do we know if this was published? For the time being, we do this in the ContentEditControllerBase->persist().
}
So my understanding is that you basically need to detect if doctrine is going to update an entity in the database so you can record that change or insert a version of the old entity.
The way you should do that is by adding a listener to the onFlush event. You can read more about registering doctrine events here.
For example you will need to add to your config file a new service definition like that:
my.flush.listener:
class: Gutensite\CmsBundle\EventListener\IsVersionableListener
calls:
- [setEntityHelper, ["#gutensite_cms.entity_helper"]]
tags:
- {name: doctrine.event_listener, event: onFlush}
Then you will create the class EventListener like any symfony service. In this class, a function with the same name as the event will be called, ( onFlush in this case )
Inside this function you can go through all updated entities:
namespace Gutensite\CmsBundle\EventListener;
class IsVersionableListener {
private $entityHelper;
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$updatedEntities = $uow->getScheduledEntityUpdates();
foreach ($updatedEntities as $entity) {
if ($entity->isVersionable()) {
$changes = $uow->getEntityChangeSet($entity);
//Do what you want with the changes...
}
}
}
public function setEntityHelper($entityHelper)
{
$this->entityHelper = $entityHelper;
return $this;
}
}
$entity->isVersionable() is just a method I made up which you can add to your entities to easily decide whether this entity is tracked for changes or not.
NOTE: Since you are doing this in the onFlush. That means that all changes that will be saved to the DB have been computed. Doctrine will not persist any new entities. If you create new entities you will need to manually compute the changes and persist them.
First thing: there is a versionable extension for Doctrine (it was recently renamed to Loggable), that does exactly what you are describing, check that out, maybe it solves your use case.
With that said, this sounds like a job for an onFlush event listener. The UnitOfWork is already in a "changes computed" state, where you can just ask for all of the the changes on all of the entities (you can filter them with an instanceof, or something like that).
This still doesn't solve your issue about saving a new, and the old version too. I am not 100% sure this will work, because persisting something in an onFlush listener will involve workarounds (since doing a flush in an onFlush will result in an infinite loop), but there is $em->refresh($entity) that will roll back an entity to its "default" state (as it was constructed from the database).
So you can try something like, check to see if there are changes to the entity, if there are, clone it, persist the new one, refresh the old one, and save them. You will have to do extra legwork for your relations though, because cloning only creates a shallow copy in PHP.
I'd advise to go with the versionable extension, since it has everything figured out, but read up on the onFlush listener too, maybe you can come up with something.
In case someone is still interested in a different way than the accepted answer (it was not working for me and I found it messier than this way in my personal opinion).
I installed the JMS Serializer Bundle and on each entity and on each property that I consider a change I added a #Group({"changed_entity_group"}). This way, I can then make a serialization between the old entity, and the updated entity and after that it's just a matter of saying $oldJson == $updatedJson. If the properties that you are interested in or that you would like to consider changes the JSON won't be the same and if you even want to register WHAT specifically changed then you can turn it into an array and search for the differences.
I used this method since I was interested mainly in a few properties of a bunch of entities and not in the entity entirely. An example where this would be useful is if you have a #PrePersist #PreUpdate and you have a last_update date, that will always be updated therefore you will always get that the entity was updated using unit of work and stuff like that.
Hope this method is helpful to anyone.

Symfony Set value of a form field in doSave

I need to override my form like this:
protected function doSave($con = null)
{
$filename= Helper::generateFilename($this->getValue('name')).'jpg';
imagejpeg(Helper::createImage($this->getValues()), sfConfig::get('sf_upload_dir').'/poster/'.$filename );
/* Here I need to set value of *fielname* field of the form equal to
$filename */
parent::doSave($con);
}
Helper.class.php is located in /lib/so it acts like a helper class.
generateFilename actually gives a randomized string. since there is no function like setValue in a form, I'm stuck.
I tried macgyver's approach but didn't work for me. Not sure if I did anything wrong.
Syfmony version is 1.4 with Doctrine.
I had to override the updateObject() function inside the form class.
In my case its inside TimesheetForm.class.php. I had to change the status of the timesheet based on user action.
Following is the piece of code that that I tried. It did update the object before saving. Which was seen effected in the db.
public function updateObject($values = null)
{
$object = parent::updateObject($values);
$object->setStatus("Submitted");
return $object;
}
I didn't impelement/override the doSave() function. I'm using symfony 1.4.
Hope the above helps.
You have to use, say you have used filename as field in your schema to store the image filename, $this->object->setFilename($filename); before parent::doSave($con);

Categories