vendor/damienharper/auditor/src/Provider/Doctrine/DoctrineProvider.php line 51

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace DH\Auditor\Provider\Doctrine;
  4. use DH\Auditor\Event\LifecycleEvent;
  5. use DH\Auditor\Exception\InvalidArgumentException;
  6. use DH\Auditor\Exception\ProviderException;
  7. use DH\Auditor\Provider\AbstractProvider;
  8. use DH\Auditor\Provider\ConfigurationInterface;
  9. use DH\Auditor\Provider\Doctrine\Auditing\Annotation\AnnotationLoader;
  10. use DH\Auditor\Provider\Doctrine\Auditing\Event\DoctrineSubscriber;
  11. use DH\Auditor\Provider\Doctrine\Auditing\Transaction\TransactionManager;
  12. use DH\Auditor\Provider\Doctrine\Persistence\Event\CreateSchemaListener;
  13. use DH\Auditor\Provider\Doctrine\Persistence\Event\TableSchemaSubscriber;
  14. use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;
  15. use DH\Auditor\Provider\Doctrine\Service\AuditingService;
  16. use DH\Auditor\Provider\Doctrine\Service\StorageService;
  17. use DH\Auditor\Provider\ProviderInterface;
  18. use DH\Auditor\Provider\Service\AuditingServiceInterface;
  19. use DH\Auditor\Provider\Service\StorageServiceInterface;
  20. use Doctrine\ORM\EntityManagerInterface;
  21. use Exception;
  22. /**
  23. * @see \DH\Auditor\Tests\Provider\Doctrine\DoctrineProviderTest
  24. */
  25. class DoctrineProvider extends AbstractProvider
  26. {
  27. private TransactionManager $transactionManager;
  28. public function __construct(ConfigurationInterface $configuration)
  29. {
  30. $this->configuration = $configuration;
  31. $this->transactionManager = new TransactionManager($this);
  32. \assert($this->configuration instanceof Configuration); // helps PHPStan
  33. $this->configuration->setProvider($this);
  34. }
  35. public function registerAuditingService(AuditingServiceInterface $service): ProviderInterface
  36. {
  37. parent::registerAuditingService($service);
  38. \assert($service instanceof AuditingService); // helps PHPStan
  39. $entityManager = $service->getEntityManager();
  40. $evm = $entityManager->getEventManager();
  41. // Register subscribers
  42. $evm->addEventSubscriber(new TableSchemaSubscriber($this));
  43. $evm->addEventSubscriber(new CreateSchemaListener($this));
  44. $evm->addEventSubscriber(new DoctrineSubscriber($this->transactionManager));
  45. return $this;
  46. }
  47. public function isStorageMapperRequired(): bool
  48. {
  49. return \count($this->getStorageServices()) > 1;
  50. }
  51. public function getAuditingServiceForEntity(string $entity): AuditingServiceInterface
  52. {
  53. foreach ($this->auditingServices as $name => $service) {
  54. \assert($service instanceof AuditingService); // helps PHPStan
  55. try {
  56. // entity is managed by the entity manager of this service
  57. $service->getEntityManager()->getClassMetadata($entity)->getTableName();
  58. return $service;
  59. } catch (Exception $e) {
  60. }
  61. }
  62. throw new InvalidArgumentException(sprintf('Auditing service not found for "%s".', $entity));
  63. }
  64. public function getStorageServiceForEntity(string $entity): StorageServiceInterface
  65. {
  66. $this->checkStorageMapper();
  67. \assert($this->configuration instanceof Configuration); // helps PHPStan
  68. $storageMapper = $this->configuration->getStorageMapper();
  69. if (null === $storageMapper || 1 === \count($this->getStorageServices())) {
  70. // No mapper and only 1 storage entity manager
  71. return array_values($this->getStorageServices())[0];
  72. }
  73. if (\is_string($storageMapper) && class_exists($storageMapper)) {
  74. $storageMapper = new $storageMapper();
  75. }
  76. \assert(\is_callable($storageMapper)); // helps PHPStan
  77. return $storageMapper($entity, $this->getStorageServices());
  78. }
  79. public function persist(LifecycleEvent $event): void
  80. {
  81. $payload = $event->getPayload();
  82. $auditTable = $payload['table'];
  83. $entity = $payload['entity'];
  84. unset($payload['table'], $payload['entity']);
  85. $fields = [
  86. 'type' => ':type',
  87. 'object_id' => ':object_id',
  88. 'discriminator' => ':discriminator',
  89. 'transaction_hash' => ':transaction_hash',
  90. 'diffs' => ':diffs',
  91. 'blame_id' => ':blame_id',
  92. 'blame_user' => ':blame_user',
  93. 'blame_user_fqdn' => ':blame_user_fqdn',
  94. 'blame_user_firewall' => ':blame_user_firewall',
  95. 'ip' => ':ip',
  96. 'created_at' => ':created_at',
  97. ];
  98. $query = sprintf(
  99. 'INSERT INTO %s (%s) VALUES (%s)',
  100. $auditTable,
  101. implode(', ', array_keys($fields)),
  102. implode(', ', array_values($fields))
  103. );
  104. /** @var StorageService $storageService */
  105. $storageService = $this->getStorageServiceForEntity($entity);
  106. $statement = $storageService->getEntityManager()->getConnection()->prepare($query);
  107. foreach ($payload as $key => $value) {
  108. $statement->bindValue($key, $value);
  109. }
  110. DoctrineHelper::executeStatement($statement);
  111. // let's get the last inserted ID from the database so other providers can use that info
  112. $payload = $event->getPayload();
  113. $payload['id'] = (int) $storageService->getEntityManager()->getConnection()->lastInsertId();
  114. $event->setPayload($payload);
  115. }
  116. /**
  117. * Returns true if $entity is auditable.
  118. *
  119. * @param object|string $entity
  120. */
  121. public function isAuditable($entity): bool
  122. {
  123. $class = DoctrineHelper::getRealClassName($entity);
  124. // is $entity part of audited entities?
  125. \assert($this->configuration instanceof Configuration); // helps PHPStan
  126. // no => $entity is not audited
  127. return !(!\array_key_exists($class, $this->configuration->getEntities()));
  128. }
  129. /**
  130. * Returns true if $entity is audited.
  131. *
  132. * @param object|string $entity
  133. */
  134. public function isAudited($entity): bool
  135. {
  136. \assert(null !== $this->auditor);
  137. if (!$this->auditor->getConfiguration()->isEnabled()) {
  138. return false;
  139. }
  140. /** @var Configuration $configuration */
  141. $configuration = $this->configuration;
  142. $class = DoctrineHelper::getRealClassName($entity);
  143. $entities = $configuration->getEntities();
  144. // is $entity part of audited entities?
  145. if (!\array_key_exists($class, $entities)) {
  146. // no => $entity is not audited
  147. return false;
  148. }
  149. $entityOptions = $entities[$class];
  150. if (null === $entityOptions) {
  151. // no option defined => $entity is audited
  152. return true;
  153. }
  154. if (isset($entityOptions['enabled'])) {
  155. return (bool) $entityOptions['enabled'];
  156. }
  157. return true;
  158. }
  159. /**
  160. * Returns true if $field is audited.
  161. *
  162. * @param object|string $entity
  163. */
  164. public function isAuditedField($entity, string $field): bool
  165. {
  166. // is $field is part of globally ignored columns?
  167. \assert($this->configuration instanceof Configuration); // helps PHPStan
  168. if (\in_array($field, $this->configuration->getIgnoredColumns(), true)) {
  169. // yes => $field is not audited
  170. return false;
  171. }
  172. // is $entity audited?
  173. if (!$this->isAudited($entity)) {
  174. // no => $field is not audited
  175. return false;
  176. }
  177. $class = DoctrineHelper::getRealClassName($entity);
  178. $entityOptions = $this->configuration->getEntities()[$class];
  179. if (null === $entityOptions) {
  180. // no option defined => $field is audited
  181. return true;
  182. }
  183. // are columns excluded and is field part of them?
  184. // yes => $field is not audited
  185. return !(isset($entityOptions['ignored_columns'])
  186. && \in_array($field, $entityOptions['ignored_columns'], true));
  187. }
  188. public function supportsStorage(): bool
  189. {
  190. return true;
  191. }
  192. public function supportsAuditing(): bool
  193. {
  194. return true;
  195. }
  196. public function setStorageMapper(callable $storageMapper): void
  197. {
  198. \assert($this->configuration instanceof Configuration); // helps PHPStan
  199. $this->configuration->setStorageMapper($storageMapper);
  200. }
  201. public function loadAnnotations(EntityManagerInterface $entityManager, array $entities): self
  202. {
  203. \assert($this->configuration instanceof Configuration); // helps PHPStan
  204. $ormConfiguration = $entityManager->getConfiguration();
  205. /** @since doctrine/orm:2.7.5 */
  206. $metadataCache = method_exists($ormConfiguration, 'getMetadataCache')
  207. ? $ormConfiguration->getMetadataCache()
  208. : null;
  209. $annotationLoader = new AnnotationLoader($entityManager);
  210. if (null !== $metadataCache) {
  211. $item = $metadataCache->getItem('__DH_ANNOTATIONS__');
  212. if (!$item->isHit() || !\is_array($annotationEntities = $item->get())) {
  213. $annotationEntities = $annotationLoader->load();
  214. $item->set($annotationEntities);
  215. $metadataCache->save($item);
  216. }
  217. } else {
  218. $annotationEntities = $annotationLoader->load();
  219. }
  220. $this->configuration->setEntities(array_merge($entities, $annotationEntities));
  221. return $this;
  222. }
  223. private function checkStorageMapper(): self
  224. {
  225. \assert($this->configuration instanceof Configuration); // helps PHPStan
  226. if (null === $this->configuration->getStorageMapper() && $this->isStorageMapperRequired()) {
  227. throw new ProviderException('You must provide a mapper callback to map audits to storage.');
  228. }
  229. return $this;
  230. }
  231. }