I'm trying to resize an image after persisting an entity with Doctrine. In my Entity code, I'm setting a field to a specific value before the flush and the update :
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->getFile()) {
// do whatever you want to generate a unique name
$filename = sha1(uniqid(mt_rand(), true));
$this->image = $filename.'.png';
}
}
So the image field is supposed to be updated.
Then in my controller, I'd like to do my resize job:
if ($form->isValid())
{
$em->persist($activite);
$em->flush();
//resize the image
$img_path = $activite->getImage();
resizeImage($img_path);
}
However, at this point in the code, the value of $activite->image is still null. How can I get the new value?
(Everything is saved well in the database.)
The EntityManager has a refresh() method to update your entity with the latest values from database.
$em->refresh($entity);
I found my error.
Actually, I was following this tutorial: http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html
and at some point they give this code to set the file:
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
// check if we have an old image path
if (isset($this->path)) {
// store the old name to delete after the update
$this->temp = $this->path;
$this->path = null;
} else {
$this->path = 'initial';
}
}
And then after the upload, in the first version (with the random filename), they do :
$this->file = null;
But then in the second version, this code is replace by:
$this->setFile(null);
My problem is that I've tried the two versions to finally come back to the first. However, I forgot to change the line to set the file to null and so everytime my path field was reset to null.
Sorry for this absurdity and thanks for your help.
Related
I am using the Symfony CMF Media Bundle to achieve the following. I am having several nodes that can have an image and a downloadable PDF.
I have already figured out that the setImage method has to be implemented like that:
public function setPreviewImage($previewImage)
{
if ($previewImage === null) {
return $this;
}
if (!$previewImage instanceof ImageInterface && !$previewImage instanceof UploadedFile) {
$type = is_object($previewImage) ? get_class($previewImage) : gettype($previewImage);
throw new \InvalidArgumentException(sprintf(
'Image is not a valid type, "%s" given.',
$type
));
}
if ($this->previewImage) {
$this->previewImage->copyContentFromFile($previewImage);
} elseif ($previewImage instanceof ImageInterface) {
$previewImage->setName('previewImage');
$this->previewImage = $previewImage;
} else {
$this->previewImage = new Image();
$this->previewImage->copyContentFromFile($previewImage);
}
return $this;
}
Then in another forum someone was suggested to make this property cascade-persistent. with that hint: https://github.com/symfony-cmf/BlockBundle/blob/master/Resources/config/doctrine-phpcr/ImagineBlock.phpcr.xml#L22. Now i am wondering how and were i can set this option in my configuration.
The next part i am wondering about is the cmf_media_file type. Has anyone out here ever managed to store a PDF into a PHPCR node property?
For any help i would be really thankful.
I figured it out by myself.
For anyone who is using annotations you have to set it up like this:
use Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
/**
* #var Image
* #PHPCR\Child(cascade="persist")
*/
I'm making an API and I need to display data from entity based on action type. For example, I have User and his visibility preferences (to hide/show his name for other people). Doing this like that:
<?php
// entity
public function getSurname()
{
$visibility = $this->getVisibility();
if($visibility['name'] == 0)
return $this->surname;
return '';
}
is ok, but if User is logged in, I want to show him his name, for example, in edit account.
The best way I think is to edit record when I get it from database, but how to this on doctrine object?
<?php
//controller
$user = $this->getDoctrine()->getRepository('AcmeDemoBundle:User')->findOneById($id);
$user = $this->getVisibility();
if($user != $this->getUser() && $visibility['name'] == 0)
$user->setSurname(''); //but this save this to DB, not to "view"
UPDATE
Unfortunately (or I'm doing something wrong) my problem can't be solved by Snake answer, beause when I do this code:
<?php
$user = $this->getDoctrine()->getRepository('AcmeDemoBundle')-findOneById($id);
return array(
self::USER => $user
);
In my API response, entity modifications don't work, because I think this is getting record directly from DB? And I need return whole object like in code above.
UPDATE2
I found workaround for this
<?php
// entity
/**
* #ORM\PostLoad
*/
public function postLoad() {
$this->surname = $this->getSurname();
}
and then I can just return full $user object
If you want to show the surname depends of visibility, you can add the Symfony\Component\Security\Core\User\EquatableInterface and edit your function:
// entity
public function getSurname(Acme\DemoBundle\User $user = null)
{
// Nothing to compare or is the owner
if( !is_null( $user ) && $this->isEqualTo($user) ){
return $this->surname;
}
// else...
$visibility = $this->getVisibility();
if($visibility['name'] == 0)
return $this->surname;
return '';
}
After in your controller you only must get the surname:
//controller
$user = $this->getDoctrine()->getRepository('AcmeDemoBundle:User')->findOneById($id);
// If the user is the owner, show the surname, otherwise it shows the surname depends of visibility
$surname = $user->getSurname( $this->getUser() );
Also, you can execute the logic in the controller (check if is the same user and get the visibility...).
I suggest you read about ACL too.
The Symfony2 PunkAve FileUpload Bundle works, but because of the returns inside the UploadHandler of BlueImp, it is not possible to get the filename.
<?php
/**
*
* #Route("/upload")
* #Template()
*/
public function uploadAction(Request $request)
{
$editId = $this->getRequest()->get('editId');
if (!preg_match('/^\d+$/', $editId))
{
throw new Exception("Bad edit id");
}
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('Foobar:Foobar')->find($editId);
$destinationFolder = 'test';
$fileUploader = $this->get('punk_ave.file_uploader');
$imageName = $fileUploader->handleFileUpload(array('folder' => $destinationFolder ));
$imageEntity = new \Foobar\Entity\Image();
$imageEntity->setImage($imageName);
$imageEntity->setFolder($destinationFolder);
$em->persist($media);
$em->flush();
return true;
}
The example above uploads the image.
The variable $imageName triggers the fileUploadHandler. There is somewhere a return, why it doesn't go the the next lines where it should save the imagename.
How can I still get it working in Symfony? To save the filename in the Entity after he handled the upload?
As they said in documentation: handleFileUpload DOES NOT RETURN as the response is generated in native PHP by BlueImp's UploadHandler class. handleFileUpload has exit(0); at the end so when you call it then entire process stops there. If you want to save files to database you should do it in action which handles request (from documentation's example it will be editAction) and there, again as documentation said, use getFiles to get the list of filenames and mirror that in your database as you see fit.
In doctrine2 I have a OneToMany association: One Application <=> Many ApplicationCost
// Application.php
/**
* #ORM\OneToMany(targetEntity="ApplicationCost", mappedBy="application", orphanRemoval=true)
*/
protected $costs;
// ApplicationCost.php
/**
* #ORM\ManyToOne(targetEntity="Application", inversedBy="costs")
* #ORM\JoinColumn(name="application_id", referencedColumnName="id")
*/
protected $application;
In Application entity I have an agregate field sumCosts:
/**
* #ORM\Column(type="decimal", scale=2)
*/
protected $sumCosts;
Which is updated when addCost and removeCost are called:
// Application.php
public function addCost(ApplicationCost $cost)
{
if (!$this->costs->contains($cost)) {
$this->sumCosts += $cost->getBalance();
$this->costs[] = $cost;
$cost->setApplication($this);
}
return $this;
}
public function removeCost(ApplicationCost $cost)
{
if ($this->costs->contains($cost)) {
$this->sumCosts -= $cost->getBalance();
$this->costs->removeElement($cost);
}
}
Assuming User can edit already existing ApplicationCost's and can change it's parent Application, how do I make sure that this agregate field is up to date?
My approach is:
// ApplicationCost.php
public function setApplication(Application $application = null)
{
if ($this->application !== null) {
$this->application->removeCost($this);
}
if ($application !== null) {
$application->addCost($this);
}
$this->application = $application;
return $this;
}
Is that good? Or am I makeing here some huge mistake here and sumCosts may be out of sync?
EDIT: I've read Doctrine's Aggregate Fields cookbook and I have the versioning (and I use locking mechanism). My question is not about concurrency.
EDIT: I've created some tests
public function testSumCosts()
{
$app = new Application();
$costA = new ApplicationCost();
$costA->setBalance(150);
$costB = new ApplicationCost();
$costB->setBalance(100);
$costC = new ApplicationCost();
$costC->setBalance(50);
$app->addCost($costA);
$app->addCost($costB);
$app->addCost($costC);
$app->removeCost($costC);
$this->assertEquals(250, $app->sumCosts(), 'Costs are summed correctly');
}
public function testCostsChangeApplication()
{
$appA = new Application();
$appB = new Application();
$costA = new ApplicationCost();
$costA->setBalance(100);
$costB = new ApplicationCost();
$costB->setBalance(50);
$appA->addCost($costA);
$appB->addCost($costB);
$costA->setApplication($appB);
$costB->setApplication(null);
$this->assertEquals(0, $appA->sumCosts(), 'Costs are removed correctly');
$this->assertEquals(100, $appB->sumCosts(), 'Costs are added correctly');
}
And after adding $cost->setApplication($this); to addEntry both tests are green. Though I still wonder if I might have missed something.
Okay, I think I finally achieved desired result. I'll describe it for future reference and anyone who might have the same problem:
First of all correct the class
// Application.php
public function addCost(ApplicationCost $cost)
{
if (!$this->costs->contains($cost)) {
$this->sumCosts += $cost->getBalance();
}
$this->costs[] = $cost;
return $this;
}
public function removeCost(ApplicationCost $cost)
{
if ($this->costs->contains($cost)) {
$this->sumCosts -= $cost->getBalance();
}
$this->costs->removeElement($cost);
}
If you compare this to my original code you'll see that only updateing the agregate field is under condition. It does not hurt as collections can't hold duplicate elements and can't remove non existing elements.
Second of all, configure the cascade={all} option on inverse side of association (that is, on costs inside Application.php). So whenever you add/remove costs they are persisted too.
to be continued... (have to test what happens when i change application it from the owning side and persist only ApplicationCost -> will both old and new Application be updated?)
I asked this question and found out that we can't get the error message thrown by a DataTransformer (according to the only user who answered, maybe it's possible, I don't know).
Anyway, now that I know that, I am stucked with a problem of validation. Suppose my model is this one: I have threads that contains several participants (users).
<?php
class Thread
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="My\UserBundle\Entity\User")
* #ORM\JoinTable(name="messaging_thread_user")
*/
private $participants;
// other fields, getters, setters, etc
}
For thread creation, I want the user to specify the participants usernames in a textarea, separated by "\n".
And I want that if one or more of the usernames specified don't exist, a message is displayed with the usernames that don't exist.
For example, "Users titi, tata and toto don't exist".
For that I created a DataTransformer that transforms the raw text in the textarea into an ArrayCollection containing instances of users. Since I can't get the error message provided by this DataTransformer (such a shame! Is it really impossible?), I don't check the existence of each usernames in the DataTransformer but in the Validator.
Here is the DataTransformer that converts \n-separated user list into an ArrayCollection (so that the DataBinding is ok):
<?php
public function reverseTransform($val)
{
if (empty($val)) {
return null;
}
$return = new ArrayCollection();
// Extract usernames in an array from the raw text
$val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
foreach ($usernames as $username) {
$user = new User();
$user->setUsername($username);
if (!$return->contains($user)) {
$return->add($user);
}
}
return $return;
}
And here is my validator:
<?php
public function isValid($value, Constraint $constraint)
{
$repo = $this->em->getRepository('MyUserBundle:User');
$notValidUsernames = array();
foreach ($value as $user) {
$username = $user->getUsername();
if (!($user = $repo->findOneByUsername($username))) {
$notValidUsernames[] = $username;
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
// Create the list of usernames separated by commas
$list = '';
$i = 1;
foreach ($notValidUsernames as $username) {
if ($i < count($notValidUsernames)) {
$list .= $username;
if ($i < count($notValidUsernames) - 1) {
$list .= ', ';
}
}
$i++;
}
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $list,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
This current implementation looks ugly. I can see the error message well, but the users in the ArrayCollection returned by the DataTransformer are not synchronized with Doctrine.
I got two questions:
Is there any way that my validator could modify the value given in parameter? So that I can replace the simple User instances in the ArrayCollection returned by the DataTransformer into instances retrieved from the database?
Is there a simple and elegant way to do what I'm doing?
I guess the most simple way to do this is to be able to get the error message given by the DataTransformer. In the cookbook, they throw this exception: throw new TransformationFailedException(sprintf('An issue with number %s does not exist!', $val));, if I could put the list of non-existing usernames in the error message, it would be cool.
Thanks!
I am the one that answered your previous thread so maybe someone else will jump in here.
Your code can be simplified considerably. You are only dealing with user names. No need for use objects or array collections.
public function reverseTransform($val)
{
if (empty($val)) { return null; }
// Extract usernames in an array from the raw text
// $val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
// No real need to check for dups here
return $usernames;
}
The validator:
public function isValid($userNames, Constraint $constraint)
{
$repo = $this->em->getRepository('SkepinUserBundle:User');
$notValidUsernames = array();
foreach ($userNames as $userName)
{
if (!($user = $repo->findOneByUsername($username)))
{
$notValidUsernames[$userName] = $userName; // Takes care of dups
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
$invalidNames = implode(' ,',$notValidUsernames);
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $invalidNames,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
=========================================================================
So at this point
We have used transformer to copy the data from the text area and generated an array of user names during form->bind().
We then used a validator to confirm that each user name actually exists in the database. If there are any that don't then we generate an error message and form->isValid() will fail.
So now we are back in the controller, we know we have a list of valid user names (possibly comma delimited or possibly just an array). Now we want to add these to our thread object.
One way would to create a thread manager service and add this functionality to it. So in the controller we might have:
$threadManager = $this->get('thread.manager');
$threadManager->addUsersToThread($thread,$users);
For the thread manager we would inject our entity manager. In the add users method we would get a reference to each of the users, verify that the thread does not already have a link to this user, call $thread->addUser() and then flush.
The fact that we have wrapped up this sort of functionality into a service class will make things easier to test as we can also make a command object and run this from the command line. it also gives us a nice spot to add additional thread related functionality. We might even consider injecting this manager into the user name validator and moving some of the isValid code to the manager.