I have working entity References.php including Image, but I don't know how to in Symfony2 delete old image saved in this reference (if exists) and create new. Because now, it didn't delete current image, so only created a new and set new image_path into this entity. Here is my try to delete it on preUpload method but it set current file to NULL and then nothing (so I have error - You have to choose a file)
<?php
namespace Acme\ReferenceBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* #ORM\Entity(repositoryClass="Acme\ReferenceBundle\Entity\ReferenceRepository")
* #ORM\Table(name="`references`")
* #ORM\HasLifecycleCallbacks
*/
class Reference
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=200)
* #Assert\NotBlank(
* message = "Name cannot be blank"
* )
* #Assert\Length(
* min = "3",
* minMessage = "Name is too short"
* )
*/
private $name;
/**
* #ORM\Column(type="string", length=200)
* #Assert\NotBlank(
* message = "Description cannot be blank"
* )
* #Assert\Length(
* min = "3",
* minMessage = "Description is too short"
* )
*/
private $description;
/**
* #ORM\Column(type="string", length=200)
* #Assert\Url(
* message = "URL is not valid"
* )
*/
private $url;
/**
* #ORM\ManyToMany(targetEntity="Material", inversedBy="references")
* #Assert\Count(min = 1, minMessage = "Choose any material")
*/
private $materials;
/**
* #ORM\Column(type="text", length=255, nullable=false)
* #Assert\NotNull(
* message = "You have to choose a file"
* )
*/
private $image_path;
/**
* #Assert\File(
* maxSize = "5M",
* mimeTypes = {"image/jpeg", "image/gif", "image/png", "image/tiff"},
* maxSizeMessage = "Max size of file is 5MB.",
* mimeTypesMessage = "There are only allowed jpeg, gif, png and tiff images"
* )
*/
private $file;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Reference
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return Reference
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set url
*
* #param string $url
* #return Reference
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get url
*
* #return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set materials
*
* #param string $materials
* #return Reference
*/
public function setMaterials($materials)
{
$this->materials = $materials;
return $this;
}
/**
* Get materials
*
* #return string
*/
public function getMaterials()
{
return $this->materials;
}
/**
* Set image_path
*
* #param string $imagePath
* #return Reference
*/
public function setImagePath($imagePath)
{
$this->image_path = $imagePath;
return $this;
}
/**
* Get image_path
*
* #return string
*/
public function getImagePath()
{
return $this->image_path;
}
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
}
/**
* Get file.
*
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Called before saving the entity
*
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
$oldImage = $this->image_path;
$oldImagePath = $this->getUploadRootDir().'/'.$oldImage;
if (null !== $this->file) {
if($oldImage && file_exists($oldImagePath)) unlink($oldImagePath); // not working correctly
$filename = sha1(uniqid(mt_rand(), true));
$this->image_path = $filename.'.'.$this->file->guessExtension();
}
}
/**
* Called before entity removal
*
* #ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
/**
* Called after entity persistence
*
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->file) {
return;
}
// use the original file name here but you should
// sanitize it at least to avoid any security issues
// move takes the target directory and then the
// target filename to move to
$this->file->move(
$this->getUploadRootDir(),
$this->image_path
);
// set the path property to the filename where you've saved the file
$this->image_path = $this->file->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->file = null;
}
protected function getAbsolutePath()
{
return null === $this->image_path
? null
: $this->getUploadRootDir().'/'.$this->image_path;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded
// documents should be saved
return __DIR__.'/../../../../'.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn't screw up
// when displaying uploaded doc/image in the view.
return 'uploads/references';
}
public function getWebPath()
{
return $this->getUploadDir().'/'.$this->image_path;
}
/**
* Constructor
*/
public function __construct()
{
$this->materials = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add materials
*
* #param \Acme\ReferenceBundle\Entity\Material $materials
* #return Reference
*/
public function addMaterial(\Acme\ReferenceBundle\Entity\Material $materials)
{
$this->materials[] = $materials;
return $this;
}
/**
* Remove materials
*
* #param \Acme\ReferenceBundle\Entity\Material $materials
*/
public function removeMaterial(\Acme\ReferenceBundle\Entity\Material $materials)
{
$this->materials->removeElement($materials);
}
}
Any idea?
So I found solution. For first, I had to create an Assert callback for File Uploading, because I was using NotNull() Assert for Reference entity. So if I selected any file and sent form, I was always getting error You have to choose a file. So my first edit was here:
use Symfony\Component\Validator\ExecutionContextInterface; // <-- here
/**
* #ORM\Entity(repositoryClass="Acme\ReferenceBundle\Entity\ReferenceRepository")
* #ORM\Table(name="`references`")
* #ORM\HasLifecycleCallbacks
* #Assert\Callback(methods={"isFileUploadedOrExists"}) <--- and here
*/
class Reference
{
// code
}
and then in my code add a new method:
public function isFileUploadedOrExists(ExecutionContextInterface $context)
{
if(null === $this->image_path && null === $this->file)
$context->addViolationAt('file', 'You have to choose a file', array(), null);
}
Also I deleted NotNull assertion in my $image_path property.
Then it was working successfuly - if I selected a file and submitted the form, reference was created with image. But it wasn't finished yet. There was my problem which I asked in this question - delete old image and create a new image with new path, of course.
After many experiments, i found the working and good looking solution. In my controller, I added one variable before form validation and after it is used to delete old image:
$oldImagePath = $reference->getImagePath(); // get path of old image
if($form->isValid())
{
if ($form->get('file')->getData() !== null) { // if any file was updated
$file = $form->get('file')->getData();
$reference->removeFile($oldImagePath); // remove old file, see this at the bottom
$reference->setImagePath($file->getClientOriginalName()); // set Image Path because preUpload and upload method will not be called if any doctrine entity will not be changed. It tooks me long time to learn it too.
}
$em->persist($reference);
try {
$em->flush();
} catch (\PDOException $e) {
//sth
}
And my removeFile() method:
public function removeFile($file)
{
$file_path = $this->getUploadRootDir().'/'.$file;
if(file_exists($file_path)) unlink($file_path);
}
And at the end, I deleted $this->image_path = $this->file->getClientOriginalName(); line in upload() method because it causes a problem with preview image in the form, if you use any. It sets an original file name as path, but if you reload page, you will see the real path of image. Removing this line will fix the problem.
Thanks everyone to posting answers, who helps me to find the solution.
If the image_path is already set there is an "old" image you want to replace.
Inside your upload() method instead of ...
// set the path property to the filename where you've saved the file
$this->image_path = $this->file->getClientOriginalName();
... check for existance of a previous file and remove it before:
if ($this->image_path) {
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
$this->image_path = $this->file->getClientOriginalName();
the #Assert\NotNull on the image_path property is tested before your PrePersist/PreUpdate method, so the form validation is not happy because image_path is only provided in the entity internal, the request does not provide the form with "image_path" property, I think you should remove this Assert which is not really useful I think since it is not linked to a form.
OR
your old image_path is the fresh one, and not the old one because it is processed after form binding.
You should use event listeners, which are way better then annotation events in entities, so that you will be able in preUpdate event to retrieve the right values.
You could the use methods like these:
hasChangedField($fieldName) to check if the given field name of the current entity changed.
getOldValue($fieldName) and getNewValue($fieldName) to access the values of a field.
setNewValue($fieldName, $value) to change the value of a field to be updated.
Related
I'm using Symfony 3.4 to work on a simple REST API microservice. There are not much resources to be found when working with HTTP APIs and file uploads. I'm following some of the instructions from the documentation but I found a wall.
What I want to do is to store the relative path to an uploaded file on an entity field, but it seems like the validation expects the field to be a full path.
Here's some of my code:
<?php
// BusinessClient.php
namespace DemoBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use ApiBundle\Entity\BaseEntity;
use ApiBundle\Entity\Client;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints;
/**
* Class BusinessClient
* #package DemoBundle\Entity
* #ORM\Entity(repositoryClass="DemoBundle\Repository\ClientRepository")
* #ORM\Table(name="business_client")
* #Serializer\ExclusionPolicy("all")
* #Serializer\AccessorOrder("alphabetical")
*/
class BusinessClient extends BaseEntity
{
/**
* #Constraints\NotBlank()
* #ORM\ManyToOne(targetEntity="ApiBundle\Entity\Client", fetch="EAGER")
* #ORM\JoinColumn(name="client_id", referencedColumnName="oauth2_client_id", nullable=false)
*/
public $client;
/**
* #Constraints\NotBlank()
* #ORM\Column(type="string", length=255, nullable=false)
* #Serializer\Expose
*/
protected $name;
/**
* #Constraints\Image(minWidth=100, minHeight=100)
* #ORM\Column(type="text", nullable=true)
*/
protected $logo;
/**
* One Business may have many brands
* #ORM\OneToMany(targetEntity="DemoBundle\Entity\Brand", mappedBy="business")
* #Serializer\Expose
*/
protected $brands;
/**
* BusinessClient constructor.
*/
public function __construct()
{
$this->brands = new ArrayCollection();
}
/**
* Set the links property for the resource response
*
* #Serializer\VirtualProperty(name="_links")
* #Serializer\SerializedName("_links")
*/
public function getLinks()
{
return [
"self" => "/clients/{$this->getId()}",
"brands" => "/clients/{$this->getId()}/brands"
];
}
/**
* Get the name of the business client
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the name of the business client
*
* #param string $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* Get the logo
*
* #Serializer\Expose
* #Serializer\VirtualProperty(name="logo")
* #Serializer\SerializedName("logo")
*/
public function getLogo()
{
return $this->logo;
}
/**
* Set the logo field
*
* #param string|File|UploadedFile $logo
*/
public function setLogo($logo): void
{
$this->logo = $logo;
}
/**
* Get the client property
*
* #return Client
*/
public function getClient()
{
return $this->client;
}
/**
* Set the client property
*
* #param Client $client
*/
public function setClient($client): void
{
$this->client = $client;
}
}
Uploader Service:
<?php
namespace DemoBundle\Services;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Class FileUploader
* #package DemoBundle\Services
*/
class FileUploader
{
/** #var string $uploadDir The directory where the files will be uploaded */
private $uploadDir;
/**
* FileUploader constructor.
* #param $uploadDir
*/
public function __construct($uploadDir)
{
$this->uploadDir = $uploadDir;
}
/**
* Upload a file to the specified upload dir
* #param UploadedFile $file File to be uploaded
* #return string The unique filename generated
*/
public function upload(UploadedFile $file)
{
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move($this->getTargetDirectory(), $fileName);
return $fileName;
}
/**
* Get the base dir for the upload files
* #return string
*/
public function getTargetDirectory()
{
return $this->uploadDir;
}
}
I've registered the service:
services:
# ...
public: false
DemoBundle\Services\FileUploader:
arguments:
$uploadDir: '%logo_upload_dir%'
And last the controller:
<?php
namespace DemoBundle\Controller;
use ApiBundle\Exception\HttpException;
use DemoBundle\Entity\BusinessClient;
use DemoBundle\Services\FileUploader;
use FOS\RestBundle\Controller\Annotations as REST;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints\ImageValidator;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Class BusinessClientController
* #package DemoBundle\Controller
*/
class BusinessClientController extends BaseController
{
/**
* Create a new business entity and persist it in database
*
* #REST\Post("/clients")
* #SWG\Tag(name="business_clients")
* #SWG\Response(
* response="201",
* description="Create a business client and return it's data"
* )
* #param Request $request
* #param FileUploader $uploader
* #return Response
* #throws HttpException
*/
public function createAction(Request $request, FileUploader $uploader, LoggerInterface $logger)
{
$entityManager = $this->getDoctrine()->getManager();
$oauthClient = $this->getOauthClient();
$data = $request->request->all();
$client = new BusinessClient();
$client->setName($data["name"]);
$client->setClient($oauthClient);
$file = $request->files->get('logo');
if (!is_null($file)) {
$fileName = $uploader->upload($file);
$client->setLogo($fileName);
}
$errors = $this->validate($client);
if (count($errors) > 0) {
$err = [];
/** #var ConstraintViolationInterface $error */
foreach ($errors as $error) {
$err[] = [
"message" => $error->getMessage(),
"value" => $error->getInvalidValue(),
"params" => $error->getParameters()
];
}
throw HttpException::badRequest($err);
}
$entityManager->persist($client);
$entityManager->flush();
$r = new Response();
$r->setContent($this->serialize($client));
$r->setStatusCode(201);
$r->headers->set('Content-type', 'application/json');
return $r;
}
/**
* Get data for a single business client
*
* #REST\Get("/clients/{id}", requirements={"id" = "\d+"})
* #param $id
* #return Response
* #SWG\Tag(name="business_clients")
* #SWG\Response(
* response="200",
* description="Get data for a single business client"
* )
*/
public function getClientAction($id)
{
$object = $this->getDoctrine()->getRepository(BusinessClient::class)
->find($id);
$j = new Response($this->serialize($object));
return $j;
}
}
When I try to set the logo as a file basename string the request will fail. with error that the file (basename) is not found. This makes sense in a way.
If otherwise I try to set not a string but a File with valid path to the newly uploaded file the request will succeed, but the field in the table will be replaced with a full system path. The same happens when I put a valid system path instead of a file.
<?php
// Controller
.....
// This works
if (!is_null($file)) {
$fileName = $uploader->upload($file);
$client->setLogo($this->getParameter("logo_upload_dir")."/$fileName");
}
Parameter for the upload dir:
parameters:
logo_upload_dir: '%kernel.project_dir%/web/uploads/logos'
I'm not using any forms as this is a third party API and I'm mainly using the request objects directly to handle the data. Most of the documentations used Forms to handle this. Also all my responses are in JSON.
I'd appreciate any help on this. Otherwise I'll have to store the full path and that in not a good idea and very impractical.
Thanks in advance.
Here is a thought on this: Instead of validating the property which your plan to be a relative path to an image, validate the method. Something like this maybe:
class BusinessClient extends BaseEntity
{
public static $basePath;
// ....
/**
* Get the logo
*
* #Constraints\Image(minWidth=100, minHeight=100)
*/
public function getAbsolutePathLogo()
{
return self::$basePath . '/' . $this->logo;
}
So, remove the validation from your logo member, add a new method (I named it getAbsolutePathLogo buy you can choose anything) and set up validation on top of it.
This way, your logo will be persisted as relative path and validation should work. However, the challenge now is to determine the right moment to set that static $basePath. In reality, this one does not even need to be a class static member, but could be something global:
return MyGlobalPath::IMAGE_PATH . '/' . $this->logo;
Does this make sense?
Hope it helps a bit...
Based on: http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html
I made an Images Model:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Filesystem\Filesystem;
/**
* #ORM\Entity
* #ORM\Table(name="images")
* #ORM\Entity(repositoryClass="AppBundle\Entity\ImagesRepository")
* #ORM\HasLifecycleCallbacks
*/
class Images
{
/**
* #ORM\Column(type="string", length=60)
* #ORM\Id
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="AppBundle\Doctrine\AutoIdGenerate")
*/
private $id;
/**
* Filename of the Image
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* Filename of the Thumbnail
* #ORM\Column(type="string", length=100)
*/
private $name_small;
/**
* ImageGroup og the Image
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ImageGroups", inversedBy="images")
*/
private $group;
/**
* #Assert\File(maxSize="20000000")
*/
private $file;
private $upload_dir='images';
private $temp;
/**
* Get file.
*
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Sets file.
*
* #param UploadedFile $file
*/
public function setFile(UploadedFile $file,$upload_dir)
{
$this->file = $file;
// check if we have an old image path
if (isset($this->name))
{
// store the old name to delete after the update
$this->temp = $this->name;
$this->name = null;
}
else
{
$this->name = sha1(uniqid(mt_rand(), true)).'.'.$file->guessExtension();
}
$this->name_small="small_".$this->name;
$this->upload_dir=$upload_dir;
return $this;
}
/**
* #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->name = $filename.'.'.$this->getFile()->guessExtension();
$this->name_small='small_'.$this->name;
}
}
public function getUploadRootDir()
{
return __DIR__.'/../../../../web/'.$this->upload_dir;
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->getFile())
{
return;
}
// if there is an error when moving the file, an exception will
// be automatically thrown by move(). This will properly prevent
// the entity from being persisted to the database on error
//-- Make the Thumbnail Here --
$dir=$this->getUploadRootDir();
echo $dir;
$fs = new Filesystem();
if(!$fs->exists($dir))
{
echo "\nCreating\n";
$fs->mkdir($dir,0777,true);
}
$this->getFile()->move($dir, $this->name);
$file=$dir.'/'.$this->name;
// check if we have an old image
if (isset($this->temp))
{
// delete the old image
unlink($this->getUploadRootDir().'/'.$this->temp);
// clear the temp image path
$this->temp = null;
}
$this->file = null;
}
/**
* #ORM\PostRemove()
*/
public function removeUpload()
{
$file = $this->getAbsolutePath();
if ($file)
{
unlink($file);
}
}
/**
* Get id
*
* #return string
*/
public function getId()
{
return $this->id;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Get nameSmall
*
* #return string
*/
public function getNameSmall()
{
return $this->name_small;
}
/**
* Set group
*
* #param \AppBundle\Entity\ImageGroups $group
*
* #return Images
*/
public function setGroup(\AppBundle\Entity\ImageGroups $group = null)
{
$this->group = $group;
return $this;
}
/**
* Get group
*
* #return \AppBundle\Entity\ImageGroups
*/
public function getGroup()
{
return $this->group;
}
/**
* Set name
*
* #param string $name
*
* #return Images
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Set nameSmall
*
* #param string $nameSmall
*
* #return Images
*/
public function setNameSmall($nameSmall)
{
$this->name_small = $nameSmall;
return $this;
}
}
I also made a Custom Repository In order to do the Uploads:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use AppBundle\Entity\Images;
class ImagesRepository extends EntityRepository
{
public function add($file,$group_id,$user_id)
{
if(empty($group_id)) return -1;
if(empty($user_id)) return -2;
/*Do stuff for uploads*/
/*End of Do stuff for uploads*/
$em = $this->getEntityManager();
$imagesGroups = $em->getRepository('AppBundle:ImageGroups')
->getUserImageGroups($user_id,null,$group_id);
if(empty($imagesGroups) ||(is_int($imagesGroups) && $imagesGroups<0)) return -3; //Null and negative values are false
if(empty($file)) return -4;
$image=new Images();
$image->setFile($file,'images')->setGroup($imagesGroups);
try
{
$em->persist($image);
$em->flush();
return ['id'=>$image->getId(),'image'=>$image->getName(),'thumb'=>$image->getNameSmall()];
}
catch (\Exception $e)
{
echo $e->getMessage();
return false;
}
}
public function delete($user_id,$image_id)
{
if(empty($image_id)) return -1;
if(empty($user_id)) return -2;
$em = $this->getEntityManager();
try
{
$q=$em->createQueryBuilder('i')
->from('AppBundle:Images','i')
->innerJoin('i.group', 'g')
->innerJoin('AppBundle:Users','u')
->select('i')
->where('i.id=:iid')
->andWhere('u.id=:uid')
->setParameter(':uid', $user_id)
->setParameter(':iid', $image_id)
->setMaxResults(1)
->getQuery();
$data=$q->getOneOrNullResult();
if(empty($data)) return -3;
/*Delete Image Stuff*/
/*End Of: Delete Image Stuff*/
$em->remove($data);
$em->flush();
return true;
}
catch (\Exception $e)
{
echo $e->getMessage();
return false;
}
}
}
When I sucessfully do the POST action (I Use curl to test the code above) for some reason I get the following Error:
The file "IMG_20160305_155302.jpg" was not uploaded due to an unknown error.
By echoing the Exception message.
I Originally thought that is a permissions Issues on my filesystem therefore on the Folder /home/pcmagas/Kwdikas/php/apps/symphotest/src/AppBundle/Entity/../../../../web/images
I set the following permissions:
drwxrwxrwx 2 www-data www-data 4096 Μάρ 5 23:02 images
But it does not seem that is the problem. And I wonder what else could it be.
May I have a solution?
Edit 1:
I checked the upload_max_filesize on php.ini and is on 2M and the file that I am Uploading is on 56,0 Kbytes.
The UploadedFile Instance somehow was not setup corectly in controller.
In order to get the file Input I made this class:
<?php
namespace AppBundle\Helpers;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class MyUploadedFile
{
/**
*Storing Uploaded Files
*/
private $files=null;
function __construct($string)
{
if(isset($_FILES[$string]))
{
$files=$_FILES[$string];
if(is_array($files['name']))
{
$this->files=array();
$tmp_files=array();
/**
*Sanitizing When Multiple Files
*/
foreach($files as $key=>$val)
{
foreach($val as $key2=>$val2)
{
$tmp_files[$key2][$key]=$val2;
}
}
foreach($tmp_files as $val)
{
$this->files[]=new UploadedFile($val['tmp_name'],$val['name'],$val['type'],$val['size'],$val['error']);
}
}
elseif(is_string($files['name']))
{
$this->files= new UploadedFile($files['tmp_name'],$files['name'],$files['type'],$files['size'],$files['error']);
}
}
}
/**
*#return {UploadedFile} Or {Array of UploadedFile} or null
*/
public function getFiles()
{
return $this->files;
}
}
?>
Especially in these lines
$this->files= new UploadedFile($files['tmp_name'],$files['name'],$files['type'],$files['size'],$files['error']);
$this->files[]=new UploadedFile($val['tmp_name'],$val['name'],$val['type'],$val['size'],$val['error']);
So when ytou manually make an uploaded file the size comes before the error
I am not writing "what did I try" or "what is not working" since I can think of many ways to implement something like this. But I cannot believe that no one did something similar before and that is why I would like to ask the question to see what kind of Doctrine2 best practices show up.
What I want is to trigger an event on a property change. So let's say I have an entity with an $active property and I want a EntityBecameActive event to fire for each entity when the property changes from false to true.
Other libraries often have a PropertyChanged event but there is no such thing available in Doctrine2.
So I have some entity like this:
<?php
namespace Application\Entity;
class Entity
{
/**
* #var int
* #ORM\Id
* #ORM\Column(type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var boolean
* #ORM\Column(type="boolean", nullable=false)
*/
protected $active = false;
/**
* Get active.
*
* #return string
*/
public function getActive()
{
return $this->active;
}
/**
* Is active.
*
* #return string
*/
public function isActive()
{
return $this->active;
}
/**
* Set active.
*
* #param bool $active
* #return self
*/
public function setActive($active)
{
$this->active = $active;
return $this;
}
}
Maybe ChangeTracking Policy is what you want, maybe it is not!
The NOTIFY policy is based on the assumption that the entities notify
interested listeners of changes to their properties. For that purpose,
a class that wants to use this policy needs to implement the
NotifyPropertyChanged interface from the Doctrine\Common namespace.
Check full example in link above.
class MyEntity extends DomainObject
{
private $data;
// ... other fields as usual
public function setData($data) {
if ($data != $this->data) { // check: is it actually modified?
$this->onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
}
UPDATE
This is a full example but silly one so you can work on it as you wish. It just demonstrates how you do it, so don't take it too serious!
entity
namespace Football\TeamBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="country")
*/
class Country extends DomainObject
{
/**
* #var int
*
* #ORM\Id
* #ORM\Column(type="smallint")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(type="string", length=2, unique=true)
*/
protected $code;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set code
*
* #param string $code
* #return Country
*/
public function setCode($code)
{
if ($code != $this->code) {
$this->onPropertyChanged('code', $this->code, $code);
$this->code = $code;
}
return $this;
}
/**
* Get code
*
* #return string
*/
public function getCode()
{
return $this->code;
}
}
domainobject
namespace Football\TeamBundle\Entity;
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\PropertyChangedListener;
abstract class DomainObject implements NotifyPropertyChanged
{
private $listeners = array();
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->listeners[] = $listener;
}
protected function onPropertyChanged($propName, $oldValue, $newValue)
{
$filename = '../src/Football/TeamBundle/Entity/log.txt';
$content = file_get_contents($filename);
if ($this->listeners) {
foreach ($this->listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
file_put_contents($filename, $content . "\n" . time());
}
}
}
}
controller
namespace Football\TeamBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Football\TeamBundle\Entity\Country;
class DefaultController extends Controller
{
public function indexAction()
{
// First run this to create or just manually punt in DB
$this->createAction('AB');
// Run this to update it
$this->updateAction('AB');
return $this->render('FootballTeamBundle:Default:index.html.twig', array('name' => 'inanzzz'));
}
public function createAction($code)
{
$em = $this->getDoctrine()->getManager();
$country = new Country();
$country->setCode($code);
$em->persist($country);
$em->flush();
}
public function updateAction($code)
{
$repo = $this->getDoctrine()->getRepository('FootballTeamBundle:Country');
$country = $repo->findOneBy(array('code' => $code));
$country->setCode('BB');
$em = $this->getDoctrine()->getManager();
$em->flush();
}
}
And have this file with 777 permissions (again, this is test) to it: src/Football/TeamBundle/Entity/log.txt
When you run the code, your log file will have timestamp stored in it, just for demonstration purposes.
I'm creating a ticket system for practice purposes.
Right now I'm having a problem with uploading files.
The idea is that a ticket can have multiple attachments, so I created a many-to-one relationship between the ticket and the upload tables.
class Ticket {
// snip
/**
* #ORM\OneToMany(targetEntity="Ticket", mappedBy="ticket")
*/
protected $uploads;
// snip
}
The upload entity class contains the upload functionality, which I took from this tutorial:
<?php
namespace Sytzeandreae\TicketsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* #ORM\Entity(repositoryClass="Sytzeandreae\TicketsBundle\Repository\UploadRepository")
* #ORM\Table(name="upload")
* #ORM\HasLifecycleCallbacks
*/
class Upload
{
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
private $temp;
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Ticket", inversedBy="upload")
* #ORM\JoinColumn(name="ticket_id", referencedColumnName="id")
*/
protected $ticket;
/**
* #ORM\Column(type="string")
*/
protected $title;
/**
* #ORM\Column(type="string")
*/
protected $src;
public function getAbsolutePath()
{
return null === $this->src
? null
: $this->getUploadRootDir().'/'.$this->src;
}
public function getWebPath()
{
return null === $this->src
? null
: $this->getUploadDir().'/'.$this->src;
}
public function getUploadRootDir()
{
// The absolute directory path where uplaoded documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
public function getUploadDir()
{
// Get rid of the __DIR__ so it doesn/t screw up when displaying uploaded doc/img in the view
return 'uploads/documents';
}
/**
* Sets file
*
* #param UploadedFile $file
*/
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
if (isset($this->path)) {
// store the old name to delete after the update
$this->temp = $this->path;
$this->path = null;
} else {
$this->path = 'initial';
}
}
/**
* Get file
*
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set src
*
* #param string $src
*/
public function setSrc($src)
{
$this->src = $src;
}
/**
* Get src
*
* #return string
*/
public function getSrc()
{
return $this->src;
}
/**
* Set ticket
*
* #param Sytzeandreae\TicketsBundle\Entity\Ticket $ticket
*/
public function setTicket(\Sytzeandreae\TicketsBundle\Entity\Ticket $ticket)
{
$this->ticket = $ticket;
}
/**
* Get ticket
*
* #return Sytzeandreae\TicketsBundle\Entity\Ticket
*/
public function getTicket()
{
return $this->ticket;
}
/**
* #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->src = $filename.'.'.$this->getFile()->guessExtension();
}
}
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->getFile()) {
return;
}
// move takes the target directory and then the target filename to move to
$this->getFile()->move(
$this->getUploadRootDir(),
$this->src
);
// check if we have an old image
if (isset($this->temp)) {
// delete the old image
unlink($this->getUploadRootDir().'/'.$this->temp);
// clear the temp image path
$this->temp = null;
}
// clean up the file property as you won't need it anymore
$this->file = null;
}
/**
* #ORM\PostRemove
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
}
The form is build as follows:
class TicketType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('priority')
->add('uploads', new UploadType())
}
Where UploadType looks like this:
class UploadType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options) {
$builder->add('file', 'file', array(
'label' => 'Attachments',
'required' => FALSE,
'attr' => array (
'accept' => 'image/*',
'multiple' => 'multiple'
)
));
}
This part seems to work fine, I do get presented a form which contains a file uploader.
Once I put this line in the Ticket entity's constuctor:
$this->uploads = new \Doctrine\Common\Collections\ArrayCollection();
It throws me the following error:
Neither property "file" nor method "getFile()" nor method "isFile()"
exists in class "Doctrine\Common\Collections\ArrayCollection"
If I leave this line out, and upload a file, it throws me the following error:
"Sytzeandreae\TicketsBundle\Entity\Ticket". Maybe you should create
the method "setUploads()"?
So next thing I did was creating this method, try an upload again and now it throws me:
Class Symfony\Component\HttpFoundation\File\UploadedFile is not a
valid entity or mapped super class.
This is where I am really stuck. I fail to see what, at what stage, I did wrong and am hoping for some help :)
Thanks!
You can either add a collection field-type of UploadType() to your form. Notice that you can't use the multiple option with your current Upload entity ... but it's the quickest solution.
Or adapt your Ticket entity to be able to handle multiple files in an ArrayCollection and looping over them.
I have followed Multiple file upload with Symfony2 and created a entity
/*
* #ORM\HasLifecycleCallbacks
* #ORM\Entity(repositoryClass="Repair\StoreBundle\Entity\attachmentsRepository")
*/
class attachments
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #Assert\File(maxSize="6000000")
* #ORM\Column(name="files", type="array", length=255, nullable=true)
*/
private $files=array();
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set files
* #param object $files
*
* #return attachments
*/
public function setFiles($files) {
$this->files = $files;
}
/**
* Get files
*
* #return object
*/
public function getFiles() {
return $this->files;
}
public function __construct() {
$files = array();
}
public function uploadFiles() {
// the files property can be empty if the field is not required
if (null === $this->files) {
return;
}
if (!$this->id) {
$this->files->move($this->getTmpUploadRootDir(), $this->files->getClientOriginalName());
} else {
$this->files->move($this->getUploadRootDir(), $this->files->getClientOriginalName());
}
$this->setFiles($this->files->getClientOriginalName());
}
public function getAbsolutePath() {
return null === $this->path
? null
: $this->getUploadRootDir() . DIRECTORY_SEPARATOR . $this->path;
}
public function getWebPath() {
return null === $this->path
? null
: $this->getUploadDir() . DIRECTORY_SEPARATOR . $this->path;
}
protected function getUploadRootDir() {
return __DIR__ . '/../../../../web/'. $this->getUploadDir();
}
protected function getUploadDir() {
return 'uploads/';
}
}
I have a controller which has the folowing code
class uploadController extends Controller
{
public function uploadAction(Request $request) {
$id= $_GET['id'];
$user = new attachments();
$form = $this->createFormBuilder($user)->add('files','file',array("attr"=>array("multiple" =>"multiple",)))->getForm();
$formView = $form->createView();
$formView->getChild('files')->set('full_name','form[file][]');
if ($request->getMethod() == 'POST') {
$em = $this->getDoctrine()->getManager();
$form->bind($request);
$files = $form["files"]->getFilenames();
$user->uploadFiles(); //--here iam not able to get te file names in order to send to the upload function.upload files is returning null values
}
}
}
the controller is not able to get the filenames that is uploded by the uder from the view.it is returning null values when i send to the upload function in entity
I think you should read the Documentation about File Upload again. You are correctly using the annotation #ORM\HasLifecycleCallbacks but your code is missing the right annotations for the methods. You can use the #ORM\PostPersist() and #ORM\PostUpdate() annotations to call the upload() function automatically after the request was validated and the object was persisted. Also I would suggest using an entity for each file and creating a OneToMany relation. This will make it more easy and logical to store your files correctly.