src/EventSubscriber/DatabaseAuditSubscriber.php line 122

Open in your IDE?
  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: zeldystern
  5. * Date: 2022-01-04
  6. * Time: 11:38
  7. */
  8. namespace App\EventSubscriber;
  9. use App\Logger\AuditLogger;
  10. use Doctrine\Common\EventSubscriber;
  11. use Doctrine\DBAL\Connection;
  12. use Doctrine\DBAL\Logging\LoggerChain;
  13. use Doctrine\DBAL\Types\Type;
  14. use Doctrine\DBAL\Types\Types;
  15. use Doctrine\ORM\EntityManager;
  16. use Doctrine\ORM\Event\OnFlushEventArgs;
  17. use Doctrine\ORM\Events;
  18. use Doctrine\ORM\PersistentCollection;
  19. use Doctrine\ORM\Proxy\Proxy;
  20. use Doctrine\ORM\UnitOfWork;
  21. use Psr\Log\LoggerInterface;
  22. use Symfony\Component\Security\Core\Security;
  23. class DatabaseAuditSubscriber implements EventSubscriber
  24. {
  25. const LOG_TABLE = 'queryLog';
  26. public const INSERT = 'insert';
  27. public const UPDATE = 'update';
  28. public const DELETE = 'delete';
  29. public const ASSOCIATE = 'CASC';
  30. public const DISSOCIATE = 'CDSC';
  31. private $changes;
  32. private $security;
  33. private $logger;
  34. public function __construct(Security $security, LoggerInterface $logger)
  35. {
  36. $this->security = $security;
  37. $this->logger = $logger;
  38. }
  39. public function onFlush(OnFlushEventArgs $args): void
  40. {
  41. $loggers = [];
  42. $em = $args->getEntityManager();
  43. $uow = $em->getUnitOfWork();
  44. $this->collectScheduledUpdates($uow, $em);
  45. $this->collectScheduledInsertions($uow, $em);
  46. $this->collectScheduledDeletions($uow, $em);
  47. #$this->manager->collectScheduledCollectionDeletions($uow, $em);
  48. #$this->manager->collectScheduledCollectionUpdates($uow, $em);
  49. $defaultLogger = $em->getConnection()->getConfiguration()->getSQLLogger();
  50. if ($defaultLogger) {
  51. $loggers[] = $defaultLogger;
  52. }
  53. $auditLogger = new AuditLogger(function () use ($em): void {
  54. $this->processChanges($em);
  55. $this->resetChangeset();
  56. });
  57. $loggers[] = $auditLogger;
  58. $loggerChain = new LoggerChain($loggers);
  59. $em->getConnection()->getConfiguration()->setSQLLogger($loggerChain);
  60. }
  61. /**
  62. * {@inheritdoc}
  63. */
  64. public function getSubscribedEvents(): array
  65. {
  66. return [Events::onFlush, 'onFlush'];
  67. }
  68. private function collectScheduledInsertions(UnitOfWork $uow, EntityManager $em): void
  69. {
  70. foreach ($uow->getScheduledEntityInsertions() as $entity) {
  71. $changeSet = $uow->getEntityChangeSet($entity);
  72. $diff = $this->getQuery($em, $entity, $changeSet);
  73. $this->changes[] = [
  74. 'action' => self::INSERT,
  75. 'data' => [
  76. $entity,
  77. $diff,
  78. ],
  79. ];
  80. }
  81. }
  82. public function collectScheduledUpdates(UnitOfWork $uow, EntityManager $em): void
  83. {
  84. foreach ($uow->getScheduledEntityUpdates() as $entity) {
  85. $changeSet = $uow->getEntityChangeSet($entity);
  86. $diff = $this->getQuery($em, $entity, $changeSet);
  87. $this->changes[] = [
  88. 'action' => self::UPDATE,
  89. 'data' => [
  90. $entity,
  91. $diff,
  92. ],
  93. ];
  94. }
  95. }
  96. public function collectScheduledDeletions(UnitOfWork $uow, EntityManager $em): void
  97. {
  98. foreach ($uow->getScheduledEntityDeletions() as $entity) {
  99. $uow->initializeObject($entity);
  100. $id = $this->id($em, $entity);
  101. $deleteStatement = '';
  102. $entityClassName = \get_class($entity);
  103. foreach ((array) $entity as $fieldName => $value) {
  104. $realFieldName = \str_replace(["\x00*\x00", "\x00${entityClassName}\x00"], '', $fieldName);
  105. if (\is_object($value)) {
  106. if (\method_exists($value, '__toString')) {
  107. $realValue = (string) $value;
  108. } elseif ($value instanceof \DateTime) {
  109. $realValue = $value->format('r');
  110. } elseif ($value instanceof PersistentCollection) {
  111. continue;
  112. } else {
  113. $realValue = \get_class($value) . '#' . $this->id($em, $value);
  114. }
  115. } else {
  116. $realValue = $value;
  117. }
  118. $deleteStatement .= $realFieldName . ' = ' . $realValue . ', ';
  119. }
  120. $this->changes[] = [
  121. 'action' => self::DELETE,
  122. 'data' => [
  123. $entity,
  124. $deleteStatement,
  125. $id,
  126. ],
  127. ];
  128. }
  129. }
  130. private function resetChangeset(): void
  131. {
  132. $this->changes = [];
  133. }
  134. public function getQuery(EntityManager $em, $entity, array $ch): string
  135. {
  136. $meta = $em->getClassMetadata(\get_class($entity));
  137. $query = '';
  138. foreach ($ch as $fieldName => [$old, $new]) {
  139. if (!isset($meta->embeddedClasses[$fieldName]) && $meta->hasField($fieldName)) {
  140. $mapping = $meta->fieldMappings[$fieldName];
  141. $type = Type::getType($mapping['type']);
  142. $o = $this->value($em, $type, $old, $mapping);
  143. $n = $this->value($em, $type, $new, $mapping);
  144. }
  145. if ($o !== $n) {
  146. $query .= $fieldName . ' = ' . $n . ', ';
  147. }
  148. }
  149. return $query;
  150. }
  151. public function summarize(EntityManager $em, $entity = null, $id = null): ?array
  152. {
  153. if (null === $entity) {
  154. return null;
  155. }
  156. $em->getUnitOfWork()->initializeObject($entity); // ensure that proxies are initialized
  157. $meta = $em->getClassMetadata(self::getRealClass($entity));
  158. $pkName = $meta->getSingleIdentifierFieldName();
  159. $pkValue = $id ?? $this->id($em, $entity);
  160. if (\method_exists($entity, '__toString')) {
  161. $label = (string) $entity;
  162. } else {
  163. $label = \get_class($entity) . '#' . $pkValue;
  164. }
  165. return [
  166. 'label' => $label,
  167. 'class' => $meta->name,
  168. 'table' => $meta->getTableName(),
  169. $pkName => $pkValue,
  170. ];
  171. }
  172. public static function getRealClass($subject): string
  173. {
  174. $class = \is_object($subject) ? \get_class($subject) : $subject;
  175. if (false === $pos = \strrpos($class, '\\' . Proxy::MARKER . '\\')) {
  176. return $class;
  177. }
  178. return \substr($class, $pos + Proxy::MARKER_LENGTH + 2);
  179. }
  180. public function id(EntityManager $em, $entity)
  181. {
  182. $meta = $em->getClassMetadata(\get_class($entity));
  183. $pk = $meta->getSingleIdentifierFieldName();
  184. if (isset($meta->fieldMappings[$pk])) {
  185. $type = Type::getType($meta->fieldMappings[$pk]['type']);
  186. return $this->value($em, $type, $meta->getReflectionProperty($pk)->getValue($entity));
  187. }
  188. // Primary key is not part of fieldMapping
  189. // @see https://github.com/DamienHarper/DoctrineAuditBundle/issues/40
  190. // @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html#identity-through-foreign-entities
  191. // We try to get it from associationMapping (will throw a MappingException if not available)
  192. $targetEntity = $meta->getReflectionProperty($pk)->getValue($entity);
  193. $mapping = $meta->getAssociationMapping($pk);
  194. $meta = $em->getClassMetadata($mapping['targetEntity']);
  195. $pk = $meta->getSingleIdentifierFieldName();
  196. $type = Type::getType($meta->fieldMappings[$pk]['type']);
  197. return $this->value($em, $type, $meta->getReflectionProperty($pk)->getValue($targetEntity));
  198. }
  199. private function getChanger()
  200. {
  201. return $this->security->getUser() ? (
  202. (in_array('ROLE_APIUSER',$this->security->getUser()->getRoles()) ?
  203. (isset($_GET['username'])?$this->security->getUser()->getUsername().'-'.$_GET['username']:$this->security->getUser()->getUsername()):
  204. (in_array('ROLE_API',$this->security->getUser()->getRoles()) ?
  205. (isset($_GET['username'])?(isset($_GET['maspedStudentId'])?'MASPED-'.$_GET['username']:'TTI-'.$_GET['username']):'API'):
  206. $this->security->getUser()->getUsername())))
  207. : 'nsi';
  208. }
  209. private function value(EntityManager $em, Type $type, $value, $mapping = [])
  210. {
  211. if (null === $value) {
  212. return null;
  213. }
  214. $platform = $em->getConnection()->getDatabasePlatform();
  215. switch ($type->getName()) {
  216. case Types::DECIMAL:
  217. if ($mapping) {
  218. $convertedValue = \number_format((float) $value, $mapping['scale'], '.', '');
  219. break;
  220. }
  221. // no break
  222. case Types::BIGINT:
  223. $convertedValue = (string) $value;
  224. break;
  225. case Types::INTEGER:
  226. case Types::SMALLINT:
  227. $convertedValue = (int) $value;
  228. break;
  229. case Types::FLOAT:
  230. case Types::BOOLEAN:
  231. $convertedValue = $type->convertToPHPValue($value, $platform);
  232. break;
  233. case Types::BLOB:
  234. if (\is_resource($value)) {
  235. $convertedValue = base64_encode(stream_get_contents($value));
  236. rewind($value);
  237. } else {
  238. $convertedValue = base64_encode($value);
  239. }
  240. break;
  241. default:
  242. $convertedValue = $type->convertToDatabaseValue($value, $platform);
  243. }
  244. return $convertedValue;
  245. }
  246. public function insert(EntityManager $em, $entity, $diff): array
  247. {
  248. $meta = $em->getClassMetadata(\get_class($entity));
  249. return $this->saveAudit(
  250. $em,
  251. [
  252. 'action' => self::INSERT,
  253. 'changer' => $this->getChanger(),
  254. 'diff' => $diff,
  255. 'table' => $meta->getTableName(),
  256. 'schema' => $meta->getSchemaName(),
  257. 'id' => $this->id($em, $entity),
  258. ]
  259. );
  260. }
  261. public function update(EntityManager $em, $entity, $diff): array
  262. {
  263. if (!$diff) {
  264. return []; // if there is no entity diff, do not log it
  265. }
  266. $meta = $em->getClassMetadata(\get_class($entity));
  267. return $this->saveAudit(
  268. $em,
  269. [
  270. 'action' => self::UPDATE,
  271. 'changer' => $this->getChanger(),
  272. 'diff' => $diff,
  273. 'table' => $meta->getTableName(),
  274. 'schema' => $meta->getSchemaName(),
  275. 'id' => $this->id($em, $entity),
  276. ]
  277. );
  278. }
  279. public function remove(EntityManager $em, $entity, $diff, $id): array
  280. {
  281. $meta = $em->getClassMetadata(\get_class($entity));
  282. return $this->saveAudit(
  283. $em,
  284. [
  285. 'action' => self::DELETE,
  286. 'changer' => $this->getChanger(),
  287. 'diff' => $diff,
  288. 'table' => $meta->getTableName(),
  289. 'schema' => $meta->getSchemaName(),
  290. 'id' => $id,
  291. ]
  292. );
  293. }
  294. private function saveAudit(EntityManager $em, array $data): array
  295. {
  296. $fields = [
  297. 'tname' => ':tname',
  298. 'op' => ':op',
  299. 'tid' => ':tid',
  300. 'querySql' => ':querySql',
  301. 'loginName' => ':loginName',
  302. 'stamp' => ':stamp'
  303. ];
  304. $query = \sprintf(
  305. 'INSERT INTO %s (%s) VALUES (%s)',
  306. self::LOG_TABLE,
  307. \implode(', ', \array_keys($fields)),
  308. \implode(', ', \array_values($fields))
  309. );
  310. $dt = new \DateTime('now');
  311. $params = [
  312. 'tname' => (string) $data['table'],
  313. 'op' => $data['action'],
  314. 'tid' => $data['id'],
  315. 'querySql' => $data['diff'],
  316. 'loginName' => $data['changer'],
  317. 'stamp' => $dt->format('Y-m-d H:i:s')
  318. ];
  319. return [$query, $params];
  320. }
  321. private function isLogTable($em, $data)
  322. {
  323. $meta = $em->getClassMetadata(\get_class($data));
  324. return $meta->getTableName() == self::LOG_TABLE;
  325. }
  326. public function processChanges(EntityManager $em): void
  327. {
  328. if ($this->changes) {
  329. $queries = [];
  330. foreach ($this->changes as $entityChanges) {
  331. $action = $entityChanges['action'];
  332. $data = $entityChanges['data'];
  333. if ($this->isLogTable($em, $data[0])) {
  334. continue;
  335. }
  336. switch ($action) {
  337. case self::INSERT:
  338. $query = $this->insert($em, $data[0], $data[1]);
  339. break;
  340. case self::UPDATE:
  341. $query = $this->update($em, $data[0], $data[1]);
  342. break;
  343. case self::DELETE:
  344. $query = $this->remove($em, $data[0], $data[1], $data[2]);
  345. break;
  346. // case self::ASSOCIATE:
  347. // case self::DISSOCIATE:
  348. // $query = $this->toggleAssociation($action, $em, $data[0], $data[1]);
  349. // break;
  350. }
  351. if ($query) {
  352. $queries[] = $query;
  353. }
  354. }
  355. $em->getConnection()->transactional(function (Connection $connection) use ($queries): void {
  356. foreach ($queries as $query) {
  357. $stmt = $connection->prepare($query[0]);
  358. $stmt->execute($query[1]);
  359. }
  360. });
  361. }
  362. }
  363. }