|
@@ -2,13 +2,15 @@
|
|
|
|
|
|
namespace Gedmo\Loggable;
|
|
|
|
|
|
-use Gedmo\Loggable\AbstractLoggableListener,
|
|
|
- Doctrine\Common\Persistence\ObjectManager,
|
|
|
+use Doctrine\Common\Persistence\ObjectManager,
|
|
|
Doctrine\Common\Persistence\Mapping\ClassMetadata,
|
|
|
- Doctrine\ORM\Events,
|
|
|
+ Gedmo\Mapping\MappedEventSubscriber,
|
|
|
+ Gedmo\Mapping\Event\AdapterInterface,
|
|
|
Doctrine\Common\EventArgs;
|
|
|
|
|
|
/**
|
|
|
+ * Loggable listener
|
|
|
+ *
|
|
|
* @author Boussekeyt Jules <jules.boussekeyt@gmail.com>
|
|
|
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
|
|
|
* @package Gedmo.Loggable
|
|
@@ -16,111 +18,239 @@ use Gedmo\Loggable\AbstractLoggableListener,
|
|
|
* @link http://www.gediminasm.org
|
|
|
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
|
|
|
*/
|
|
|
-class LoggableListener extends AbstractLoggableListener
|
|
|
+class LoggableListener extends MappedEventSubscriber
|
|
|
{
|
|
|
/**
|
|
|
- * The default LogEntry class used to store the logs
|
|
|
+ * Create action
|
|
|
+ */
|
|
|
+ const ACTION_CREATE = 'create';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Update action
|
|
|
+ */
|
|
|
+ const ACTION_UPDATE = 'update';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Remove action
|
|
|
+ */
|
|
|
+ const ACTION_REMOVE = 'remove';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Username for identification
|
|
|
*
|
|
|
* @var string
|
|
|
*/
|
|
|
- protected $defaultLogEntryEntity = 'Gedmo\Loggable\Entity\LogEntry';
|
|
|
+ protected $username;
|
|
|
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * List of log entries which do not have the foreign
|
|
|
+ * key generated yet - MySQL case. These entries
|
|
|
+ * will be updated with new keys on postPersist event
|
|
|
+ *
|
|
|
+ * @var array
|
|
|
*/
|
|
|
- public function getSubscribedEvents()
|
|
|
- {
|
|
|
- return array(
|
|
|
- Events::onFlush,
|
|
|
- Events::loadClassMetadata,
|
|
|
- Events::postPersist
|
|
|
- );
|
|
|
- }
|
|
|
+ private $pendingLogEntryInserts = array();
|
|
|
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * For log of changed relations we use
|
|
|
+ * its identifiers to avoid storing serialized Proxies.
|
|
|
+ * These are pending relations in case it does not
|
|
|
+ * have an identifier yet
|
|
|
+ *
|
|
|
+ * @var array
|
|
|
*/
|
|
|
- protected function getLogEntryClass(array $config, $class)
|
|
|
- {
|
|
|
- return isset($this->configurations[$class]['logEntryClass']) ?
|
|
|
- $this->configurations[$class]['logEntryClass'] :
|
|
|
- $this->defaultLogEntryEntity;
|
|
|
- }
|
|
|
+ private $pendingRelatedObjects = array();
|
|
|
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * Set username for identification
|
|
|
+ *
|
|
|
+ * @param mixed $username
|
|
|
*/
|
|
|
- protected function getObjectManager(EventArgs $args)
|
|
|
+ public function setUsername($username)
|
|
|
{
|
|
|
- return $args->getEntityManager();
|
|
|
+ if (is_string($username)) {
|
|
|
+ $this->username = $username;
|
|
|
+ } elseif (is_object($username) && method_exists($username, 'getUsername')) {
|
|
|
+ $this->username = (string)$username->getUsername();
|
|
|
+ } else {
|
|
|
+ throw new \Gedmo\Exception\InvalidArgumentException("Username must be a string, or object should have method: getUsername");
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* {@inheritdoc}
|
|
|
*/
|
|
|
- protected function getScheduledObjectUpdates($uow)
|
|
|
+ public function getSubscribedEvents()
|
|
|
{
|
|
|
- return $uow->getScheduledEntityUpdates();
|
|
|
+ return array(
|
|
|
+ 'onFlush',
|
|
|
+ 'loadClassMetadata',
|
|
|
+ 'postPersist'
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * Get the LogEntry class
|
|
|
+ *
|
|
|
+ * @param AdapterInterface $ea
|
|
|
+ * @param string $class
|
|
|
+ * @return string
|
|
|
*/
|
|
|
- protected function getScheduledObjectInsertions($uow)
|
|
|
+ protected function getLogEntryClass(AdapterInterface $ea, $class)
|
|
|
{
|
|
|
- return $uow->getScheduledEntityInsertions();
|
|
|
+ return isset($this->configurations[$class]['logEntryClass']) ?
|
|
|
+ $this->configurations[$class]['logEntryClass'] :
|
|
|
+ $ea->getDefaultLogEntryClass();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * Mapps additional metadata
|
|
|
+ *
|
|
|
+ * @param EventArgs $eventArgs
|
|
|
+ * @return void
|
|
|
*/
|
|
|
- protected function getScheduledObjectDeletions($uow)
|
|
|
+ public function loadClassMetadata(EventArgs $eventArgs)
|
|
|
{
|
|
|
- return $uow->getScheduledEntityDeletions();
|
|
|
+ $ea = $this->getEventAdapter($eventArgs);
|
|
|
+ $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * Checks for inserted object to update its logEntry
|
|
|
+ * foreign key
|
|
|
+ *
|
|
|
+ * @param EventArgs $args
|
|
|
+ * @return void
|
|
|
*/
|
|
|
- protected function getObjectChangeSet($uow, $object)
|
|
|
+ public function postPersist(EventArgs $args)
|
|
|
{
|
|
|
- return $uow->getEntityChangeSet($object);
|
|
|
+ $ea = $this->getEventAdapter($args);
|
|
|
+ $object = $ea->getObject();
|
|
|
+ $om = $ea->getObjectManager();
|
|
|
+ $oid = spl_object_hash($object);
|
|
|
+ $uow = $om->getUnitOfWork();
|
|
|
+ if ($this->pendingLogEntryInserts && array_key_exists($oid, $this->pendingLogEntryInserts)) {
|
|
|
+ $meta = $om->getClassMetadata(get_class($object));
|
|
|
+ $config = $this->getConfiguration($om, $meta->name);
|
|
|
+ // there should be single identifier
|
|
|
+ $identifierField = $ea->getSingleIdentifierFieldName($meta);
|
|
|
+ $logEntry = $this->pendingLogEntryInserts[$oid];
|
|
|
+ $logEntryMeta = $om->getClassMetadata(get_class($logEntry));
|
|
|
+
|
|
|
+ $id = $meta->getReflectionProperty($identifierField)->getValue($object);
|
|
|
+ $logEntryMeta->getReflectionProperty('objectId')->setValue($logEntry, $id);
|
|
|
+ $uow->scheduleExtraUpdate($logEntry, array(
|
|
|
+ 'objectId' => array(null, $id)
|
|
|
+ ));
|
|
|
+ unset($this->pendingLogEntryInserts[$oid]);
|
|
|
+ }
|
|
|
+ if ($this->pendingRelatedObjects && array_key_exists($oid, $this->pendingRelatedObjects)) {
|
|
|
+ $identifiers = $ea->extractIdentifier($om, $object, false);
|
|
|
+ $props = $this->pendingRelatedObjects[$oid];
|
|
|
+
|
|
|
+ $logEntry = $props['log'];
|
|
|
+ $logEntryMeta = $om->getClassMetadata(get_class($logEntry));
|
|
|
+ $oldData = $data = $logEntry->getData();
|
|
|
+ $data[$props['field']] = $identifiers;
|
|
|
+ $logEntry->setData($data);
|
|
|
+
|
|
|
+ $uow->scheduleExtraUpdate($logEntry, array(
|
|
|
+ 'data' => array($oldData, $data)
|
|
|
+ ));
|
|
|
+ unset($this->pendingRelatedObjects[$oid]);
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * Looks for loggable objects being inserted or updated
|
|
|
+ * for further processing
|
|
|
+ *
|
|
|
+ * @param EventArgs $args
|
|
|
+ * @return void
|
|
|
*/
|
|
|
- protected function getSingleIdentifierFieldName(ClassMetadata $meta)
|
|
|
+ public function onFlush(EventArgs $eventArgs)
|
|
|
{
|
|
|
- return $meta->getSingleIdentifierFieldName();
|
|
|
+ $ea = $this->getEventAdapter($eventArgs);
|
|
|
+ $om = $ea->getObjectManager();
|
|
|
+ $uow = $om->getUnitOfWork();
|
|
|
+
|
|
|
+ foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
|
|
|
+ $this->createLogEntry(self::ACTION_CREATE, $object, $ea);
|
|
|
+ }
|
|
|
+ foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
|
|
|
+ $this->createLogEntry(self::ACTION_UPDATE, $object, $ea);
|
|
|
+ }
|
|
|
+ foreach ($ea->getScheduledObjectDeletions($uow) as $object) {
|
|
|
+ $this->createLogEntry(self::ACTION_REMOVE, $object, $ea);
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * {@inheritDoc}
|
|
|
*/
|
|
|
- protected function getObject(EventArgs $args)
|
|
|
+ protected function getNamespace()
|
|
|
{
|
|
|
- return $args->getEntity();
|
|
|
+ return __NAMESPACE__;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
- * {@inheritdoc}
|
|
|
+ * Create a new Log instance
|
|
|
+ *
|
|
|
+ * @param string $action
|
|
|
+ * @param object $object
|
|
|
+ * @param AdapterInterface $ea
|
|
|
+ * @return void
|
|
|
*/
|
|
|
- protected function getNewVersion(ClassMetadata $meta, ObjectManager $om, $object)
|
|
|
+ private function createLogEntry($action, $object, AdapterInterface $ea)
|
|
|
{
|
|
|
- $objectMeta = $om->getClassMetadata(get_class($object));
|
|
|
- $identifierField = $this->getSingleIdentifierFieldName($objectMeta);
|
|
|
- $objectId = $objectMeta->getReflectionProperty($identifierField)->getValue($object);
|
|
|
-
|
|
|
- $dql = "SELECT MAX(log.version) FROM {$meta->name} log";
|
|
|
- $dql .= " WHERE log.objectId = :objectId";
|
|
|
- $dql .= " AND log.objectClass = :objectClass";
|
|
|
-
|
|
|
- $q = $om->createQuery($dql);
|
|
|
- $q->setParameters(array(
|
|
|
- 'objectId' => $objectId,
|
|
|
- 'objectClass' => $objectMeta->name
|
|
|
- ));
|
|
|
- return $q->getSingleScalarResult() + 1;
|
|
|
+ $om = $ea->getObjectManager();
|
|
|
+ $meta = $om->getClassMetadata(get_class($object));
|
|
|
+ if ($config = $this->getConfiguration($om, $meta->name)) {
|
|
|
+ $logEntryClass = $this->getLogEntryClass($ea, $meta->name);
|
|
|
+ $logEntry = new $logEntryClass;
|
|
|
+
|
|
|
+ $logEntry->setAction($action);
|
|
|
+ $logEntry->setUsername($this->username);
|
|
|
+ $logEntry->setObjectClass($meta->name);
|
|
|
+ $logEntry->setLoggedAt();
|
|
|
+
|
|
|
+ // check for the availability of the primary key
|
|
|
+ $identifierField = $ea->getSingleIdentifierFieldName($meta);
|
|
|
+ $objectId = $meta->getReflectionProperty($identifierField)->getValue($object);
|
|
|
+ if (!$objectId && $action === self::ACTION_CREATE) {
|
|
|
+ $this->pendingLogEntryInserts[spl_object_hash($object)] = $logEntry;
|
|
|
+ }
|
|
|
+ $uow = $om->getUnitOfWork();
|
|
|
+ $logEntry->setObjectId($objectId);
|
|
|
+ if ($action !== self::ACTION_REMOVE) {
|
|
|
+ $newValues = array();
|
|
|
+ foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) {
|
|
|
+ $value = $changes[1];
|
|
|
+ if ($meta->isCollectionValuedAssociation($field)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if ($meta->isSingleValuedAssociation($field) && $value) {
|
|
|
+ $value = $ea->extractIdentifier($om, $value, false);
|
|
|
+ if (!is_array($value)) {
|
|
|
+ $this->pendingRelatedObjects[$value] = array(
|
|
|
+ 'log' => $logEntry,
|
|
|
+ 'field' => $field
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $newValues[$field] = $value;
|
|
|
+ }
|
|
|
+ $logEntry->setData($newValues);
|
|
|
+ }
|
|
|
+ $version = 1;
|
|
|
+ $logEntryMeta = $om->getClassMetadata($logEntryClass);
|
|
|
+ if ($action !== self::ACTION_CREATE) {
|
|
|
+ $version = $ea->getNewVersion($logEntryMeta, $om, $object);
|
|
|
+ }
|
|
|
+ $logEntry->setVersion($version);
|
|
|
+
|
|
|
+ $om->persist($logEntry);
|
|
|
+ $uow->computeChangeSet($logEntryMeta, $logEntry);
|
|
|
+ }
|
|
|
}
|
|
|
}
|