├── Resources ├── views │ └── Default │ │ └── index.html.twig ├── config │ ├── default │ │ ├── event-resolver.yml │ │ ├── doctrine-event-resolver.yml │ │ └── logger.yml │ ├── doctrine_services.yml │ ├── services.yml │ └── config-sample.yml └── doc │ ├── override-resolver.md │ ├── custom-logger.md │ ├── logger-channel.md │ ├── embed-resolver.md │ ├── pre-persist-listener.md │ ├── custom-resolver.md │ ├── subscriber.md │ ├── doctrine-entity-events.md │ └── audit-log-entity-orm.md ├── Exception ├── InvalidServiceException.php ├── UnrecognizedEventInfoException.php └── UnrecognizedEntityException.php ├── Resolver ├── EmbeddedEventResolverInterface.php ├── EventResolverInterface.php ├── UserEventCommand │ ├── UserLoginCommand.php │ ├── InteractiveLoginCommand.php │ └── ResolverCommand.php ├── DefaultEventResolver.php ├── UserEventResolver.php ├── DoctrineObjectEventResolver.php └── EventResolverFactory.php ├── Logger ├── LoggerInterface.php ├── Logger.php ├── MonologLogger.php └── LoggerFactory.php ├── Subscriber ├── EasyAuditEventSubscriberInterface.php ├── DoctrineDeleteEventLogger.php └── DoctrineSubscriber.php ├── Attribute └── SubscribeDoctrineEvents.php ├── Traits └── EntityHydrationMethod.php ├── Listener └── LogEventsListener.php ├── Events ├── DoctrineObjectEvent.php └── DoctrineEvents.php ├── DependencyInjection ├── Compiler │ ├── MonologLoggerPass.php │ ├── LoggerFactoryPass.php │ ├── ResolverFactoryPass.php │ └── SubscriberPass.php ├── XiideaEasyAuditExtension.php └── Configuration.php ├── LICENSE ├── XiideaEasyAuditBundle.php ├── composer.json ├── Common └── UserAwareComponent.php ├── Model └── BaseAuditLog.php └── README.md /Resources/views/Default/index.html.twig: -------------------------------------------------------------------------------- 1 | Event Log Reporting Comming Soon 2 | -------------------------------------------------------------------------------- /Resources/config/default/event-resolver.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | xiidea.easy_audit.default_event_resolver: 4 | class: Xiidea\EasyAuditBundle\Resolver\DefaultEventResolver 5 | public: true 6 | -------------------------------------------------------------------------------- /Resources/config/default/doctrine-event-resolver.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | xiidea.easy_audit.default_doctrine_event_resolver: 4 | class: Xiidea\EasyAuditBundle\Resolver\DoctrineObjectEventResolver 5 | public: true 6 | calls: 7 | - [ setDoctrine,[ '@doctrine' ] ] 8 | -------------------------------------------------------------------------------- /Resources/doc/override-resolver.md: -------------------------------------------------------------------------------- 1 | # Override Default Resolver 2 | 3 | You could override your default resolver with [creating](./custom-resolver.md) Your own custom resolver. Then configure as follow to use the custom.event_resolver.service as your default resolver 4 | 5 | ```yaml 6 | xiidea_easy_audit: 7 | #resolver: xiidea.easy_audit.default_event_resolver 8 | ``` 9 | -------------------------------------------------------------------------------- /Exception/InvalidServiceException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Exception; 13 | 14 | class InvalidServiceException extends \Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /Resolver/EmbeddedEventResolverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver; 13 | 14 | interface EmbeddedEventResolverInterface 15 | { 16 | public function getEventLogInfo($eventName); 17 | } 18 | -------------------------------------------------------------------------------- /Exception/UnrecognizedEventInfoException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Exception; 13 | 14 | class UnrecognizedEventInfoException extends \Exception 15 | { 16 | protected $message = 'Unrecognized Event info'; 17 | } 18 | -------------------------------------------------------------------------------- /Exception/UnrecognizedEntityException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Exception; 13 | 14 | class UnrecognizedEntityException extends \Exception 15 | { 16 | protected $message = 'Entity must extend Xiidea\\EasyAuditBundle\\Entity\\BaseAuditLog'; 17 | } 18 | -------------------------------------------------------------------------------- /Logger/LoggerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Logger; 13 | 14 | use Xiidea\EasyAuditBundle\Model\BaseAuditLog; 15 | 16 | interface LoggerInterface 17 | { 18 | /** 19 | * @param BaseAuditLog $event 20 | */ 21 | public function log(BaseAuditLog $event); 22 | } 23 | -------------------------------------------------------------------------------- /Resources/config/default/logger.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | xiidea.easy_audit.logger.service: 4 | class: Xiidea\EasyAuditBundle\Logger\Logger 5 | arguments: ['@doctrine'] 6 | tags: 7 | - { name: easy_audit.logger } 8 | 9 | xiidea.easy_audit.entity_delete_event.subscriber: 10 | class: Xiidea\EasyAuditBundle\Subscriber\DoctrineDeleteEventLogger 11 | arguments: ['@xiidea.easy_audit.logger.service'] 12 | public: true 13 | tags: 14 | - { name: kernel.event_subscriber } 15 | -------------------------------------------------------------------------------- /Subscriber/EasyAuditEventSubscriberInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Subscriber; 13 | 14 | interface EasyAuditEventSubscriberInterface 15 | { 16 | /** 17 | * Returns an array of events this subscriber wants to listen to. 18 | * 19 | * @return array 20 | */ 21 | public function getSubscribedEvents(): array; 22 | } 23 | -------------------------------------------------------------------------------- /Resolver/EventResolverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver; 13 | 14 | use Symfony\Contracts\EventDispatcher\Event; 15 | 16 | interface EventResolverInterface 17 | { 18 | /** 19 | * @param Event $event 20 | * @param string $eventName 21 | * 22 | * @return mixed 23 | */ 24 | public function getEventLogInfo(Event $event, $eventName); 25 | } 26 | -------------------------------------------------------------------------------- /Resources/doc/custom-logger.md: -------------------------------------------------------------------------------- 1 | # Custom Logger 2 | 3 | You could easily create your own logger suitable to your needs. The Change Required to use your Logger is very simple. Just follow the steps: 4 | 5 | ### 1. Write Your LoggerClass 6 | 7 | ```php 8 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver\UserEventCommand; 13 | 14 | abstract class UserLoginCommand extends ResolverCommand 15 | { 16 | /** 17 | * @return string 18 | */ 19 | #[\Override] 20 | public function getType() 21 | { 22 | return 'User Logged in'; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | #[\Override] 29 | public function getTemplate() 30 | { 31 | return "User '%s' Logged in Successfully"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Attribute/SubscribeDoctrineEvents.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Attribute; 13 | 14 | /** 15 | * Attribute for ORM Subscribed Event. 16 | * 17 | * @author Roni Saha 18 | */ 19 | #[\Attribute(\Attribute::TARGET_CLASS)] 20 | class SubscribeDoctrineEvents 21 | { 22 | public array $events = []; 23 | 24 | public function __construct(array|string $values) 25 | { 26 | $this->events = is_array($values) ? $values : array_map('trim', explode(',', $values)); 27 | 28 | $this->events = array_filter($this->events); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Resolver/DefaultEventResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver; 13 | 14 | use Symfony\Contracts\EventDispatcher\Event; 15 | 16 | class DefaultEventResolver implements EventResolverInterface 17 | { 18 | /** 19 | * @param Event $event 20 | * @param $eventName 21 | * 22 | * @return array 23 | */ 24 | #[\Override] 25 | public function getEventLogInfo(Event $event, $eventName) 26 | { 27 | return array( 28 | 'description' => $eventName, 29 | 'type' => $eventName, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Traits/EntityHydrationMethod.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Traits; 13 | 14 | use Symfony\Component\PropertyAccess\PropertyAccess; 15 | 16 | trait EntityHydrationMethod 17 | { 18 | final public function fromArray($data = array()) 19 | { 20 | $accessor = PropertyAccess::createPropertyAccessor(); 21 | 22 | foreach ($data as $property => $value) { 23 | if ($accessor->isWritable($this, $property)) { 24 | $accessor->setValue($this, $property, $value); 25 | } 26 | } 27 | 28 | return $this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Resolver/UserEventCommand/InteractiveLoginCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver\UserEventCommand; 13 | 14 | use Xiidea\EasyAuditBundle\Common\UserAwareComponent; 15 | 16 | class InteractiveLoginCommand extends UserLoginCommand 17 | { 18 | public function __construct(private UserAwareComponent $userAwareComponent) 19 | { 20 | } 21 | 22 | /** 23 | * @param $event 24 | * 25 | * @return mixed 26 | */ 27 | #[\Override] 28 | public function resolve($event) 29 | { 30 | return $this->getEventDetailsArray($this->userAwareComponent->getUsername()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Resources/doc/logger-channel.md: -------------------------------------------------------------------------------- 1 | # Logger channel 2 | 3 | It is now possible to register logger for specific channel. channel is refers to log level. channel can be inclusive or exclusive. to define exclusive list add `!` sign before it. 4 | Let assume you have two logger registered. you want `xiidea.easy_audit.logger.service` service to log only "info" and "debug" level events. Wheres rest of the events you wants to be logged by `file.logger` service. 5 | Then you can configure the logger channel as bellow: 6 | 7 | ```yaml 8 | xiidea_easy_audit: 9 | 10 | logger_channel: 11 | xiidea.easy_audit.logger.service: ["info", "debug"] 12 | file.logger: ["!info", "!debug"] 13 | ``` 14 | 15 | ## Notes: 16 | 17 | - If no channel configured for a logger service, it will log all event 18 | - You can define either inclusive or exclusive list but not both for a logger. `file.logger: ["!info", "debug"]` is an invalid configuration 19 | -------------------------------------------------------------------------------- /Listener/LogEventsListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Listener; 13 | 14 | use Symfony\Contracts\EventDispatcher\Event; 15 | use Xiidea\EasyAuditBundle\Logger\LoggerFactory; 16 | use Xiidea\EasyAuditBundle\Resolver\EventResolverFactory; 17 | 18 | class LogEventsListener 19 | { 20 | public function __construct(private LoggerFactory $loggerFactory, private EventResolverFactory $resolverFactory) 21 | { 22 | } 23 | 24 | public function resolveEventHandler(Event $event, $eventName) 25 | { 26 | $eventInfo = $this->resolverFactory->getEventLog($event, $eventName); 27 | $this->loggerFactory->executeLoggers($eventInfo); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Resources/config/doctrine_services.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | xiidea.easy_audit.doctrine_subscriber: 4 | class: Xiidea\EasyAuditBundle\Subscriber\DoctrineSubscriber 5 | arguments: ['%xiidea.easy_audit.doctrine_objects%'] 6 | calls: 7 | - [ setDispatcher,[ '@event_dispatcher' ] ] 8 | tags: 9 | - { name: doctrine.event_listener, event: preRemove } 10 | - { name: doctrine.event_listener, event: postUpdate } 11 | - { name: doctrine.event_listener, event: postRemove } 12 | - { name: doctrine.event_listener, event: postPersist } 13 | - { name: doctrine_mongodb.odm.event_listener, event: preRemove } 14 | - { name: doctrine_mongodb.odm.event_listener, event: postUpdate } 15 | - { name: doctrine_mongodb.odm.event_listener, event: postRemove } 16 | - { name: doctrine_mongodb.odm.event_listener, event: postPersist } 17 | -------------------------------------------------------------------------------- /Events/DoctrineObjectEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the MIT license that is bundled 8 | * with this source code in the file LICENSE. 9 | */ 10 | 11 | namespace Xiidea\EasyAuditBundle\Events; 12 | 13 | use Doctrine\Persistence\Event\LifecycleEventArgs; 14 | use Symfony\Contracts\EventDispatcher\Event; 15 | 16 | class DoctrineObjectEvent extends Event 17 | { 18 | public function __construct(private LifecycleEventArgs $lifecycleEventArgs, private $identity) 19 | { 20 | } 21 | 22 | /** 23 | * @return LifecycleEventArgs 24 | */ 25 | public function getLifecycleEventArgs() 26 | { 27 | return $this->lifecycleEventArgs; 28 | } 29 | 30 | /** 31 | * @return mixed 32 | */ 33 | public function getIdentity() 34 | { 35 | return $this->identity; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Resources/doc/embed-resolver.md: -------------------------------------------------------------------------------- 1 | # Embed Resolver with event 2 | 3 | Sometime it is easy if you could embed your resolver with your event itself. Easy audit also support such implementation. What you need to do just write your event class implementing `Xiidea\EasyAuditBundle\Resolver\EmbeddedEventResolverInterface` 4 | 5 | ```php 6 | data = $data; 18 | } 19 | 20 | 21 | public function getData() 22 | { 23 | return $this->data; 24 | } 25 | 26 | public function getEventLogInfo($eventName) 27 | { 28 | return array( 29 | 'description'=>'Embeded Event description', 30 | 'type'=>$eventName 31 | ); 32 | } 33 | 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/MonologLoggerPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 16 | 17 | class MonologLoggerPass implements CompilerPassInterface 18 | { 19 | #[\Override] 20 | public function process(ContainerBuilder $container): void 21 | { 22 | if (false === $container->hasAlias('logger')) { 23 | $container->removeDefinition('xiidea.easy_audit.mono_logger.service'); 24 | return; 25 | } 26 | 27 | $definition = $container->getDefinition('xiidea.easy_audit.mono_logger.service'); 28 | 29 | $definition->setPublic(true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Resolver/UserEventCommand/ResolverCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver\UserEventCommand; 13 | 14 | abstract class ResolverCommand 15 | { 16 | /** 17 | * @param string $username 18 | * 19 | * @return array 20 | */ 21 | protected function getEventDetailsArray($username) 22 | { 23 | return array( 24 | 'type' => $this->getType(), 25 | 'description' => sprintf($this->getTemplate(), $username), 26 | ); 27 | } 28 | 29 | /** 30 | * @param \Symfony\Contracts\EventDispatcher\Event $event 31 | * 32 | * @return mixed 33 | */ 34 | abstract public function resolve($event); 35 | 36 | /** 37 | * @return string 38 | */ 39 | abstract public function getType(); 40 | 41 | /** 42 | * @return string 43 | */ 44 | abstract public function getTemplate(); 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Xiidea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Events/DoctrineEvents.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Events; 13 | 14 | class DoctrineEvents 15 | { 16 | private static $prefix = 'easy_audit.doctrine.object.'; 17 | 18 | const ENTITY_UPDATED = 'easy_audit.doctrine.object.updated'; 19 | const ENTITY_CREATED = 'easy_audit.doctrine.object.created'; 20 | const ENTITY_DELETED = 'easy_audit.doctrine.object.deleted'; 21 | 22 | /** 23 | * @param string $eventName 24 | * 25 | * @return string 26 | */ 27 | public static function getShortEventType($eventName) 28 | { 29 | return str_replace(self::$prefix, '', $eventName); 30 | } 31 | 32 | /** 33 | * @return array 34 | * 35 | * @throws \ReflectionException 36 | */ 37 | public static function getConstants() 38 | { 39 | $oClass = new \ReflectionClass(__CLASS__); 40 | return $oClass->getConstants(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /XiideaEasyAuditBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle; 13 | 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\HttpKernel\Bundle\Bundle; 16 | use Xiidea\EasyAuditBundle\DependencyInjection\Compiler\LoggerFactoryPass; 17 | use Xiidea\EasyAuditBundle\DependencyInjection\Compiler\MonologLoggerPass; 18 | use Xiidea\EasyAuditBundle\DependencyInjection\Compiler\ResolverFactoryPass; 19 | use Xiidea\EasyAuditBundle\DependencyInjection\Compiler\SubscriberPass; 20 | 21 | class XiideaEasyAuditBundle extends Bundle 22 | { 23 | #[\Override] 24 | public function build(ContainerBuilder $container): void 25 | { 26 | parent::build($container); 27 | 28 | $container->addCompilerPass(new MonologLoggerPass()); 29 | $container->addCompilerPass(new LoggerFactoryPass()); 30 | $container->addCompilerPass(new SubscriberPass()); 31 | $container->addCompilerPass(new ResolverFactoryPass()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Subscriber/DoctrineDeleteEventLogger.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Subscriber; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\HttpKernel\KernelEvents; 16 | use Xiidea\EasyAuditBundle\Logger\Logger; 17 | use Symfony\Component\Console\ConsoleEvents; 18 | 19 | class DoctrineDeleteEventLogger implements EventSubscriberInterface 20 | { 21 | /** 22 | * DoctrineDeleteEventLogger constructor. 23 | * 24 | * @param Logger $logger 25 | */ 26 | public function __construct(private Logger $logger) 27 | { 28 | } 29 | 30 | public function savePendingLogs() 31 | { 32 | $this->logger->savePendingLogs(); 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | #[\Override] 39 | public static function getSubscribedEvents(): array 40 | { 41 | return [ 42 | ConsoleEvents::TERMINATE => 'savePendingLogs', 43 | KernelEvents::TERMINATE => 'savePendingLogs', 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Resources/doc/pre-persist-listener.md: -------------------------------------------------------------------------------- 1 | # Pre-Persist Listener 2 | 3 | When using the default **doctrine logger**, you may want to add customized data to the AuditLog object. You can do it by defining a doctrine Pre-Persist listener service like bellow: 4 | 5 | #### Note: This appraoch is sugested only if you are using the doctrine logger. Because the extra data you set here would not be available to other loggers. In that case overriding the resolver should be your choice. 6 | 7 | ### 1. Write Your Listener Class 8 | 9 | ```php 10 | getEntity(); 24 | 25 | if ($entity instanceof BaseAuditLog) { 26 | //Do your extra processing 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | ### 2. Define your Listener as service 33 | 34 | ```yaml 35 | services: 36 | xiidea.easy_audit.prepersist_listener: 37 | class: App\Listener\AuditLogPrePersistListener 38 | tags: 39 | - { name: doctrine.event_listener, event: prePersist } 40 | ``` 41 | -------------------------------------------------------------------------------- /Resources/doc/custom-resolver.md: -------------------------------------------------------------------------------- 1 | # Custom Resolver For Specific Event 2 | 3 | You could easily customize CustomEventResolver with a version adapted to your needs. The Change Required to use your Resolver is very simple. Just follow the steps: 4 | 5 | ### 1. Write Your ResolverClass 6 | 7 | ```php 8 | 'Custom description', 23 | 'type'=>$eventName 24 | ); 25 | } 26 | 27 | } 28 | ``` 29 | 30 | ### 2. Define your resolver as service 31 | 32 | ```yaml 33 | services: 34 | custom.event_resolver.service: 35 | class: App\Resolver\CustomEventResolver 36 | ``` 37 | 38 | ### 3. Use this resolver for specific event(s) 39 | 40 | You can now use this resolver for specific event by setting following configuration 41 | 42 | ```yaml 43 | xiidea_easy_audit: 44 | custom_resolvers : 45 | security.interactive_login : custom.event_resolver.service 46 | fos_user.security.implicit_login : custom.event_resolver.service 47 | ``` 48 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/LoggerFactoryPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Reference; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 17 | 18 | class LoggerFactoryPass implements CompilerPassInterface 19 | { 20 | #[\Override] 21 | public function process(ContainerBuilder $container): void 22 | { 23 | if (false === $container->hasDefinition('xiidea.easy_audit.logger_factory')) { 24 | return; 25 | } 26 | 27 | $definition = $container->getDefinition('xiidea.easy_audit.logger_factory'); 28 | 29 | $calls = $definition->getMethodCalls(); 30 | $definition->setMethodCalls(array()); 31 | 32 | foreach ($container->findTaggedServiceIds('easy_audit.logger') as $id => $attributes) { 33 | $definition->addMethodCall('addLogger', array($id, new Reference($id))); 34 | } 35 | 36 | $definition->setMethodCalls(array_merge($definition->getMethodCalls(), $calls)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | 4 | xiidea.easy_audit.logger_factory: 5 | class: Xiidea\EasyAuditBundle\Logger\LoggerFactory 6 | public: false 7 | arguments : [ '%xiidea.easy_audit.logger_channel%' ] 8 | calls: 9 | - [ setDebug,[ '%kernel.debug%' ] ] 10 | 11 | xiidea.easy_audit.event_resolver_factory: 12 | class: Xiidea\EasyAuditBundle\Resolver\EventResolverFactory 13 | public: false 14 | arguments : [ '%xiidea.easy_audit.custom_resolvers%', '%xiidea.easy_audit.user_property%', '%xiidea.easy_audit.audit_log_class%' ] 15 | calls: 16 | - [ setAuthChecker,[ '@security.authorization_checker' ] ] 17 | - [ setRequestStack,[ '@request_stack' ] ] 18 | - [ setTokenStorage,[ '@security.token_storage' ] ] 19 | - [ setDebug,[ '%kernel.debug%' ] ] 20 | 21 | xiidea.easy_audit.event_listener: 22 | class: Xiidea\EasyAuditBundle\Listener\LogEventsListener 23 | arguments: ['@xiidea.easy_audit.logger_factory', '@xiidea.easy_audit.event_resolver_factory'] 24 | 25 | xiidea.easy_audit.mono_logger.service: 26 | class: Xiidea\EasyAuditBundle\Logger\MonologLogger 27 | arguments: ['@logger'] 28 | public: false, 29 | tags: 30 | - { name: easy_audit.logger } 31 | - { name: monolog.logger, channel: easy_audit } -------------------------------------------------------------------------------- /Resources/doc/subscriber.md: -------------------------------------------------------------------------------- 1 | # Define events with subscriber 2 | 3 | You can now expose/define your loggable events from your bundle using event subscriber instead of defining in configuration file. What you need to do, define a easy_audit.event_subscriber service implementing `Xiidea\EasyAuditBundle\Subscriber\EasyAuditEventSubscriberInterface` 4 | 5 | ### 1. Write Your AuditLogEventSubscriber class 6 | 7 | ```php 8 | "some_event", 21 | "some_other_resolver" => array( 22 | "event_for_other_resolver_1", 23 | "event_for_other_resolver_2" 24 | ), 25 | "event_for_default_resolver_1", 26 | "event_for_default_resolver_2", 27 | "event_for_default_resolver_3" 28 | ); 29 | } 30 | 31 | } 32 | ``` 33 | 34 | ### 2. Define Subscriber as service 35 | 36 | ```yaml 37 | services: 38 | class: App\Subscriber\MyAuditLogEventSubscriber 39 | tags: 40 | - { name: easy_audit.event_subscriber } 41 | ``` 42 | 43 | If you want you can optionally define the resolver for the subscribed events like: 44 | 45 | ```yaml 46 | services: 47 | class: App\Subscriber\MyAuditLogEventSubscriber 48 | tags: 49 | - { name: easy_audit.event_subscriber, resolver : your_resolver_service_id } 50 | ``` 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xiidea/easy-audit", 3 | "type": "symfony-bundle", 4 | "description": "A Symfony Bundle To Log Selective Events. It is easy to configure and easy to customize for your need", 5 | "keywords": ["Event Log", "Audit Log", "Symfony2", "Symfony3", "Symfony4", "Symfony5", "Symfony6", "Audit trail", "PSR-3"], 6 | "license": "MIT", 7 | "homepage": "http://xiidea.github.io/EasyAuditBundle/", 8 | "authors": [ 9 | { 10 | "name": "Roni Saha", 11 | "email": "roni.cse@gmail.com", 12 | "role": "Developer" 13 | }, 14 | { 15 | "name": "EasyAuditBundle Community", 16 | "homepage": "https://github.com/xiidea/EasyAuditBundle/contributors" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/xiidea/EasyAuditBundle/issues", 21 | "source": "https://github.com/xiidea/EasyAuditBundle" 22 | }, 23 | "require": { 24 | "php": ">=8.1", 25 | "psr/log": "^1|^2|^3", 26 | "symfony/framework-bundle": ">=5.4 <8.0", 27 | "symfony/security-bundle": ">=5.4 <8.0" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^9.5", 31 | "php-coveralls/php-coveralls": "^2.1", 32 | "doctrine/common": ">=2.2 <4.0", 33 | "symfony/test-pack": "^1.0", 34 | "symfony/twig-bundle": ">=5.4 <8.0", 35 | "symfony/form": ">=5.4 <8.0", 36 | "symfony/validator": ">=5.4 <8.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Xiidea\\EasyAuditBundle\\": "" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { "Xiidea\\EasyAuditBundle\\Tests\\": "Tests/" } 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "3.0.x-dev" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Resources/config/config-sample.yml: -------------------------------------------------------------------------------- 1 | # Xiidea Easy Audit Configuration Sample 2 | 3 | xiidea_easy_audit: 4 | #resolver: xiidea.easy_audit.default_event_resolver #Optional 5 | #audit_log_class : MyProject\Bundle\MyBundle\Entity\AuditLog #Required 6 | #doctrine_event_resolver : xiidea.easy_audit.default_doctrine_event_resolver #Optional 7 | #default_logger : true #Optional 8 | 9 | #user property to use as actor of an event 10 | #valid value will be any valid property of your user class ~ 11 | user_property : ~ # or username #Required 12 | 13 | #List of doctrine entity:event you wish to track or set to false to disable logs for doctrine events 14 | #doctrine_objects : #Optional 15 | # MyProject\Bundle\MyBundle\Entity\MyEntity : [created, updated, deleted] 16 | # MyProject\Bundle\MyBundle\Entity\MyEntity2 : [] 17 | 18 | #List all events you want to track (Optional from v1.2.1 you can now use subscriber to define it) 19 | events : #Optional 20 | - security.interactive_login 21 | - security.authentication.failure : user.event_resolver 22 | #- //More event 23 | 24 | logger_channel: 25 | xiidea.easy_audit.logger.service: ["info", "debug"] 26 | file.logger: ["!info", "!debug"] 27 | 28 | #Custom Event Resolver Service 29 | #services: 30 | #user.event_resolver: 31 | #class: Xiidea\EasyAuditBundle\Resolver\UserEventResolver 32 | #calls: 33 | #- [ setAuthChecker,[ '@security.authorization_checker' ] ] 34 | #- [ setRequestStack,[ '@request_stack' ] ] 35 | #- [ setRequestStack,[ '@security.token_storage' ] ] 36 | -------------------------------------------------------------------------------- /Logger/Logger.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Logger; 13 | 14 | use Doctrine\Persistence\ManagerRegistry; 15 | use Doctrine\Persistence\ObjectManager; 16 | use Xiidea\EasyAuditBundle\Model\BaseAuditLog as AuditLog; 17 | use Xiidea\EasyAuditBundle\Events\DoctrineEvents; 18 | 19 | class Logger implements LoggerInterface 20 | { 21 | private $entityDeleteLogs = []; 22 | 23 | public function __construct(private ManagerRegistry $doctrine) 24 | { 25 | } 26 | 27 | #[\Override] 28 | public function log(AuditLog $event = null) 29 | { 30 | if (empty($event)) { 31 | return; 32 | } 33 | 34 | if (DoctrineEvents::ENTITY_DELETED === $event->getTypeId()) { 35 | $this->entityDeleteLogs[] = $event; 36 | 37 | return; 38 | } 39 | 40 | $this->saveLog($event); 41 | } 42 | 43 | /** 44 | * @return ObjectManager 45 | */ 46 | protected function getManager() 47 | { 48 | return $this->getDoctrine()->getManager(); 49 | } 50 | 51 | /** 52 | * @return ManagerRegistry 53 | */ 54 | public function getDoctrine() 55 | { 56 | return $this->doctrine; 57 | } 58 | 59 | /** 60 | * @param AuditLog $event 61 | */ 62 | protected function saveLog(AuditLog $event) 63 | { 64 | $this->getManager()->persist($event); 65 | $this->getManager()->flush($event); 66 | } 67 | 68 | public function savePendingLogs() 69 | { 70 | foreach ($this->entityDeleteLogs as $log) { 71 | $this->saveLog($log); 72 | } 73 | 74 | $this->entityDeleteLogs = []; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Resolver/UserEventResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver; 13 | 14 | use Symfony\Contracts\EventDispatcher\Event; 15 | use Xiidea\EasyAuditBundle\Common\UserAwareComponent; 16 | use Xiidea\EasyAuditBundle\Resolver\UserEventCommand\InteractiveLoginCommand; 17 | use Xiidea\EasyAuditBundle\Resolver\UserEventCommand\ResolverCommand; 18 | 19 | /** Custom Event Resolver Example For FosUserBundle */ 20 | class UserEventResolver extends UserAwareComponent implements EventResolverInterface 21 | { 22 | private $commands = array(); 23 | 24 | private $default; 25 | 26 | public function __construct() 27 | { 28 | $this->commands = array( 29 | 'security.interactive_login' => new InteractiveLoginCommand($this) 30 | ); 31 | } 32 | 33 | /** 34 | * @param Event $event 35 | * @param $eventName 36 | * 37 | * @return array 38 | */ 39 | #[\Override] 40 | public function getEventLogInfo(Event $event, $eventName) 41 | { 42 | $this->default = array( 43 | 'type' => $eventName, 44 | 'description' => $eventName, 45 | ); 46 | 47 | if (!isset($this->commands[$eventName])) { 48 | return $this->default; 49 | } 50 | 51 | return $this->getEventLogDetails($event, $this->commands[$eventName]); 52 | } 53 | 54 | /** 55 | * @param Event $event 56 | * @param ResolverCommand $command 57 | * 58 | * @return array 59 | */ 60 | protected function getEventLogDetails(Event $event, ResolverCommand $command) 61 | { 62 | $details = $command->resolve($event); 63 | 64 | return empty($details) ? $this->default : $details; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Resources/doc/doctrine-entity-events.md: -------------------------------------------------------------------------------- 1 | # Doctrine Object Events 2 | 3 | You can track doctrine object(ORM/MongoDB) events. Currently Supported Events are [created, updated, deleted]. There are two way to achieve this. 4 | 5 | ### 1. By Configuration : 6 | 7 | You can configure to select the entity you like to track as well as the events. If you like to track all events for your entity App\\Entity\\MyEntity just put `App\Entity\MyEntity : ~` 8 | 9 | See the following example configuration value: 10 | 11 | ```yaml 12 | xiidea_easy_audit: 13 | doctrine_objects : #Optional 14 | App\Entity\MyEntity : [updated, deleted] 15 | App\Entity\MyEntity2 : [deleted] 16 | App\Entity\MyEntity3 : ~ 17 | ``` 18 | 19 | ### 2. By Annotation 20 | 21 | You can use annotation to tell XiideaEasyAuditBundle to track events of an entity. 22 | 23 | @SubscribeDoctrineEvents: This annotation lets you define which event you like to track for a doctrine entity: 24 | 25 | ```php 26 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Logger; 13 | 14 | use ReflectionProperty; 15 | use Symfony\Component\PropertyAccess\PropertyAccess; 16 | use Xiidea\EasyAuditBundle\Model\BaseAuditLog as AuditLog; 17 | 18 | class MonologLogger implements LoggerInterface 19 | { 20 | private static $ignoreProperties = array('description', 'id', 'level'); 21 | 22 | public function __construct(private \Psr\Log\LoggerInterface $logger) 23 | { 24 | } 25 | 26 | #[\Override] 27 | public function log(AuditLog $event = null) 28 | { 29 | if (null === $event) { 30 | return; 31 | } 32 | 33 | $this->logger->log($event->getLevel(), $event->getDescription(), $this->getContextArray($event)); 34 | } 35 | 36 | /** 37 | * @param \ReflectionObject $refObject 38 | * 39 | * @return ReflectionProperty[] 40 | */ 41 | protected function getAllProperties(\ReflectionObject $refObject) 42 | { 43 | return $refObject->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_STATIC); 44 | } 45 | 46 | /** 47 | * @param AuditLog $event 48 | * 49 | * @return array 50 | */ 51 | protected function getContextArray(AuditLog $event) 52 | { 53 | $accessor = PropertyAccess::createPropertyAccessor(); 54 | 55 | $refObject = new \ReflectionObject($event); 56 | 57 | $arr = array(); 58 | 59 | foreach ($this->getAllProperties($refObject) as $property) { 60 | if (!$accessor->isReadable($event, $property->getName()) || in_array($property->getName(), self::$ignoreProperties)) { 61 | continue; 62 | } 63 | 64 | $arr[$property->getName()] = $accessor->getValue($event, $property->getName()); 65 | } 66 | 67 | return $arr; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/ResolverFactoryPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Reference; 17 | 18 | class ResolverFactoryPass implements CompilerPassInterface 19 | { 20 | #[\Override] 21 | public function process(ContainerBuilder $container): void 22 | { 23 | if (false === $container->hasDefinition('xiidea.easy_audit.event_resolver_factory')) { 24 | return; 25 | } 26 | 27 | $definition = $container->getDefinition('xiidea.easy_audit.event_resolver_factory'); 28 | 29 | $calls = $definition->getMethodCalls(); 30 | $definition->setMethodCalls(array()); 31 | 32 | foreach ($container->getParameter('xiidea.easy_audit.custom_resolvers') as $id) { 33 | if ($container->hasDefinition($id)) { 34 | $definition->addMethodCall('addCustomResolver', array($id, new Reference($id))); 35 | } 36 | } 37 | 38 | $definition->addMethodCall('setCommonResolver', array( 39 | $this->getServiceReferenceByConfigName($container, 'resolver'), ) 40 | ); 41 | 42 | if (null !== $container->getParameter('xiidea.easy_audit.doctrine_event_resolver')) { 43 | $definition->addMethodCall('setEntityEventResolver', array( 44 | $this->getServiceReferenceByConfigName($container, 'doctrine_event_resolver'), ) 45 | ); 46 | } 47 | 48 | $definition->setMethodCalls(array_merge($definition->getMethodCalls(), $calls)); 49 | } 50 | 51 | /** 52 | * @param ContainerBuilder $container 53 | * @param $configName 54 | * 55 | * @return Reference 56 | */ 57 | protected function getServiceReferenceByConfigName(ContainerBuilder $container, $configName) 58 | { 59 | return new Reference($container->getParameter('xiidea.easy_audit.'.$configName)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Logger/LoggerFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Logger; 13 | 14 | use Xiidea\EasyAuditBundle\Model\BaseAuditLog; 15 | use Xiidea\EasyAuditBundle\Exception\InvalidServiceException; 16 | 17 | class LoggerFactory 18 | { 19 | /** @var LoggerInterface[] */ 20 | private static $loggers = array(); 21 | 22 | private $debug = false; 23 | 24 | public function __construct(private array $loggersChannel = array()) 25 | { 26 | } 27 | 28 | /** 29 | * @param null|\Xiidea\EasyAuditBundle\Model\BaseAuditLog $eventInfo 30 | */ 31 | public function executeLoggers($eventInfo) 32 | { 33 | if (empty($eventInfo)) { 34 | return; 35 | } 36 | 37 | foreach (self::$loggers as $id => $logger) { 38 | if ($this->isValidLoggerForThisEvent($eventInfo, $logger, $id)) { 39 | $logger->log($eventInfo); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * @param string $loggerName 46 | * @param LoggerInterface $logger 47 | * 48 | * @throws InvalidServiceException 49 | */ 50 | public function addLogger($loggerName, $logger) 51 | { 52 | if ($logger instanceof LoggerInterface) { 53 | self::$loggers[$loggerName] = $logger; 54 | } elseif ($this->debug) { 55 | throw new InvalidServiceException('Logger Service must implement'.__NAMESPACE__.'LoggerInterface'); 56 | } 57 | } 58 | 59 | /** 60 | * @param string $id 61 | * @param string $level 62 | * 63 | * @return bool 64 | */ 65 | private function isChannelRegisterWithLogger($id, $level) 66 | { 67 | if (!isset($this->loggersChannel[$id])) { 68 | return true; 69 | } 70 | 71 | if ($this->isChannelTypeOf('inclusive', $id)) { 72 | return $this->levelExistsInList($level, $id); 73 | } 74 | 75 | if ($this->isChannelTypeOf('exclusive', $id)) { 76 | return !$this->levelExistsInList($level, $id); 77 | } 78 | 79 | return false; 80 | } 81 | 82 | /** 83 | * @param string $type 84 | * @param string $id 85 | * 86 | * @return bool 87 | */ 88 | private function isChannelTypeOf($type, $id) 89 | { 90 | return $this->loggersChannel[$id]['type'] === $type; 91 | } 92 | 93 | /** 94 | * @param string $level 95 | * @param string $id 96 | * 97 | * @return bool 98 | */ 99 | private function levelExistsInList($level, $id) 100 | { 101 | return in_array($level, $this->loggersChannel[$id]['elements']); 102 | } 103 | 104 | /** 105 | * @param BaseAuditLog $eventInfo 106 | * @param $logger 107 | * @param $id 108 | * 109 | * @return bool 110 | */ 111 | protected function isValidLoggerForThisEvent(BaseAuditLog $eventInfo, $logger, $id) 112 | { 113 | return $logger instanceof LoggerInterface && $this->isChannelRegisterWithLogger($id, $eventInfo->getLevel()); 114 | } 115 | 116 | public function setDebug(mixed $debug) 117 | { 118 | $this->debug = $debug; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Resources/doc/audit-log-entity-orm.md: -------------------------------------------------------------------------------- 1 | # Step 3: Create audit_log entity mapping 2 | 3 | BaseAuditLog class does not provide ODM/ODM Mapping, 4 | you must create one. This can be done by extending the BaseAuditLog model 5 | provided by the bundle and creating the appropriate mappings. 6 | 7 | For example: 8 | 9 | ### Doctrine ORM Entity Class 10 | 11 | ```php 12 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Loader\LoaderInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\Config\FileLocator; 17 | use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; 18 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 19 | use Symfony\Component\DependencyInjection\Loader; 20 | 21 | /** 22 | * This is the class that loads and manages your bundle configuration. 23 | * 24 | * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} 25 | */ 26 | class XiideaEasyAuditExtension extends Extension implements PrependExtensionInterface 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | #[\Override] 32 | public function load(array $configs, ContainerBuilder $container): void 33 | { 34 | $configuration = new Configuration(); 35 | $config = $this->processConfiguration($configuration, $configs); 36 | 37 | foreach ($config as $key => $value) { 38 | $container->setParameter('xiidea.easy_audit.' . $key, $value); 39 | } 40 | 41 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 42 | $loader->load('services.yml'); 43 | 44 | $this->loadDefaultResolverServices($config, $loader); 45 | 46 | if (false !== $config['doctrine_objects']) { 47 | $loader->load('doctrine_services.yml'); 48 | } 49 | } 50 | 51 | /** 52 | * @param $config 53 | * @param LoaderInterface $loader 54 | * 55 | * @throws \Exception 56 | */ 57 | protected function loadDefaultResolverServices($config, LoaderInterface $loader) 58 | { 59 | if ('xiidea.easy_audit.default_event_resolver' === $config['resolver']) { 60 | $loader->load('default/event-resolver.yml'); 61 | } 62 | 63 | if (true === $config['default_logger']) { 64 | $loader->load('default/logger.yml'); 65 | } 66 | 67 | if (false !== $config['doctrine_objects'] && 'xiidea.easy_audit.default_doctrine_event_resolver' === $config['doctrine_event_resolver']) { 68 | $loader->load('default/doctrine-event-resolver.yml'); 69 | } 70 | } 71 | 72 | /** 73 | * Allow an extension to prepend the extension configurations. 74 | * @param ContainerBuilder $container 75 | */ 76 | #[\Override] 77 | public function prepend(ContainerBuilder $container): void 78 | { 79 | $prependConfig = $this->getExtendedConfig($container); 80 | 81 | if (!empty($prependConfig)) { 82 | $container->prependExtensionConfig($this->getAlias(), $prependConfig); 83 | } 84 | } 85 | 86 | /** 87 | * @param ContainerBuilder $container 88 | * 89 | * @return array 90 | */ 91 | protected function getExtendedConfig(ContainerBuilder $container): array 92 | { 93 | $configs = array_merge(...$container->getExtensionConfig($this->getAlias())); 94 | 95 | $prependConfig = []; 96 | 97 | $doctrineConfig = $container->getExtensionConfig('doctrine'); 98 | 99 | if (!empty($doctrineConfig) && !isset($configs['doctrine_event_resolver'])) { 100 | $prependConfig['doctrine_event_resolver'] = 'xiidea.easy_audit.default_doctrine_event_resolver'; 101 | } 102 | 103 | return $prependConfig; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Common/UserAwareComponent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Common; 13 | 14 | use Symfony\Component\HttpFoundation\RequestStack; 15 | use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; 16 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 17 | use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; 18 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 19 | 20 | class UserAwareComponent 21 | { 22 | /** 23 | * @var TokenStorageInterface 24 | */ 25 | private $tokenStorage; 26 | 27 | /** 28 | * @var AuthorizationCheckerInterface 29 | */ 30 | private $authChecker; 31 | 32 | /** 33 | * @var RequestStack 34 | */ 35 | private $requestStack; 36 | 37 | /** 38 | * @param TokenStorageInterface $tokenStorage 39 | */ 40 | public function setTokenStorage($tokenStorage) 41 | { 42 | $this->tokenStorage = $tokenStorage; 43 | } 44 | 45 | /** 46 | * Get a user from the Security Context. 47 | * 48 | * @return mixed 49 | * 50 | * @throws \LogicException If SecurityBundle is not available 51 | */ 52 | public function getUser() 53 | { 54 | if (null === $token = $this->tokenStorage->getToken()) { 55 | return null; 56 | } 57 | 58 | if (!is_object($user = $token->getUser())) { 59 | return null; 60 | } 61 | 62 | return $user; 63 | } 64 | 65 | /** 66 | * @param AuthorizationCheckerInterface $authChecker 67 | */ 68 | public function setAuthChecker($authChecker) 69 | { 70 | $this->authChecker = $authChecker; 71 | } 72 | 73 | /** 74 | * @param RequestStack $requestStack 75 | */ 76 | public function setRequestStack($requestStack) 77 | { 78 | $this->requestStack = $requestStack; 79 | } 80 | 81 | /** 82 | * @return mixed 83 | */ 84 | final protected function getImpersonatingUser() 85 | { 86 | if (null === $token = $this->tokenStorage->getToken()) { 87 | return null; 88 | } 89 | 90 | if ($this->authChecker->isGranted('IS_IMPERSONATOR')) { 91 | return $this->getImpersonatingUserFromRole($token); 92 | } 93 | 94 | return null; 95 | } 96 | 97 | /** 98 | * @return string 99 | */ 100 | public function getUsername() 101 | { 102 | $user = $this->getUser(); 103 | 104 | if (empty($user)) { 105 | return $this->getAnonymousUserName(); 106 | } 107 | 108 | return $user->getUsername(); 109 | } 110 | 111 | /** 112 | * @return string 113 | */ 114 | protected function getAnonymousUserName() 115 | { 116 | $request = $this->getRequest(); 117 | 118 | if ($request && $request->getClientIp()) { 119 | return 'Anonymous'; 120 | } 121 | 122 | return 'By Command'; 123 | } 124 | 125 | /** 126 | * @param TokenInterface $token 127 | * @param null $user 128 | * 129 | * @return mixed 130 | */ 131 | protected function getImpersonatingUserFromRole($token, $user = null) 132 | { 133 | if ($token instanceof SwitchUserToken) { 134 | $user = $token->getOriginalToken()->getUser(); 135 | } 136 | 137 | return $user; 138 | } 139 | 140 | protected function getRequest() 141 | { 142 | if (null === $this->requestStack) { 143 | return false; 144 | } 145 | 146 | return $this->requestStack->getCurrentRequest(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Model/BaseAuditLog.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Model; 13 | 14 | use Psr\Log\InvalidArgumentException; 15 | use Psr\Log\LogLevel; 16 | use Xiidea\EasyAuditBundle\Traits\EntityHydrationMethod; 17 | 18 | class BaseAuditLog 19 | { 20 | use EntityHydrationMethod; 21 | 22 | /** 23 | * @var string 24 | */ 25 | protected $typeId; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $type; 31 | 32 | /** 33 | * @var string 34 | */ 35 | protected $description; 36 | 37 | /** 38 | * @var \DateTime 39 | */ 40 | protected $eventTime; 41 | 42 | protected $user; 43 | 44 | protected $impersonatingUser; 45 | 46 | /** 47 | * @var string 48 | */ 49 | protected $ip; 50 | 51 | /** 52 | * @var string 53 | */ 54 | protected $level = LogLevel::INFO; 55 | 56 | public function getUser() 57 | { 58 | return $this->user; 59 | } 60 | 61 | public function setUser($user) 62 | { 63 | $this->user = $user; 64 | } 65 | 66 | /** 67 | * @return \DateTime 68 | */ 69 | final public function getEventTime() 70 | { 71 | return $this->eventTime; 72 | } 73 | 74 | /** 75 | * @param \DateTime $eventTime 76 | */ 77 | final public function setEventTime(\DateTime $eventTime) 78 | { 79 | $this->eventTime = $eventTime; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function getType() 86 | { 87 | return $this->type; 88 | } 89 | 90 | /** 91 | * @param string $type 92 | */ 93 | public function setType($type) 94 | { 95 | $this->type = $type; 96 | } 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getDescription() 102 | { 103 | return $this->description; 104 | } 105 | 106 | /** 107 | * @param string $description 108 | */ 109 | public function setDescription($description) 110 | { 111 | $this->description = $description; 112 | } 113 | 114 | /** 115 | * @return string 116 | */ 117 | public function getTypeId() 118 | { 119 | return $this->typeId; 120 | } 121 | 122 | /** 123 | * @param string $typeId 124 | * 125 | * @return $this 126 | */ 127 | public function setTypeId($typeId) 128 | { 129 | $this->typeId = $typeId; 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * @return string 136 | */ 137 | public function getIp() 138 | { 139 | return $this->ip; 140 | } 141 | 142 | /** 143 | * @param string $ip 144 | * 145 | * @return $this 146 | */ 147 | public function setIp($ip) 148 | { 149 | $this->ip = $ip; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @return string 156 | */ 157 | final public function getLevel() 158 | { 159 | return $this->level; 160 | } 161 | 162 | /** 163 | * @param string $level 164 | * 165 | * @return $this 166 | */ 167 | final public function setLevel($level) 168 | { 169 | if (!in_array(strtolower($level), $this->getAllowedLevel())) { 170 | throw new InvalidArgumentException(); 171 | } 172 | 173 | $this->level = $level; 174 | 175 | return $this; 176 | } 177 | 178 | private function getAllowedLevel() 179 | { 180 | $oClass = new \ReflectionClass('Psr\Log\LogLevel'); 181 | 182 | return $oClass->getConstants(); 183 | } 184 | 185 | /** 186 | * @return mixed 187 | */ 188 | public function getImpersonatingUser() 189 | { 190 | return $this->impersonatingUser; 191 | } 192 | 193 | public function setImpersonatingUser(mixed $impersonatingUser) 194 | { 195 | $this->impersonatingUser = $impersonatingUser; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Resolver/DoctrineObjectEventResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver; 13 | 14 | use Doctrine\Persistence\ManagerRegistry; 15 | use Doctrine\Common\Util\ClassUtils; 16 | use Symfony\Contracts\EventDispatcher\Event; 17 | use Xiidea\EasyAuditBundle\Events\DoctrineObjectEvent; 18 | use Xiidea\EasyAuditBundle\Events\DoctrineEvents; 19 | 20 | /** Custom Event Resolver Example Class */ 21 | class DoctrineObjectEventResolver implements EventResolverInterface 22 | { 23 | protected $eventShortName; 24 | 25 | /** @var $event DoctrineObjectEvent */ 26 | protected $event; 27 | 28 | protected $entity; 29 | 30 | protected $eventName; 31 | 32 | protected $identity = ['', '']; 33 | 34 | /** 35 | * @var ManagerRegistry 36 | */ 37 | protected $doctrine; 38 | 39 | protected $changeSetGetterMethods = [ 40 | 'getEntityChangeSet', 41 | 'getDocumentChangeSet', 42 | ]; 43 | 44 | /** 45 | * @param Event|DoctrineObjectEvent $event 46 | * @param $eventName 47 | * 48 | * @return array 49 | * 50 | * @throws \ReflectionException 51 | */ 52 | #[\Override] 53 | public function getEventLogInfo(Event $event, $eventName) 54 | { 55 | if (!$event instanceof DoctrineObjectEvent) { 56 | return null; 57 | } 58 | 59 | $this->initialize($event, $eventName); 60 | 61 | if ($this->isUpdateEvent() && null === $this->getChangeSets($this->entity)) { 62 | return null; 63 | } 64 | 65 | $reflectionClass = $this->getReflectionClassFromObject($this->entity); 66 | 67 | return array( 68 | 'description' => $this->getDescription($reflectionClass->getShortName()), 69 | 'type' => $this->getEventType($reflectionClass->getShortName()), 70 | ); 71 | } 72 | 73 | protected function getSingleIdentity() 74 | { 75 | foreach ($this->event->getIdentity() as $field => $value) { 76 | return [$field, $value]; 77 | } 78 | 79 | return ['', '']; 80 | } 81 | 82 | /** 83 | * @param DoctrineObjectEvent $event 84 | * @param string $eventName 85 | */ 86 | private function initialize(DoctrineObjectEvent $event, $eventName) 87 | { 88 | $this->eventShortName = null; 89 | $this->eventName = $eventName; 90 | $this->event = $event; 91 | $this->entity = $event->getLifecycleEventArgs()->getObject(); 92 | $this->identity = $this->getSingleIdentity(); 93 | } 94 | 95 | private function getIdField() 96 | { 97 | return $this->identity[0]; 98 | } 99 | 100 | private function getIdValue() 101 | { 102 | return $this->identity[1]; 103 | } 104 | 105 | protected function getChangeSets($entity) 106 | { 107 | $unitOfWork = $this->getUnitOfWork(); 108 | foreach ($this->changeSetGetterMethods as $method) { 109 | $getter = [$unitOfWork, $method]; 110 | if (is_callable($getter)) { 111 | return call_user_func($getter, $entity); 112 | } 113 | } 114 | 115 | return null; 116 | } 117 | 118 | protected function isUpdateEvent() 119 | { 120 | return 'updated' === $this->getEventShortName(); 121 | } 122 | 123 | /** 124 | * @param string $typeName 125 | * 126 | * @return string 127 | */ 128 | protected function getEventType($typeName) 129 | { 130 | return $typeName.' '.$this->getEventShortName(); 131 | } 132 | 133 | /** 134 | * @param string $shortName 135 | * 136 | * @return string 137 | */ 138 | protected function getDescription($shortName) 139 | { 140 | return sprintf( 141 | '%s has been %s with %s = "%s"', 142 | $shortName, 143 | $this->getEventShortName(), 144 | $this->getIdField(), 145 | $this->getIdValue() 146 | ); 147 | } 148 | 149 | /** 150 | * @return string 151 | */ 152 | protected function getEventShortName() 153 | { 154 | if (null === $this->eventShortName) { 155 | $this->eventShortName = DoctrineEvents::getShortEventType($this->getName()); 156 | } 157 | 158 | return $this->eventShortName; 159 | } 160 | 161 | /** 162 | * @return string 163 | */ 164 | protected function getName() 165 | { 166 | return $this->eventName; 167 | } 168 | 169 | /** 170 | * @param $object 171 | * 172 | * @return \ReflectionClass 173 | * 174 | * @throws \ReflectionException 175 | */ 176 | protected function getReflectionClassFromObject($object) 177 | { 178 | return new \ReflectionClass(ClassUtils::getClass($object)); 179 | } 180 | 181 | /** 182 | * @return \Doctrine\ODM\MongoDB\UnitOfWork|\Doctrine\ORM\UnitOfWork 183 | */ 184 | protected function getUnitOfWork() 185 | { 186 | return $this->getDoctrine()->getManager()->getUnitOfWork(); 187 | } 188 | 189 | /** 190 | * @return ManagerRegistry|object 191 | */ 192 | protected function getDoctrine() 193 | { 194 | return $this->doctrine; 195 | } 196 | 197 | /** 198 | * @param ManagerRegistry $doctrine 199 | */ 200 | public function setDoctrine($doctrine) 201 | { 202 | $this->doctrine = $doctrine; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Subscriber/DoctrineSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Subscriber; 13 | 14 | use Doctrine\Common\Util\ClassUtils; 15 | use Doctrine\Persistence\Event\LifecycleEventArgs; 16 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 17 | use Xiidea\EasyAuditBundle\Attribute\SubscribeDoctrineEvents; 18 | use Xiidea\EasyAuditBundle\Events\DoctrineEvents; 19 | use Xiidea\EasyAuditBundle\Events\DoctrineObjectEvent; 20 | 21 | class DoctrineSubscriber 22 | { 23 | private array $toBeDeleted = []; 24 | private $dispatcher = null; 25 | 26 | public function __construct(private array $entities = []) 27 | { 28 | } 29 | 30 | public function postPersist(LifecycleEventArgs $args) 31 | { 32 | $this->handleEvent(DoctrineEvents::ENTITY_CREATED, $args); 33 | } 34 | 35 | public function postUpdate(LifecycleEventArgs $args) 36 | { 37 | $this->handleEvent(DoctrineEvents::ENTITY_UPDATED, $args); 38 | } 39 | 40 | public function preRemove(LifecycleEventArgs $args) 41 | { 42 | if (false === $this->isConfiguredToTrack($args->getObject(), DoctrineEvents::ENTITY_DELETED)) { 43 | return; 44 | } 45 | 46 | $className = ClassUtils::getClass($args->getObject()); 47 | 48 | if (!isset($this->toBeDeleted[$className])) { 49 | $this->toBeDeleted[$className] = []; 50 | } 51 | 52 | $this->toBeDeleted[$className][spl_object_hash($args->getObject())] = $this->getIdentity($args, $className); 53 | } 54 | 55 | public function postRemove(LifecycleEventArgs $args) 56 | { 57 | $identity = $this->getToBeDeletedId($args->getObject()); 58 | 59 | if (null !== $identity) { 60 | $this->dispatcher->dispatch(new DoctrineObjectEvent($args, $identity), DoctrineEvents::ENTITY_DELETED); 61 | } 62 | } 63 | 64 | private function getToBeDeletedId($entity) 65 | { 66 | if ($this->isScheduledForDelete($entity)) { 67 | return $this->toBeDeleted[ClassUtils::getClass($entity)][spl_object_hash($entity)]; 68 | } 69 | 70 | return null; 71 | } 72 | 73 | /** 74 | * @param string $eventName 75 | * @param LifecycleEventArgs $args 76 | */ 77 | private function handleEvent($eventName, LifecycleEventArgs $args) 78 | { 79 | if (true === $this->isConfiguredToTrack($args->getObject(), $eventName)) { 80 | $this->dispatcher->dispatch( 81 | new DoctrineObjectEvent($args, $this->getIdentity($args, ClassUtils::getClass($args->getObject()))), 82 | $eventName 83 | ); 84 | } 85 | } 86 | 87 | /** 88 | * @param $entity 89 | * @param string $eventName 90 | * 91 | * @return bool 92 | */ 93 | private function isConfiguredToTrack($entity, $eventName = '') 94 | { 95 | $class = ClassUtils::getClass($entity); 96 | $eventType = DoctrineEvents::getShortEventType($eventName); 97 | 98 | if (null !== $track = $this->isAttributedEvent($entity, $eventType)) { 99 | return $track; 100 | } 101 | 102 | if (!isset($this->entities[$class])) { 103 | return false; 104 | } 105 | 106 | if ($this->shouldTrackAllEventType($class)) { 107 | return true; 108 | } 109 | 110 | return $this->shouldTrackEventType($eventType, $class); 111 | } 112 | 113 | /** 114 | * @param $entity 115 | * @param string $eventType 116 | * @return bool|null 117 | * @throws \ReflectionException 118 | */ 119 | protected function isAttributedEvent($entity, $eventType) 120 | { 121 | $reflection = new \ReflectionClass($entity); 122 | $attribute = $reflection->getAttributes(SubscribeDoctrineEvents::class); 123 | if (empty($attribute)) { 124 | return null; 125 | } 126 | 127 | $instance = $attribute[0]->newInstance(); 128 | 129 | return empty($instance->events) || in_array($eventType, $instance->events); 130 | } 131 | 132 | 133 | /** 134 | * @param string $eventType 135 | * @param string $class 136 | * 137 | * @return bool 138 | */ 139 | private function shouldTrackEventType($eventType, $class) 140 | { 141 | return is_array($this->entities[$class]) && in_array($eventType, $this->entities[$class]); 142 | } 143 | 144 | /** 145 | * @param string $class 146 | * 147 | * @return bool 148 | */ 149 | private function shouldTrackAllEventType($class) 150 | { 151 | return empty($this->entities[$class]); 152 | } 153 | 154 | /** 155 | * @param LifecycleEventArgs $args 156 | * @param $className 157 | * 158 | * @return array 159 | */ 160 | protected function getIdentity(LifecycleEventArgs $args, $className) 161 | { 162 | return $args->getObjectManager()->getClassMetadata($className)->getIdentifierValues($args->getObject()); 163 | } 164 | 165 | /** 166 | * @param $entity 167 | * 168 | * @return bool 169 | */ 170 | private function isScheduledForDelete($entity) 171 | { 172 | $originalClassName = ClassUtils::getClass($entity); 173 | 174 | return isset($this->toBeDeleted[$originalClassName]) && isset( 175 | $this->toBeDeleted[$originalClassName][spl_object_hash( 176 | $entity 177 | )] 178 | ); 179 | } 180 | 181 | /** 182 | * @param EventDispatcherInterface $dispatcher 183 | */ 184 | public function setDispatcher($dispatcher) 185 | { 186 | $this->dispatcher = $dispatcher; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/SubscriberPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 16 | use Xiidea\EasyAuditBundle\Events\DoctrineEvents; 17 | 18 | class SubscriberPass implements CompilerPassInterface 19 | { 20 | #[\Override] 21 | public function process(ContainerBuilder $container): void 22 | { 23 | if (false === $container->hasDefinition('xiidea.easy_audit.event_listener')) { 24 | return; 25 | } 26 | 27 | $this->initializeSubscriberEvents($container); 28 | } 29 | 30 | private function initializeSubscriberEvents(ContainerBuilder $container) 31 | { 32 | $eventsList = $container->getParameter('xiidea.easy_audit.events'); 33 | 34 | $this->appendDoctrineEventsToList($container, $eventsList); 35 | $this->appendSubscribedEventsToList($container, $eventsList); 36 | 37 | $this->registerEventsToListener($eventsList, $container); 38 | } 39 | 40 | private function appendDoctrineEventsToList(ContainerBuilder $container, &$events = array()) 41 | { 42 | if (false === $container->getParameter('xiidea.easy_audit.doctrine_objects')) { 43 | return; 44 | } 45 | 46 | foreach (DoctrineEvents::getConstants() as $constant) { 47 | array_push($events, $constant); 48 | } 49 | } 50 | 51 | /** 52 | * @param ContainerBuilder $container 53 | * @param array $events 54 | */ 55 | private function appendSubscribedEventsToList(ContainerBuilder $container, &$events = array()) 56 | { 57 | $taggedSubscribers = $container->findTaggedServiceIds('easy_audit.event_subscriber'); 58 | 59 | if (empty($taggedSubscribers)) { 60 | return; 61 | } 62 | 63 | foreach ($taggedSubscribers as $id => $attributes) { 64 | $this->appendEventsFromSubscriber($container, $events, $id, 65 | $this->getResolverFromConfigurationAttributes($attributes) 66 | ); 67 | } 68 | } 69 | 70 | /** 71 | * @param $attributes 72 | */ 73 | private function getResolverFromConfigurationAttributes($attributes) 74 | { 75 | return isset($attributes[0]) && isset($attributes[0]['resolver']) ? $attributes[0]['resolver'] : null; 76 | } 77 | 78 | /** 79 | * @param ContainerBuilder $container 80 | * @param $events 81 | * @param $id 82 | * @param $defaultResolver 83 | */ 84 | private function appendEventsFromSubscriber(ContainerBuilder $container, &$events, $id, $defaultResolver = null) 85 | { 86 | $subscriber = $container->get($id); 87 | 88 | $subscribedEvents = $subscriber->getSubscribedEvents(); 89 | 90 | foreach ($subscribedEvents as $key => $item) { 91 | $resolver = !empty($defaultResolver) && !is_string($key) ? $defaultResolver : $key; 92 | $this->addEventFromSubscriber($events, $item, $resolver); 93 | } 94 | } 95 | 96 | /** 97 | * @param $events 98 | * @param $item 99 | * @param $resolver 100 | */ 101 | private function addEventFromSubscriber(&$events, $item, $resolver) 102 | { 103 | $items = array($item); 104 | foreach ($items as $value) { 105 | $this->appendEventArray($events, $resolver, $value); 106 | } 107 | } 108 | 109 | /** 110 | * @param $events 111 | * @param $resolver 112 | * @param $item 113 | */ 114 | private function appendEventArray(&$events, $resolver, $item) 115 | { 116 | if ($this->isEventWithResolver($resolver)) { 117 | $this->appendEventWithResolver($events, $item, $resolver); 118 | return; 119 | } 120 | 121 | array_push($events, $item); 122 | } 123 | 124 | /** 125 | * @param $resolver 126 | * 127 | * @return bool 128 | */ 129 | private function isEventWithResolver($resolver) 130 | { 131 | return is_string($resolver); 132 | } 133 | 134 | /** 135 | * @param $events 136 | * @param $items 137 | * @param $key 138 | * 139 | * @internal param $event 140 | */ 141 | private function appendEventWithResolver(&$events, $items, $key) 142 | { 143 | $items = (array) $items; 144 | 145 | foreach ($items as $item) { 146 | array_push($events, array($item => $key)); 147 | } 148 | } 149 | 150 | /** 151 | * @param $events 152 | * @param ContainerBuilder $container 153 | */ 154 | private function registerEventsToListener($events, ContainerBuilder $container) 155 | { 156 | if (empty($events)) { 157 | return; 158 | } 159 | 160 | $definition = $container->getDefinition('xiidea.easy_audit.event_listener'); 161 | $customResolvers = $container->getParameter('xiidea.easy_audit.custom_resolvers'); 162 | 163 | $listenableEventsList = $this->getListenableEventList($events); 164 | $this->buildCustomResolverList($events, $customResolvers); 165 | 166 | $definition->setTags(array('kernel.event_listener' => array_values($listenableEventsList))); 167 | $container->setParameter('xiidea.easy_audit.custom_resolvers', $customResolvers); 168 | } 169 | 170 | /** 171 | * @param $events 172 | * 173 | * @return array 174 | */ 175 | private function getListenableEventList($events) 176 | { 177 | $eventList = array(); 178 | 179 | foreach ($events as $item) { 180 | $event = is_array($item) ? key($item) : $item; 181 | 182 | $eventList[$event] = array( 183 | 'event' => $event, 184 | 'method' => 'resolveEventHandler', 185 | ); 186 | } 187 | 188 | return $eventList; 189 | } 190 | 191 | private function buildCustomResolverList($events, &$customResolver) 192 | { 193 | foreach ($events as $item) { 194 | if (is_array($item)) { 195 | $event = key($item); 196 | $resolver = $item[$event]; 197 | $customResolver[$event] = $resolver; 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 15 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 16 | use Symfony\Component\Config\Definition\ConfigurationInterface; 17 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; 18 | 19 | /** 20 | * This is the class that validates and merges configuration from your app/config files. 21 | * 22 | * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} 23 | */ 24 | class Configuration implements ConfigurationInterface 25 | { 26 | const ROOT_NODE_NAME = 'xiidea_easy_audit'; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | #[\Override] 32 | public function getConfigTreeBuilder(): TreeBuilder 33 | { 34 | $treeBuilder = new TreeBuilder(self::ROOT_NODE_NAME); 35 | 36 | $rootNode = $treeBuilder->getRootNode(); 37 | 38 | $this->addRequiredConfigs($rootNode); 39 | $this->addDefaultServices($rootNode); 40 | $this->addOptionalConfigs($rootNode); 41 | $this->addChannelHandlers($rootNode); 42 | 43 | return $treeBuilder; 44 | } 45 | 46 | /** 47 | * @param ArrayNodeDefinition $rootNode 48 | */ 49 | private function addRequiredConfigs(ArrayNodeDefinition $rootNode) 50 | { 51 | $rootNode 52 | ->children() 53 | ->scalarNode('user_property')->isRequired()->end() 54 | ->scalarNode('audit_log_class')->cannotBeOverwritten()->isRequired()->cannotBeEmpty()->end() 55 | ->end(); 56 | } 57 | 58 | /** 59 | * @param ArrayNodeDefinition $rootNode 60 | */ 61 | private function addDefaultServices(ArrayNodeDefinition $rootNode) 62 | { 63 | $rootNode 64 | ->children() 65 | ->scalarNode('resolver')->defaultValue('xiidea.easy_audit.default_event_resolver')->end() 66 | ->scalarNode('doctrine_event_resolver') 67 | ->defaultValue(null) 68 | ->end() 69 | ->booleanNode('default_logger')->defaultValue(true)->end() 70 | ->end(); 71 | } 72 | 73 | /** 74 | * @param ArrayNodeDefinition $rootNode 75 | */ 76 | private function addOptionalConfigs(ArrayNodeDefinition $rootNode) 77 | { 78 | $rootNode 79 | ->children() 80 | ->variableNode('doctrine_objects') 81 | ->defaultValue(array()) 82 | ->end() 83 | ->variableNode('events')->defaultValue(array())->end() 84 | ->variableNode('custom_resolvers')->defaultValue(array())->end() 85 | ->end(); 86 | } 87 | 88 | /** 89 | * @param ArrayNodeDefinition $rootNode 90 | */ 91 | private function addChannelHandlers(ArrayNodeDefinition $rootNode) 92 | { 93 | $rootNode 94 | ->fixXmlConfig('loggerChannel') 95 | ->children() 96 | ->arrayNode('logger_channel') 97 | ->canBeUnset() 98 | ->useAttributeAsKey('name') 99 | ->prototype('array') 100 | ->fixXmlConfig('channel', 'elements') 101 | ->canBeUnset() 102 | ->beforeNormalization()->ifString()->then($this->changeToArrayFromString())->end() 103 | ->beforeNormalization()->ifTrue($this->isIndexedArray())->then($this->changeToAssoc())->end() 104 | ->validate()->ifTrue($this->isEmpty())->thenUnset()->end() 105 | ->validate()->always($this->getChannelTypeValidator())->end() 106 | ->children() 107 | ->scalarNode('type')->validate() 108 | ->ifNotInArray(array('inclusive', 'exclusive')) 109 | ->thenInvalid('The type of channels has to be inclusive or exclusive')->end()->end() 110 | ->arrayNode('elements')->prototype('scalar')->end()->end()->end() 111 | ->end() 112 | ->end() 113 | ->end() 114 | ->end(); 115 | } 116 | 117 | /** 118 | * @return \Closure 119 | */ 120 | private function getChannelTypeValidator() 121 | { 122 | return function ($v) { 123 | $isExclusiveList = isset($v['type']) ? 'exclusive' === $v['type'] : null; 124 | $elements = array(); 125 | 126 | foreach ($v['elements'] as $element) { 127 | Configuration::appendChannelTypes($element, $isExclusiveList, $elements); 128 | } 129 | 130 | return array('type' => $isExclusiveList ? 'exclusive' : 'inclusive', 'elements' => $elements); 131 | }; 132 | } 133 | 134 | /** 135 | * @param bool $invalid 136 | * 137 | * @throws InvalidConfigurationException 138 | */ 139 | public static function throwExceptionOnInvalid($invalid) 140 | { 141 | if (!$invalid) { 142 | return; 143 | } 144 | 145 | throw new InvalidConfigurationException( 146 | 'Cannot combine exclusive/inclusive definitions in channels list' 147 | ); 148 | } 149 | 150 | public static function appendChannelTypes($element, &$isExclusiveList, &$elements = array()) 151 | { 152 | $isExclusiveItem = str_starts_with($element, '!'); 153 | 154 | self::throwExceptionOnInvalid(!$isExclusiveItem === $isExclusiveList); 155 | 156 | $elements[] = $isExclusiveItem ? substr($element, 1) : $element; 157 | $isExclusiveList = $isExclusiveItem; 158 | } 159 | 160 | /** 161 | * @return \Closure 162 | */ 163 | private function isIndexedArray() 164 | { 165 | return function ($v) { 166 | return is_array($v) && is_numeric(key($v)); 167 | }; 168 | } 169 | 170 | /** 171 | * @return \Closure 172 | */ 173 | private function changeToAssoc() 174 | { 175 | return function ($v) { 176 | return array('elements' => $v); 177 | }; 178 | } 179 | 180 | /** 181 | * @return \Closure 182 | */ 183 | private function changeToArrayFromString() 184 | { 185 | return function ($v) { 186 | return array('elements' => array($v)); 187 | }; 188 | } 189 | 190 | /** 191 | * @return \Closure 192 | */ 193 | private function isEmpty() 194 | { 195 | return function ($v) { 196 | return empty($v); 197 | }; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Resolver/EventResolverFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Xiidea\EasyAuditBundle\Resolver; 13 | 14 | use Symfony\Contracts\EventDispatcher\Event; 15 | use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; 16 | use Symfony\Component\PropertyAccess\PropertyAccess; 17 | use Xiidea\EasyAuditBundle\Common\UserAwareComponent; 18 | use Xiidea\EasyAuditBundle\Model\BaseAuditLog; 19 | use Xiidea\EasyAuditBundle\Events\DoctrineEvents; 20 | use Xiidea\EasyAuditBundle\Exception\InvalidServiceException; 21 | use Xiidea\EasyAuditBundle\Exception\UnrecognizedEntityException; 22 | use Xiidea\EasyAuditBundle\Exception\UnrecognizedEventInfoException; 23 | 24 | class EventResolverFactory extends UserAwareComponent 25 | { 26 | private $customResolvers = array(); 27 | private $commonResolver; 28 | 29 | /** 30 | * @var EventResolverInterface 31 | */ 32 | private $entityEventResolver; 33 | 34 | private $debug = false; 35 | 36 | /** 37 | * EventResolverFactory constructor. 38 | * 39 | * @param array $resolverEventMap 40 | * @param $userProperty 41 | * @param $entityClass 42 | */ 43 | public function __construct(private array $resolverEventMap = array(), private $userProperty = 'userIdentifier', private $entityClass = BaseAuditLog::class) 44 | { 45 | } 46 | 47 | /** 48 | * @param Event $event 49 | * @param string $eventName 50 | * 51 | * @return null|BaseAuditLog 52 | * 53 | * @throws UnrecognizedEventInfoException 54 | * @throws \Exception 55 | */ 56 | public function getEventLog(Event $event, $eventName) 57 | { 58 | $eventLog = $this->getEventLogObject($this->getEventLogInfo($event, $eventName)); 59 | 60 | if (null === $eventLog) { 61 | return null; 62 | } 63 | 64 | $eventLog->setTypeId($eventName); 65 | $eventLog->setIp($this->getClientIp()); 66 | $eventLog->setEventTime(new \DateTime()); 67 | $this->setUser($eventLog); 68 | 69 | return $eventLog; 70 | } 71 | 72 | /** 73 | * @param $eventInfo 74 | * 75 | * @return null|BaseAuditLog 76 | * 77 | * @throws UnrecognizedEventInfoException 78 | * @throws \Exception 79 | */ 80 | protected function getEventLogObject($eventInfo) 81 | { 82 | if (empty($eventInfo)) { 83 | return null; 84 | } 85 | 86 | if ($eventInfo instanceof BaseAuditLog) { 87 | return $eventInfo; 88 | } 89 | 90 | return $this->createEventObjectFromArray($eventInfo); 91 | } 92 | 93 | /** 94 | * @param string $eventName 95 | * 96 | * @return EventResolverInterface 97 | */ 98 | protected function getResolver($eventName) 99 | { 100 | if ($this->isEntityEvent($eventName)) { 101 | return $this->entityEventResolver; 102 | } 103 | 104 | if (isset($this->resolverEventMap[$eventName]) && isset($this->customResolvers[$this->resolverEventMap[$eventName]])) { 105 | return $this->customResolvers[$this->resolverEventMap[$eventName]]; 106 | } 107 | 108 | return $this->commonResolver; 109 | } 110 | 111 | /** 112 | * @param string $eventName 113 | * 114 | * @return bool 115 | */ 116 | protected function isEntityEvent($eventName) 117 | { 118 | return in_array($eventName, DoctrineEvents::getConstants()); 119 | } 120 | 121 | /** 122 | * @param Event $event 123 | * @param string $eventName 124 | * 125 | * @throws InvalidServiceException 126 | */ 127 | protected function getEventLogInfo(Event $event, $eventName) 128 | { 129 | if ($event instanceof EmbeddedEventResolverInterface) { 130 | return $event->getEventLogInfo($eventName); 131 | } 132 | 133 | if (null === $eventResolver = $this->getResolver($eventName)) { 134 | return null; 135 | } 136 | 137 | return $eventResolver->getEventLogInfo($event, $eventName); 138 | } 139 | 140 | /** 141 | * @param BaseAuditLog $entity 142 | * 143 | * @throws \Exception 144 | */ 145 | protected function setUser(BaseAuditLog $entity) 146 | { 147 | if (null === $user = $this->getUser()) { 148 | $entity->setUser($this->getAnonymousUserName()); 149 | return; 150 | } 151 | 152 | $entity->setUser($this->getSettablePropertyValue($this->userProperty, $user)); 153 | 154 | $this->setImpersonatingUser($entity, $this->userProperty); 155 | } 156 | 157 | /** 158 | * @return string 159 | */ 160 | protected function getClientIp() 161 | { 162 | $request = $this->getRequest(); 163 | 164 | if ($request) { 165 | return $request->getClientIp(); 166 | } 167 | 168 | return ''; 169 | } 170 | 171 | /** 172 | * @param $id 173 | * @param EventResolverInterface $resolver 174 | * 175 | * @throws \Exception|InvalidServiceException 176 | */ 177 | public function addCustomResolver($id, $resolver) 178 | { 179 | if (!$resolver instanceof EventResolverInterface) { 180 | $this->handleException(new InvalidServiceException( 181 | 'Resolver Service must implement'.EventResolverInterface::class 182 | )); 183 | 184 | return; 185 | } 186 | 187 | $this->customResolvers[$id] = $resolver; 188 | } 189 | 190 | /** 191 | * @throws \Exception 192 | */ 193 | public function setCommonResolver(mixed $resolver) 194 | { 195 | if (!$resolver instanceof EventResolverInterface) { 196 | $this->commonResolver = $this->handleException(new InvalidServiceException( 197 | 'Resolver Service must implement'.EventResolverInterface::class 198 | )); 199 | 200 | return; 201 | } 202 | 203 | $this->commonResolver = $resolver; 204 | } 205 | 206 | /** 207 | * @param \Exception $e 208 | * 209 | * @throws \Exception 210 | */ 211 | protected function handleException(\Exception $e) 212 | { 213 | if ($this->isDebug()) { 214 | throw $e; 215 | } 216 | 217 | return null; 218 | } 219 | 220 | /** 221 | * @param $eventInfo 222 | * 223 | * @return null|BaseAuditLog 224 | * 225 | * @throws \Exception 226 | */ 227 | protected function createEventObjectFromArray($eventInfo) 228 | { 229 | if (!is_array($eventInfo)) { 230 | return $this->handleException(new UnrecognizedEventInfoException()); 231 | } 232 | 233 | $auditLogClass = $this->entityClass; 234 | $eventObject = new $auditLogClass(); 235 | 236 | if (!$eventObject instanceof BaseAuditLog) { 237 | return $this->handleException(new UnrecognizedEntityException()); 238 | } 239 | 240 | return $eventObject->fromArray($eventInfo); 241 | } 242 | 243 | /** 244 | * @param $userProperty 245 | * @param $user 246 | * 247 | * @return mixed 248 | */ 249 | protected function getSettablePropertyValue($userProperty, $user) 250 | { 251 | if (empty($userProperty)) { 252 | return $user; 253 | } 254 | 255 | try { 256 | $propertyAccessor = PropertyAccess::createPropertyAccessor(); 257 | return $propertyAccessor->getValue($user, $userProperty); 258 | } catch (NoSuchPropertyException $e) { 259 | return $this->handleException($e); 260 | } 261 | } 262 | 263 | /** 264 | * @param BaseAuditLog $entity 265 | * @param string $userProperty 266 | */ 267 | protected function setImpersonatingUser(BaseAuditLog $entity, $userProperty) 268 | { 269 | if (null !== $user = $this->getImpersonatingUser()) { 270 | $entity->setImpersonatingUser($this->getSettablePropertyValue($userProperty, $user)); 271 | } 272 | } 273 | 274 | public function setDebug(mixed $debug) 275 | { 276 | $this->debug = $debug; 277 | } 278 | 279 | /** 280 | * @param EventResolverInterface $entityEventResolver 281 | */ 282 | public function setEntityEventResolver($entityEventResolver) 283 | { 284 | $this->entityEventResolver = $entityEventResolver; 285 | } 286 | 287 | private function isDebug() 288 | { 289 | return $this->debug; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy Audit 2 | 3 | [![Build Status](https://github.com/xiidea/EasyAuditBundle/actions/workflows/ci.yml/badge.svg)](https://github.com/xiidea/EasyAuditBundle/actions/workflows/ci.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/xiidea/EasyAuditBundle/badge.svg?branch=master)](https://coveralls.io/github/xiidea/EasyAuditBundle?branch=master) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/xiidea/EasyAuditBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/xiidea/EasyAuditBundle/?branch=master) 6 | [![Latest Stable Version](https://poser.pugx.org/xiidea/easy-audit/v/stable.png)](https://packagist.org/packages/xiidea/easy-audit) 7 | [![Latest Unstable Version](http://poser.pugx.org/xiidea/easy-audit/v/unstable)](https://packagist.org/packages/xiidea/easy-audit) 8 | [![Total Downloads](https://poser.pugx.org/xiidea/easy-audit/downloads.png)](https://packagist.org/packages/xiidea/easy-audit) 9 | [![License](http://poser.pugx.org/xiidea/easy-audit/license)](https://packagist.org/packages/xiidea/easy-audit) 10 | 11 | A Symfony Bundle To Log Selective Events. It is easy to configure and easy to customize for your need. 12 | 13 | ### Versions 14 | 15 | | Symfony | PHP | EasyAuditBundle | Support | 16 | |:-------:|:---------:|:-------------------------------------------------------------:|:-------------------:| 17 | | 5.4-7.4 | >=8.0.2 | 3.x.x | New Features / Bugs | 18 | | 5.x | >=7.2.5 | [2.x.x](https://github.com/xiidea/EasyAuditBundle/tree/2.0.x) | Bugs | 19 | | 2.7-4.4 | ^5.6,^7.0 | [1.4.x](https://github.com/xiidea/EasyAuditBundle/tree/1.4.x) | - | 20 | | <=2.8 | ^5.4-7.3 | [1.3.x](https://github.com/xiidea/EasyAuditBundle/tree/1.3.x) | - | 21 | | <=2.4 | ^5.4 | [1.2.x](https://github.com/xiidea/EasyAuditBundle/tree/1.2.x) | - | 22 | 23 | 24 | ## Install 25 | 26 | 1. Add EasyAuditBundle in your composer.json 27 | 2. Enable the Bundle 28 | 3. Create audit_log entity class 29 | 4. Configure config.yml 30 | 5. Update Database Schema 31 | 32 | ### 1. Add EasyAuditBundle in your composer.json 33 | 34 | Add EasyAuditBundle in your composer.json: 35 | 36 | ```json 37 | { 38 | "require": { 39 | "xiidea/easy-audit": "^3.0" 40 | } 41 | } 42 | ``` 43 | 44 | Now tell composer to download the bundle by running the command: 45 | 46 | ```bash 47 | $ php composer.phar update xiidea/easy-audit 48 | ``` 49 | 50 | Composer will install the bundle to your project's `vendor/xiidea` directory. 51 | 52 | ### 2. Enable the Bundle 53 | 54 | ```php 55 |