<?php
/**
* Created by PhpStorm.
* User: zeldystern
* Date: 2022-01-04
* Time: 11:38
*/
namespace App\EventSubscriber;
use App\Logger\AuditLogger;
use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Logging\LoggerChain;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\Proxy;
use Doctrine\ORM\UnitOfWork;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Security;
class DatabaseAuditSubscriber implements EventSubscriber
{
const LOG_TABLE = 'queryLog';
public const INSERT = 'insert';
public const UPDATE = 'update';
public const DELETE = 'delete';
public const ASSOCIATE = 'CASC';
public const DISSOCIATE = 'CDSC';
private $changes;
private $security;
private $logger;
public function __construct(Security $security, LoggerInterface $logger)
{
$this->security = $security;
$this->logger = $logger;
}
public function onFlush(OnFlushEventArgs $args): void
{
$loggers = [];
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$this->collectScheduledUpdates($uow, $em);
$this->collectScheduledInsertions($uow, $em);
$this->collectScheduledDeletions($uow, $em);
#$this->manager->collectScheduledCollectionDeletions($uow, $em);
#$this->manager->collectScheduledCollectionUpdates($uow, $em);
$defaultLogger = $em->getConnection()->getConfiguration()->getSQLLogger();
if ($defaultLogger) {
$loggers[] = $defaultLogger;
}
$auditLogger = new AuditLogger(function () use ($em): void {
$this->processChanges($em);
$this->resetChangeset();
});
$loggers[] = $auditLogger;
$loggerChain = new LoggerChain($loggers);
$em->getConnection()->getConfiguration()->setSQLLogger($loggerChain);
}
/**
* {@inheritdoc}
*/
public function getSubscribedEvents(): array
{
return [Events::onFlush, 'onFlush'];
}
private function collectScheduledInsertions(UnitOfWork $uow, EntityManager $em): void
{
foreach ($uow->getScheduledEntityInsertions() as $entity) {
$changeSet = $uow->getEntityChangeSet($entity);
$diff = $this->getQuery($em, $entity, $changeSet);
$this->changes[] = [
'action' => self::INSERT,
'data' => [
$entity,
$diff,
],
];
}
}
public function collectScheduledUpdates(UnitOfWork $uow, EntityManager $em): void
{
foreach ($uow->getScheduledEntityUpdates() as $entity) {
$changeSet = $uow->getEntityChangeSet($entity);
$diff = $this->getQuery($em, $entity, $changeSet);
$this->changes[] = [
'action' => self::UPDATE,
'data' => [
$entity,
$diff,
],
];
}
}
public function collectScheduledDeletions(UnitOfWork $uow, EntityManager $em): void
{
foreach ($uow->getScheduledEntityDeletions() as $entity) {
$uow->initializeObject($entity);
$id = $this->id($em, $entity);
$deleteStatement = '';
$entityClassName = \get_class($entity);
foreach ((array) $entity as $fieldName => $value) {
$realFieldName = \str_replace(["\x00*\x00", "\x00${entityClassName}\x00"], '', $fieldName);
if (\is_object($value)) {
if (\method_exists($value, '__toString')) {
$realValue = (string) $value;
} elseif ($value instanceof \DateTime) {
$realValue = $value->format('r');
} elseif ($value instanceof PersistentCollection) {
continue;
} else {
$realValue = \get_class($value) . '#' . $this->id($em, $value);
}
} else {
$realValue = $value;
}
$deleteStatement .= $realFieldName . ' = ' . $realValue . ', ';
}
$this->changes[] = [
'action' => self::DELETE,
'data' => [
$entity,
$deleteStatement,
$id,
],
];
}
}
private function resetChangeset(): void
{
$this->changes = [];
}
public function getQuery(EntityManager $em, $entity, array $ch): string
{
$meta = $em->getClassMetadata(\get_class($entity));
$query = '';
foreach ($ch as $fieldName => [$old, $new]) {
if (!isset($meta->embeddedClasses[$fieldName]) && $meta->hasField($fieldName)) {
$mapping = $meta->fieldMappings[$fieldName];
$type = Type::getType($mapping['type']);
$o = $this->value($em, $type, $old, $mapping);
$n = $this->value($em, $type, $new, $mapping);
}
if ($o !== $n) {
$query .= $fieldName . ' = ' . $n . ', ';
}
}
return $query;
}
public function summarize(EntityManager $em, $entity = null, $id = null): ?array
{
if (null === $entity) {
return null;
}
$em->getUnitOfWork()->initializeObject($entity); // ensure that proxies are initialized
$meta = $em->getClassMetadata(self::getRealClass($entity));
$pkName = $meta->getSingleIdentifierFieldName();
$pkValue = $id ?? $this->id($em, $entity);
if (\method_exists($entity, '__toString')) {
$label = (string) $entity;
} else {
$label = \get_class($entity) . '#' . $pkValue;
}
return [
'label' => $label,
'class' => $meta->name,
'table' => $meta->getTableName(),
$pkName => $pkValue,
];
}
public static function getRealClass($subject): string
{
$class = \is_object($subject) ? \get_class($subject) : $subject;
if (false === $pos = \strrpos($class, '\\' . Proxy::MARKER . '\\')) {
return $class;
}
return \substr($class, $pos + Proxy::MARKER_LENGTH + 2);
}
public function id(EntityManager $em, $entity)
{
$meta = $em->getClassMetadata(\get_class($entity));
$pk = $meta->getSingleIdentifierFieldName();
if (isset($meta->fieldMappings[$pk])) {
$type = Type::getType($meta->fieldMappings[$pk]['type']);
return $this->value($em, $type, $meta->getReflectionProperty($pk)->getValue($entity));
}
// Primary key is not part of fieldMapping
// @see https://github.com/DamienHarper/DoctrineAuditBundle/issues/40
// @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html#identity-through-foreign-entities
// We try to get it from associationMapping (will throw a MappingException if not available)
$targetEntity = $meta->getReflectionProperty($pk)->getValue($entity);
$mapping = $meta->getAssociationMapping($pk);
$meta = $em->getClassMetadata($mapping['targetEntity']);
$pk = $meta->getSingleIdentifierFieldName();
$type = Type::getType($meta->fieldMappings[$pk]['type']);
return $this->value($em, $type, $meta->getReflectionProperty($pk)->getValue($targetEntity));
}
private function getChanger()
{
return $this->security->getUser() ? (
(in_array('ROLE_APIUSER',$this->security->getUser()->getRoles()) ?
(isset($_GET['username'])?$this->security->getUser()->getUsername().'-'.$_GET['username']:$this->security->getUser()->getUsername()):
(in_array('ROLE_API',$this->security->getUser()->getRoles()) ?
(isset($_GET['username'])?(isset($_GET['maspedStudentId'])?'MASPED-'.$_GET['username']:'TTI-'.$_GET['username']):'API'):
$this->security->getUser()->getUsername())))
: 'nsi';
}
private function value(EntityManager $em, Type $type, $value, $mapping = [])
{
if (null === $value) {
return null;
}
$platform = $em->getConnection()->getDatabasePlatform();
switch ($type->getName()) {
case Types::DECIMAL:
if ($mapping) {
$convertedValue = \number_format((float) $value, $mapping['scale'], '.', '');
break;
}
// no break
case Types::BIGINT:
$convertedValue = (string) $value;
break;
case Types::INTEGER:
case Types::SMALLINT:
$convertedValue = (int) $value;
break;
case Types::FLOAT:
case Types::BOOLEAN:
$convertedValue = $type->convertToPHPValue($value, $platform);
break;
case Types::BLOB:
if (\is_resource($value)) {
$convertedValue = base64_encode(stream_get_contents($value));
rewind($value);
} else {
$convertedValue = base64_encode($value);
}
break;
default:
$convertedValue = $type->convertToDatabaseValue($value, $platform);
}
return $convertedValue;
}
public function insert(EntityManager $em, $entity, $diff): array
{
$meta = $em->getClassMetadata(\get_class($entity));
return $this->saveAudit(
$em,
[
'action' => self::INSERT,
'changer' => $this->getChanger(),
'diff' => $diff,
'table' => $meta->getTableName(),
'schema' => $meta->getSchemaName(),
'id' => $this->id($em, $entity),
]
);
}
public function update(EntityManager $em, $entity, $diff): array
{
if (!$diff) {
return []; // if there is no entity diff, do not log it
}
$meta = $em->getClassMetadata(\get_class($entity));
return $this->saveAudit(
$em,
[
'action' => self::UPDATE,
'changer' => $this->getChanger(),
'diff' => $diff,
'table' => $meta->getTableName(),
'schema' => $meta->getSchemaName(),
'id' => $this->id($em, $entity),
]
);
}
public function remove(EntityManager $em, $entity, $diff, $id): array
{
$meta = $em->getClassMetadata(\get_class($entity));
return $this->saveAudit(
$em,
[
'action' => self::DELETE,
'changer' => $this->getChanger(),
'diff' => $diff,
'table' => $meta->getTableName(),
'schema' => $meta->getSchemaName(),
'id' => $id,
]
);
}
private function saveAudit(EntityManager $em, array $data): array
{
$fields = [
'tname' => ':tname',
'op' => ':op',
'tid' => ':tid',
'querySql' => ':querySql',
'loginName' => ':loginName',
'stamp' => ':stamp'
];
$query = \sprintf(
'INSERT INTO %s (%s) VALUES (%s)',
self::LOG_TABLE,
\implode(', ', \array_keys($fields)),
\implode(', ', \array_values($fields))
);
$dt = new \DateTime('now');
$params = [
'tname' => (string) $data['table'],
'op' => $data['action'],
'tid' => $data['id'],
'querySql' => $data['diff'],
'loginName' => $data['changer'],
'stamp' => $dt->format('Y-m-d H:i:s')
];
return [$query, $params];
}
private function isLogTable($em, $data)
{
$meta = $em->getClassMetadata(\get_class($data));
return $meta->getTableName() == self::LOG_TABLE;
}
public function processChanges(EntityManager $em): void
{
if ($this->changes) {
$queries = [];
foreach ($this->changes as $entityChanges) {
$action = $entityChanges['action'];
$data = $entityChanges['data'];
if ($this->isLogTable($em, $data[0])) {
continue;
}
switch ($action) {
case self::INSERT:
$query = $this->insert($em, $data[0], $data[1]);
break;
case self::UPDATE:
$query = $this->update($em, $data[0], $data[1]);
break;
case self::DELETE:
$query = $this->remove($em, $data[0], $data[1], $data[2]);
break;
// case self::ASSOCIATE:
// case self::DISSOCIATE:
// $query = $this->toggleAssociation($action, $em, $data[0], $data[1]);
// break;
}
if ($query) {
$queries[] = $query;
}
}
$em->getConnection()->transactional(function (Connection $connection) use ($queries): void {
foreach ($queries as $query) {
$stmt = $connection->prepare($query[0]);
$stmt->execute($query[1]);
}
});
}
}
}