Referring to Symfony 2's Cookbook about file upload, I tried to use Doctrine's @PostRemove event listener to remove a file after the file has been removed from database.
Document.php
/** @Entity */
class Document {
/** OneToOne(targetEntity="File", cascade={"all"}) */
private $file;
public function setFile(File $file) {
$this->file = $file;
}
public function getFile() {
return $this->file;
}
}
File.php
/*
** @Entity
** @HasLifecycleCallbacks
*/
class File extends {
/** @Column(type="string") */
private $name;
public function __construct(UploadedFile $file) {
$this->path = $file->getPathname();
$this->name = $file->getClientOriginalName();
}
public function getAbsolute() {
return '/var/www/cdn.myweb.com/file/'.$this->name;
}
/** @PostRemove */
public function removeFile() {
unlink($this->getAbsolute());
}
}
Database:
**Document**
--------
| id |
-------
| 1 |
--------
**DocumentFiles**
--------------------------
| document_id | file_id |
--------------------------
| 1 | 2 |
--------------------------
**File**
--------------------
| id | name |
--------------------
| 1 | file1.ext |
| 2 | file2.ext |
--------------------
When I remove a Document with Id 1, somehow doctrine unlink the File with Id 1 as well.
From what I can found out, this strange behavior occurs as a result of these steps :
1- Doctrine's UnitOfWork
will call out the commit()
method which in turn calls to executeDeletions()
2- In executeDeletions()
the persister deleted the Document's File according to it's id and then do
if ( ! $class->isIdentifierNatural()) {
$class->reflFields[$class->identifier[0]]->setValue($entity, null);
}
which sets the File's id value to null
, then it starts to call it's events
if ($invoke !== ListenersInvoker::INVOKE_NONE) {
$this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
}
3- Since $file property in Document model is a one-to-one relation, Doctrine automatically creates a File proxy class in place of the real File class as Document's property.
4- While invoking the removeFile()
function it calls to FileProxy's getAbsolutePath()
:
FileProxy.php
public function getAbsolute() {
$this->__initializer__ && $this->__initializer__invoke($this, 'getAbsolute', array());
return parent::getAbsolute();
}
Which invokes the initializer with closure:
function (BaseProxy $proxy) use ($entityPersister, $classMetadata) {
$initializer = $proxy->__getInitializer();
$cloner = $proxy->__getCloner();
$proxy->__setInitializer(null);
$proxy->__setCloner(null);
if ($proxy->__isInitialized()) {
return;
}
$properties = $proxy->__getLazyProperties();
foreach ($properties as $propertyName => $property) {
if (!isset($proxy->$propertyName)) {
$proxy->$propertyName = $properties[$propertyName];
}
}
$proxy->__setInitialized(true);
if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);
throw new EntityNotFoundException();
}
};
5- Calling the $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)
calls to EntityPersister's
public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null)
{
$sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
list($params, $types) = $this->expandParameters($criteria);
$stmt = $this->conn->executeQuery($sql, $params, $types);
if ($entity !== null) {
$hints[Query::HINT_REFRESH] = true;
$hints[Query::HINT_REFRESH_ENTITY] = $entity;
}
$hydrator = $this->em->newHydrator($this->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
$entities = $hydrator->hydrateAll($stmt, $this->rsm, $hints);
return $entities ? $entities[0] : null;
}
6- EntityPersister's load()
will then hydrate the FileProxy with the last item in it's query result, which is the File with Id 1, since according to the current transaction File with Id 2 is already deleted.
I got around this by using fetch=EAGER
in Document's $file mapping, but I'm curious about this issue.
Did I do something wrong, is this an expected behavior, or a bug perhaps?