I'm trying to save a collection of files.
When I save a collection, it goes into the DB without a hitch. Same with files.
But when I add a collection and a new file in the same request (like here in the upload function).
Whenever I then ask doctrine to get me the files in the collection (in this case the one new file).
It always responds with an empty ArrayCollection.
If I do a seperate get HTTP request and ask for the collection afterwards, it shows me the correct arrayCollection containing my one new file.
I have tried every way of persisting and flushing the entities, as well as changing cascade annotations, so far nothing has worked.
I have tried clearing the doctrine cache as well.
The Annotations seem to be correct because calling a getCollection()->getFiles() results in an ArrayCollection containing the files linked to the collection. It just doesn't seem to work right after creating both entities and linking them together.
Thank you very much for your help, code is down below.
This is my collection. Which contains files as an ArrayCollection.
/**
* #Entity #Table(name="LH_FileCollections")
**/
class LhFileCollection extends RootModel
{
/**
* #Column(type="string")
*/
protected $title;
/**
* #OneToMany(targetEntity="LhFile", mappedBy="collection")
*/
protected $files;
//Getters and Setters
}
This is my File class.
/**
* #Entity #Table(name="LH_Files")
**/
class LhFile extends RootModel
{
/**
* #Column(type="string")
*/
protected $name;
/**
* #Column(type="string")
*/
protected $type;
/**
* #Column(name="file_hash", type="string")
*/
protected $fileHash;
/**
* #ManyToOne(targetEntity="LhFileCollection", inversedBy="files", cascade={"persist"})
* #JoinColumn(name="collection_id", referencedColumnName="id")
*/
protected $collection;
//Getters and Setters
}
This is my save filecollection function.
/**
* #return array|string
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
* #throws \Exception
*/
public function fileUpload(
$title,
$attachment = null,
$allowedFileTypes = null,
$maxAllowedFileSize = 5000000
) {
//Create collection
$collection = $this->fileCollectionRepository->add($title);
foreach ($_FILES as $file) {
if ($allowedFileTypes !== null) {
$errors = $this->fileCheck($file, $allowedFileTypes, $maxAllowedFileSize);
if (!empty($errors)) {
return $errors;
}
}
$this->saveFile($file, $collection);
}
return $collection;
}
This is my save file function.
/**
* #param $file
* #param $collection
* #return LhFile
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
private function saveFile($file, $collection)
{
$currentDir = getcwd();
$uploadDir = $currentDir . '/Data/Uploads/';
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$uniqueName = uniqid() . "_" . time() . '.' . $extension;
move_uploaded_file($file['tmp_name'], $uploadDir . $uniqueName);
$metaData = "...";
$file = $this->fileRepository->add(
$file['name'],
$file['size'],
$extension,
$metaData,
$uniqueName,
$collection
);
return $file;
}
And lastly the repository functions:
Filecollection:
/**
* #param $fileCollection
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function add($title)
{
$fileCollection = new LhFileCollection();
$fileCollection->setTitle($title);
$this->em->persist($fileCollection);
$this->em->flush();
return $fileCollection;
}
And File
/**
* #param $file
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function add($name, $size, $type, $metaData, $uniqueName, $collection)
{
$file = new LhFile();
$file->setName($name);
$file->setSize($size);
$file->setType($type);
$file->setMetadata($metaData);
$file->setFileHash($uniqueName);
$file->setCollection($collection);
$this->em->persist($file);
$this->em->flush();
return $file;
}
It seems to me that in the current request from what I can make out that files are not being added to the collection->files array. The database is receiving the correct relationships which is why on the second request it is fine but the act of saving to the database doesn't auto populate the relationships.
I think you need to explicitly add the file to the collection->files, assuming you have getFiles in LhFileCollection you could add after $this->saveFile($file, $collection);:
$collection->getFiles()->add($file);
There are of course a number of ways it can be done but ultimately you need to add the files to the collection->files.
Personally, I would build up the collection adding each file to the files array and only then save the collection. I wouldn't persist and flush on every file because database actions could be expensive. You have cascade on so it should cascade to all files.
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...
I'm in a situation that need to update a Doctrine2 Entity and exclude some fields.
With ZF2 i have an action to handle update using Zend\Form and validation filter. In particular Dish Entity have a blob column called photo that is required. During an update i want to replace the photo only if a new file is provided.
Here there are the source code for the entity and the controller action that update dish.
Dishes\Entity\Dish.php
<?php
namespace Dishes\Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Entity **/
class Dish
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
**/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\Column(type="text")
*/
protected $description;
/**
* #ORM\Column(type="integer")
*/
protected $time;
/**
* #ORM\Column(type="integer")
*/
protected $complexity;
/**
* #ORM\Column(type="blob")
*/
protected $photo;
/**
* Magic getter to expose protected properties.
*
* #param string $property
* #return mixed
*/
public function __get($property)
{
return $this->$property;
}
/**
* Magic setter to save protected properties.
*
* #param string $property
* #param mixed $value
*/
public function __set($property, $value)
{
$this->$property = $value;
}
}
Dishes\Controller\AdminController.php
public function editDishAction()
{
//Params from url
$id = (int) $this->params()->fromRoute('id', 0);
$objectManager = $this->objectManager;
$hydrator = new DoctrineObject($objectManager, false);
$form = new DishForm();
$existingDish = $objectManager->find('Dishes\Entity\Dish', $id);
if ($existingDish === NULL)
$this->notFoundAction();
$request = $this->getRequest();
if ($request->isPost())
{
$filter = new DishFilter();
$filter->get('photo')->setRequired(false);
$form->setHydrator($hydrator)
->setObject($existingDish)
->setInputFilter($filter);
$post = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
//Backup photo stream
$imageData = $existingDish->photo;
$form->setData($post);
if ($form->isValid())
{
//If user upload a new image read it.
if(!empty($existingDish->photo['tmp_name']))
$imageData = file_get_contents($existingDish->photo['tmp_name']);
$existingDish->photo = $imageData;
$objectManager->flush();
$this->redirect()->toRoute('zfcadmin/dishes');
}
}
else
{
$data = $hydrator->extract($existingDish);
unset($data['photo']);
$form->setData($data);
}
return new ViewModel(array('form' => $form));
}
Actually i set $dish->photo property to NULL but this violate DB NOT NULL constraint.
How can I tell Doctrine to exclude a particular entity field from update at runtime?
Doctrine maps every column's nullable property in database level to false by default since you don't set any nullable flag in your annotation:
/**
* #ORM\Column(type="blob")
*/
protected $photo;
This means, "Photo is required, you can't insert or update row's photo column with a null value".
If you want to have null values in your database, use the following annotation:
/**
* #ORM\Column(type="blob", nullable=true)
*/
protected $photo;
and in it's setter don't forget the null default argument value:
public function setPhoto($photo = null)
{
$this->photo = $photo;
}
For the question; seems like you're setting a new Dish object on every edit operation in the action:
$form->setHydrator($hydrator)
->setObject(new Dish)
->setInputFilter($filter);
This is correct when creating new Dish objects. When editing, you have to set an already persisted Dish instance to the form:
// I'm just writing to explain the flow.
// Accessing the object repository in action level is not a good practice.
// Use a DishService for example.
$id = 32; // Grab it from route or elsewhere
$repo = $entityManager->getRepository('Dishes\Entity\Dish');
$existingDish = $repo->findOne((int) $id);
$form->setHydrator($hydrator)
->setObject($existingDish)
->setInputFilter($filter);
I'm assuming this is edit action for an existing Dish.
So, the hydrator will correctly handle both changed and untouched fields on next call since you give an already populated Dish instance via the form.
I also recommend fetching the DishFilter from the InputFilterManager instead of creating it manually in action level:
// $filter = new DishFilter();
$filter = $serviceLocator->get('InputFilterManager')->get(DishFilter::class);
// Exclude the photo on validation:
$filter->setValidationGroup('name', 'description', 'time', 'complexity');
Hope it helps.
I'm struggling with fixing this class exception error. If anyone could help it would be greatly appreciated, Thanks!
BACKGROUND INFO:
After installing a Dashnex plugin on my wordpress site & then uninstalling WP Quick cache I am getting this error message. Please note that quick cache is fully uninstalled & the wp-config.php file includes no quick cache instructions.
ERROR MESSAGE:
Fatal error: Uncaught exception 'ReflectionException' with message 'Class \quick_cache does not exist' in /home/cal108/public_html/wp-content/plugins/dashnex-plugin/vendor/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/AnnotationDriver.php:246 Stack trace: #0 /home/cal108/public_html/wp-content/plugins/dashnex-plugin/vendor/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/AnnotationDriver.php(246): ReflectionClass->__construct('\quick_cache') #1 /home/cal108/public_html/wp-content/plugins/dashnex-plugin/vendor/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php(113): Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver->getAllClassNames() #2 /home/cal108/public_html/wp-content/plugins/dashnex-plugin/DashNex/Doctrine/MagicSchema.php(18): Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory->getAllMetadata() #3 /home/cal108/public_html/wp-content/plugins/dashnex-plugin/DashNex/Doctrine/MagicSchema.php(37): DashNex\Doctrine\MagicSchema->Get in /home/cal108/public_html/wp-content/plugins/dashnex-plugin/vendor/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/AnnotationDriver.php on line 246
AnnotationDriver.php Line 246
$rc = new \ReflectionClass($className);
AnnotationDriver.php File
<?php
abstract class AnnotationDriver implements MappingDriver
{
/**
* The AnnotationReader.
*
* #var AnnotationReader
*/
protected $reader;
/**
* The paths where to look for mapping files.
*
* #var array
*/
protected $paths = array();
/**
* The paths excluded from path where to look for mapping files.
*
* #var array
*/
protected $excludePaths = array();
/**
* The file extension of mapping documents.
*
* #var string
*/
protected $fileExtension = '.php';
/**
* Cache for AnnotationDriver#getAllClassNames().
*
* #var array|null
*/
protected $classNames;
/**
* Name of the entity annotations as keys.
*
* #var array
*/
protected $entityAnnotationClasses = array();
/**
* Initializes a new AnnotationDriver that uses the given AnnotationReader for reading
* docblock annotations.
*
* #param AnnotationReader $reader The AnnotationReader to use, duck-typed.
* #param string|array|null $paths One or multiple paths where mapping classes can be found.
*/
public function __construct($reader, $paths = null)
{
$this->reader = $reader;
if ($paths) {
$this->addPaths((array) $paths);
}
}
/**
* Appends lookup paths to metadata driver.
*
* #param array $paths
*
* #return void
*/
public function addPaths(array $paths)
{
$this->paths = array_unique(array_merge($this->paths, $paths));
}
/**
* Retrieves the defined metadata lookup paths.
*
* #return array
*/
public function getPaths()
{
return $this->paths;
}
/**
* Append exclude lookup paths to metadata driver.
*
* #param array $paths
*/
public function addExcludePaths(array $paths)
{
$this->excludePaths = array_unique(array_merge($this->excludePaths, $paths));
}
/**
* Retrieve the defined metadata lookup exclude paths.
*
* #return array
*/
public function getExcludePaths()
{
return $this->excludePaths;
}
/**
* Retrieve the current annotation reader
*
* #return AnnotationReader
*/
public function getReader()
{
return $this->reader;
}
/**
* Gets the file extension used to look for mapping files under.
*
* #return string
*/
public function getFileExtension()
{
return $this->fileExtension;
}
/**
* Sets the file extension used to look for mapping files under.
*
* #param string $fileExtension The file extension to set.
*
* #return void
*/
public function setFileExtension($fileExtension)
{
$this->fileExtension = $fileExtension;
}
/**
* Returns whether the class with the specified name is transient. Only non-transient
* classes, that is entities and mapped superclasses, should have their metadata loaded.
*
* A class is non-transient if it is annotated with an annotation
* from the {#see AnnotationDriver::entityAnnotationClasses}.
*
* #param string $className
*
* #return boolean
*/
public function isTransient($className)
{
$classAnnotations = $this->reader->getClassAnnotations(new \ReflectionClass($className));
foreach ($classAnnotations as $annot) {
if (isset($this->entityAnnotationClasses[get_class($annot)])) {
return false;
}
}
return true;
}
/**
* {#inheritDoc}
*/
public function getAllClassNames()
{
if ($this->classNames !== null) {
return $this->classNames;
}
if (!$this->paths) {
throw MappingException::pathRequired();
}
$classes = array();
$includedFiles = array();
foreach ($this->paths as $path) {
if ( ! is_dir($path)) {
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
}
$iterator = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
),
'/^.+' . preg_quote($this->fileExtension) . '$/i',
\RecursiveRegexIterator::GET_MATCH
);
foreach ($iterator as $file) {
$sourceFile = $file[0];
if ( ! preg_match('(^phar:)i', $sourceFile)) {
$sourceFile = realpath($sourceFile);
}
foreach ($this->excludePaths as $excludePath) {
$exclude = str_replace('\\', '/', realpath($excludePath));
$current = str_replace('\\', '/', $sourceFile);
if (strpos($current, $exclude) !== false) {
continue 2;
}
}
require_once $sourceFile;
$includedFiles[] = $sourceFile;
}
}
$declared = get_declared_classes();
foreach ($declared as $className) {
$rc = new \ReflectionClass($className);
$sourceFile = $rc->getFileName();
if (in_array($sourceFile, $includedFiles) && ! $this->isTransient($className)) {
$classes[] = $className;
}
}
$this->classNames = $classes;
return $classes;
}
}
I WAS ABLE TO SOLVE MY PROBLEM. HELP NOT NEEDED ANYMORE :)
FOR ANYONE THIS MAY STILL HELP:
I DISABLED ALL OF MY PLUGINS BY RENAMING THEM IT IN CPANEL, THEN BROUGHT EACH ONE ONLINE AGAIN TO KNOW IF THE DASHNEX PLUGIN WAS THE CULPRIT. IT WAS. THEN RENAMED ALL PLUGINS AGAIN & ALL WORKING FINE.
I DON'T THINK THE DASHNEX PLUGIN LIKED MY UNINSTALLING WP QUICK CACHE. BUT REINSTALLING IT AFTER WP QUCIK CACHE HAD ALREADY BEEN UNINSTALLED WORKED FINE. THIS IS THE 2ND TIME I'VE HAD PROBLEMS WHEN QUICK CACHE HAS BEEN UNINSTALLED SO BE WARY OF IT. MAYBE BEST TO JUST DEACTIVATE IT & ONLY UNINSTALL IT ON A BRAND NEW SITE BEFORE INSTALLING ANY OTHER PLUGINS.
DIDN'T END UP NEEDING TO TWEAK THE PHP CODE.
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.
I've made a simple symfony2 console script which is supposed to convert data from old model to the new one.
Here's what it looks like:
class ConvertScreenshotsCommand extends Command
{
[...]
protected function execute(InputInterface $input, OutputInterface $output)
{
$em = $this->getContainer()->get('doctrine')->getManager();
$output->writeln('<info>Conversion started on ' . date(DATE_RSS) . "</info>");
$output->writeln('Getting all reviews...');
$reviews = $em->getRepository('ACCommonBundle:Review')->findAll(); // Putting all Review entities into an array
$output->writeln('<info>Got ' . count($reviews) . ' reviews.</info>');
foreach ($reviews as $review) {
$output->writeln("<info>Screenshots for " . $review->getTitle() . "</info>");
if ($review->getLegacyScreenshots()) {
foreach ($review->getLegacyScreenshots() as $filename) { // fn returns array of strings
$output->writeln("Found " . $filename);
$screenshot = new ReviewScreenshot(); // new object
$screenshot->setReview($review); // review is object
$screenshot->setFilename($filename); // filename is string
$em->persist($screenshot);
$em->flush(); // this is where it dies
$output->writeln("Successfully added to the database.");
}
} else $output->writeln("No legacy screenshots found.");
}
$output->writeln('<info>Conversion ended on ' . date(DATE_RSS) . "</info>");
}
}
The script breaks on $em->flush(), with the following error:
[ErrorException]
Warning: spl_object_hash() expects parameter 1 to be object, string given in
/[...]/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 1324
Obviously I'm doing something wrong, but can't figure out what it is. Thanks in advance!
** Update **
Review Entity mapping:
class Review
{
[...]
/**
* #ORM\OneToMany(targetEntity="ReviewScreenshot", mappedBy="review")
*/
protected $screenshots;
/**
* Won't be stored in the DB
* #deprecated
*/
private $legacyScreenshots;
/**
* New method to get screenshots, currently calls old method for the sake of compatibility
* #return array Screenshot paths
*/
public function getScreenshots()
{
// return $this->getLegacyScreenshots(); // Old method
return $this->screenshots; // New method
}
/**
* Get Screenshot paths
* #return array Screenshot paths
* #deprecated
*/
public function getLegacyScreenshots()
{
$dir=$this->getUploadRootDir();
if (file_exists($dir)) {
$fileList = scandir($dir);
$this->screenshots = array();
foreach ($fileList as $fileName)
{
preg_match("/(screenshot-\d+.*)/", $fileName, $matches);
if ($matches)
$this->screenshots[]=$matches[1];
}
return $this->screenshots;
}
else return null;
}
ReviewScreenshot mapping:
class ReviewScreenshot
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $filename
*
* #ORM\Column(name="filename", type="string", length=255)
*/
private $filename;
/**
* #ORM\ManyToOne(targetEntity="Review", inversedBy="screenshots")
* #ORM\JoinColumn(name="review_id", referencedColumnName="id")
*/
protected $review;
/**
* #var integer $priority
*
* #ORM\Column(name="priority", type="integer", nullable=true)
*/
protected $priority;
/**
* #var string $description
*
* #ORM\Column(name="description", type="string", nullable=true)
*/
protected $description;
/**
* #Assert\File(maxSize="2097152")
*/
public $screenshot_file;
protected $webPath;
UnitOfWork.php
/**
* Gets the state of an entity with regard to the current unit of work.
*
* #param object $entity
* #param integer $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
* This parameter can be set to improve performance of entity state detection
* by potentially avoiding a database lookup if the distinction between NEW and DETACHED
* is either known or does not matter for the caller of the method.
* #return int The entity state.
*/
public function getEntityState($entity, $assume = null)
{
$oid = spl_object_hash($entity); // <-- Line 1324
if (isset($this->entityStates[$oid])) {
return $this->entityStates[$oid];
}
if ($assume !== null) {
return $assume;
}
// State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
// Note that you can not remember the NEW or DETACHED state in _entityStates since
// the UoW does not hold references to such objects and the object hash can be reused.
// More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
$class = $this->em->getClassMetadata(get_class($entity));
$id = $class->getIdentifierValues($entity);
if ( ! $id) {
return self::STATE_NEW;
}
switch (true) {
case ($class->isIdentifierNatural());
// Check for a version field, if available, to avoid a db lookup.
if ($class->isVersioned) {
return ($class->getFieldValue($entity, $class->versionField))
? self::STATE_DETACHED
: self::STATE_NEW;
}
// Last try before db lookup: check the identity map.
if ($this->tryGetById($id, $class->rootEntityName)) {
return self::STATE_DETACHED;
}
// db lookup
if ($this->getEntityPersister($class->name)->exists($entity)) {
return self::STATE_DETACHED;
}
return self::STATE_NEW;
case ( ! $class->idGenerator->isPostInsertGenerator()):
// if we have a pre insert generator we can't be sure that having an id
// really means that the entity exists. We have to verify this through
// the last resort: a db lookup
// Last try before db lookup: check the identity map.
if ($this->tryGetById($id, $class->rootEntityName)) {
return self::STATE_DETACHED;
}
// db lookup
if ($this->getEntityPersister($class->name)->exists($entity)) {
return self::STATE_DETACHED;
}
return self::STATE_NEW;
default:
return self::STATE_DETACHED;
}
}
I think the problem lies within Review::$screenshots:
You map it as a OneToMany association, so the value should be a Collection of ReviewScreenshot entities. But the method Review::getLegacyScreenshots() will change it into an array of strings.
You're probably using the change-tracking policy DEFERRED_IMPLICIT (which is the default). So when the property Review::$screenshots changes, Doctrine will try to persist that change, encounters strings where it expects entities, so throws the exception.