From 2f13e7e7ef90a11f562de3e021bab4f679de3e50 Mon Sep 17 00:00:00 2001
From: Nicolas Joubert <njoubert@clever-age.com>
Date: Mon, 10 Feb 2025 10:50:27 +0100
Subject: [PATCH] #37 Refactor models to allow overriding: - replace php
 annotated entities by xml mapping/validations. Add Interfaces. - add class
 entries on configuration for models - rework install documentation with a
 Doctrine ORM Configuration chapter - rework repositories and other services
 using model classNames & Interfaces

---
 config/doctrine/LogRecord.orm.xml             |  21 ++++
 config/doctrine/ProcessExecution.orm.xml      |  20 ++++
 config/doctrine/ProcessSchedule.orm.xml       |  13 +++
 config/doctrine/User.orm.xml                  |  20 ++++
 config/services/command.yaml                  |   1 +
 config/services/event_subscriber.yaml         |   1 +
 config/services/monolog_handler.yaml          |   1 +
 config/services/repository.yaml               |  11 +-
 config/services/security.yaml                 |   2 +-
 config/validation/ProcessSchedule.orm.xml     |  22 ++++
 docs/index.md                                 |  78 ++++++++++++-
 src/CleverAgeUiProcessBundle.php              |  10 ++
 src/Command/UserCreateCommand.php             |   9 +-
 src/Controller/Admin/Process/LaunchAction.php |   6 +-
 .../Admin/ProcessExecutionCrudController.php  |  11 +-
 .../CleverAgeUiProcessExtension.php           |   6 +
 .../Compiler/ResolveTargetEntityPass.php      |  32 +++++
 src/DependencyInjection/Configuration.php     |  13 ++-
 src/Entity/LogRecord.php                      |  35 ++----
 src/Entity/LogRecordInterface.php             |  19 +++
 src/Entity/ProcessExecution.php               |  91 ++++++---------
 src/Entity/ProcessExecutionInterface.php      |  41 +++++++
 src/Entity/ProcessSchedule.php                |  82 +++++--------
 src/Entity/ProcessScheduleInterface.php       |  47 ++++++++
 src/Entity/User.php                           | 110 +++++++-----------
 src/Entity/UserInterface.php                  |  61 ++++++++++
 .../ProcessEventSubscriber.php                |  10 +-
 src/Manager/ProcessExecutionManager.php       |  18 +--
 src/Message/CronProcessMessage.php            |   4 +-
 .../Handler/DoctrineProcessHandler.php        |  14 ++-
 src/Repository/ProcessExecutionRepository.php |  27 +++--
 .../ProcessExecutionRepositoryInterface.php   |  27 +++++
 src/Repository/ProcessScheduleRepository.php  |  22 ++--
 .../ProcessScheduleRepositoryInterface.php    |  22 ++++
 src/Repository/UserRepository.php             |  34 ++++++
 src/Repository/UserRepositoryInterface.php    |  22 ++++
 src/Scheduler/CronScheduler.php               |   6 +-
 .../HttpProcessExecutionAuthenticator.php     |   7 +-
 .../ProcessExecutionExtensionRuntime.php      |   8 +-
 39 files changed, 714 insertions(+), 270 deletions(-)
 create mode 100644 config/doctrine/LogRecord.orm.xml
 create mode 100644 config/doctrine/ProcessExecution.orm.xml
 create mode 100644 config/doctrine/ProcessSchedule.orm.xml
 create mode 100644 config/doctrine/User.orm.xml
 create mode 100644 config/validation/ProcessSchedule.orm.xml
 create mode 100644 src/DependencyInjection/Compiler/ResolveTargetEntityPass.php
 create mode 100644 src/Entity/LogRecordInterface.php
 create mode 100644 src/Entity/ProcessExecutionInterface.php
 create mode 100644 src/Entity/ProcessScheduleInterface.php
 create mode 100644 src/Entity/UserInterface.php
 create mode 100644 src/Repository/ProcessExecutionRepositoryInterface.php
 create mode 100644 src/Repository/ProcessScheduleRepositoryInterface.php
 create mode 100644 src/Repository/UserRepository.php
 create mode 100644 src/Repository/UserRepositoryInterface.php

diff --git a/config/doctrine/LogRecord.orm.xml b/config/doctrine/LogRecord.orm.xml
new file mode 100644
index 0000000..6e24eef
--- /dev/null
+++ b/config/doctrine/LogRecord.orm.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
+    <mapped-superclass name="CleverAge\UiProcessBundle\Entity\LogRecord">
+        <indexes>
+            <index name="idx_log_record_level" columns="level" />
+            <index name="idx_log_record_created_at" columns="created_at" />
+        </indexes>
+
+        <id name="id" column="id" type="integer">
+            <generator strategy="AUTO" />
+        </id>
+        <field name="channel" type="string" length="64" />
+        <field name="level" type="integer" />
+        <field name="message" type="string" length="512" />
+        <field name="context" type="json" />
+        <field name="createdAt" type="datetime_immutable" />
+        <many-to-one field="processExecution" target-entity="CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface">
+            <join-column name="process_execution_id" referenced-column-name="id" on-delete="CASCADE" nullable="false" />
+        </many-to-one>
+    </mapped-superclass>
+</doctrine-mapping>
diff --git a/config/doctrine/ProcessExecution.orm.xml b/config/doctrine/ProcessExecution.orm.xml
new file mode 100644
index 0000000..829d703
--- /dev/null
+++ b/config/doctrine/ProcessExecution.orm.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
+    <mapped-superclass name="CleverAge\UiProcessBundle\Entity\ProcessExecution">
+        <indexes>
+            <index name="idx_process_execution_code" columns="code" />
+            <index name="idx_process_execution_start_date" columns="start_date" />
+        </indexes>
+
+        <id name="id" column="id" type="integer">
+            <generator strategy="AUTO" />
+        </id>
+        <field name="code" type="string" length="255" />
+        <field name="startDate" type="datetime_immutable"/>
+        <field name="endDate" type="datetime_immutable" nullable="true" />
+        <field name="status" type="string" enum-type="CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus" />
+        <field name="report" type="json" />
+        <field name="context" type="json" nullable="true" />
+        <field name="logFilename" type="string" length="255" />
+    </mapped-superclass>
+</doctrine-mapping>
diff --git a/config/doctrine/ProcessSchedule.orm.xml b/config/doctrine/ProcessSchedule.orm.xml
new file mode 100644
index 0000000..ae78b65
--- /dev/null
+++ b/config/doctrine/ProcessSchedule.orm.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
+    <mapped-superclass name="CleverAge\UiProcessBundle\Entity\ProcessSchedule">
+        <id name="id" column="id" type="integer">
+            <generator strategy="AUTO" />
+        </id>
+        <field name="process" type="string" length="255" />
+        <field name="type" type="string" length="6" enum-type="CleverAge\UiProcessBundle\Entity\Enum\ProcessScheduleType" />
+        <field name="expression" type="string" length="255" />
+        <field name="input" type="text" nullable="true" />
+        <field name="context" type="json" />
+    </mapped-superclass>
+</doctrine-mapping>
diff --git a/config/doctrine/User.orm.xml b/config/doctrine/User.orm.xml
new file mode 100644
index 0000000..7d12574
--- /dev/null
+++ b/config/doctrine/User.orm.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
+    <mapped-superclass name="CleverAge\UiProcessBundle\Entity\User" table="process_user">
+        <indexes>
+            <index name="idx_process_user_email" columns="email" />
+        </indexes>
+
+        <id name="id" column="id" type="integer">
+            <generator strategy="AUTO" />
+        </id>
+        <field name="email" type="string" length="255" unique="true" />
+        <field name="firstname" type="string" length="255" nullable="true" />
+        <field name="lastname" type="string" length="255" nullable="true" />
+        <field name="roles" type="json" />
+        <field name="password" type="string" length="255" nullable="true" />
+        <field name="timezone" type="string" length="255" nullable="true" />
+        <field name="locale" type="string" length="255" nullable="true" />
+        <field name="token" type="string" length="255" nullable="true" />
+    </mapped-superclass>
+</doctrine-mapping>
diff --git a/config/services/command.yaml b/config/services/command.yaml
index e8f5c9f..5fa8eb4 100644
--- a/config/services/command.yaml
+++ b/config/services/command.yaml
@@ -5,6 +5,7 @@ services:
         tags:
             - { name: console.command }
         arguments:
+            - '%cleverage_ui_process.entity.user.class%'
             - '@validator'
             - '@security.user_password_hasher'
             - '@doctrine.orm.entity_manager'
diff --git a/config/services/event_subscriber.yaml b/config/services/event_subscriber.yaml
index 1a35949..f1205a0 100644
--- a/config/services/event_subscriber.yaml
+++ b/config/services/event_subscriber.yaml
@@ -5,6 +5,7 @@ services:
         tags:
             - { name: 'kernel.event_subscriber' }
         arguments:
+            - '%cleverage_ui_process.entity.process_execution.class%'
             - '@cleverage_ui_process.monolog_handler.process'
             - '@cleverage_ui_process.monolog_handler.doctrine_process'
             - '@cleverage_ui_process.manager.process_execution'
diff --git a/config/services/monolog_handler.yaml b/config/services/monolog_handler.yaml
index 1bd99b8..c506f49 100644
--- a/config/services/monolog_handler.yaml
+++ b/config/services/monolog_handler.yaml
@@ -5,6 +5,7 @@ services:
         calls:
             - [ setEntityManager, [ '@doctrine.orm.entity_manager' ] ]
             - [ setProcessExecutionManager, [ '@cleverage_ui_process.manager.process_execution' ] ]
+            - [ setLogRecordClassName, [ '%cleverage_ui_process.entity.log_record.class%' ] ]
     CleverAge\UiProcessBundle\Monolog\Handler\DoctrineProcessHandler:
         alias: cleverage_ui_process.monolog_handler.doctrine_process
 
diff --git a/config/services/repository.yaml b/config/services/repository.yaml
index 6e8a5bd..f11bf14 100644
--- a/config/services/repository.yaml
+++ b/config/services/repository.yaml
@@ -4,10 +4,19 @@ services:
         public: false
         arguments:
             - '@doctrine.orm.entity_manager'
+            - '%cleverage_ui_process.entity.process_execution.class%'
+            - '%cleverage_ui_process.entity.log_record.class%'
 
     cleverage_ui_process.repository.process_schedule:
         class: CleverAge\UiProcessBundle\Repository\ProcessScheduleRepository
         public: false
         arguments:
-            - '@doctrine'
+            - '@doctrine.orm.entity_manager'
+            - '%cleverage_ui_process.entity.process_schedule.class%'
 
+    cleverage_ui_process.repository.user:
+        class: CleverAge\UiProcessBundle\Repository\UserRepository
+        public: false
+        arguments:
+            - '@doctrine.orm.entity_manager'
+            - '%cleverage_ui_process.entity.user.class%'
diff --git a/config/services/security.yaml b/config/services/security.yaml
index 2eda664..cd2ccbf 100644
--- a/config/services/security.yaml
+++ b/config/services/security.yaml
@@ -3,5 +3,5 @@ services:
         class: CleverAge\UiProcessBundle\Security\HttpProcessExecutionAuthenticator
         public: false
         arguments:
-            - '@doctrine.orm.entity_manager'
+            - '@cleverage_ui_process.repository.user'
 
diff --git a/config/validation/ProcessSchedule.orm.xml b/config/validation/ProcessSchedule.orm.xml
new file mode 100644
index 0000000..faaf797
--- /dev/null
+++ b/config/validation/ProcessSchedule.orm.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping">
+    <class name="CleverAge\UiProcessBundle\Entity\ProcessSchedule">
+        <property name="process">
+            <constraint name="CleverAge\UiProcessBundle\Validator\IsValidProcessCode" />
+        </property>
+        <property name="expression">
+            <constraint name="When">
+                <option name="expression">this.getType().value == "cron"</option>
+                <option name="constraints">
+                    <constraint name="CleverAge\UiProcessBundle\Validator\CronExpression" />
+                </option>
+            </constraint>
+            <constraint name="When">
+                <option name="expression">this.getType().value == "every"</option>
+                <option name="constraints">
+                    <constraint name="CleverAge\UiProcessBundle\Validator\EveryExpression" />
+                </option>
+            </constraint>
+        </property>
+    </class>
+</constraint-mapping>
diff --git a/docs/index.md b/docs/index.md
index db90d03..1b7d8e3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -19,6 +19,82 @@ Remember to add the following line to `config/bundles.php` (not required if Symf
 CleverAge\UiProcessBundle\CleverAgeUiProcessBundle::class => ['all' => true],
 ```
 
+## Doctrine ORM Configuration
+
+Add these in the config mapping definition (or enable [auto_mapping](https://symfony.com/doc/current/reference/configuration/doctrine.html#mapping-configuration)):
+
+```yaml
+# config/packages/doctrine.yaml
+
+doctrine:
+  orm:
+    mappings:
+      CleverAgeUiProcessBundle: ~
+```
+
+And then create the corresponding entities:
+
+```php
+// src/Entity/LogRecord.php
+
+use Doctrine\ORM\Mapping as ORM;
+use CleverAge\UiProcessBundle\Entity\LogRecord as BaseLogRecord;
+
+#[ORM\Entity]
+#[ORM\Table]
+class LogRecord extends BaseLogRecord
+{
+}
+```
+
+```php
+// src/Entity/ProcessExecution.php
+
+use Doctrine\ORM\Mapping as ORM;
+use CleverAge\UiProcessBundle\Entity\ProcessExecution as BaseProcessExecution;
+
+#[ORM\Entity]
+#[ORM\Table]
+class ProcessExecution extends BaseProcessExecution
+{
+}
+```
+
+```php
+// src/Entity/ProcessSchedule.php
+
+use Doctrine\ORM\Mapping as ORM;
+use CleverAge\UiProcessBundle\Entity\ProcessSchedule as BaseProcessSchedule;
+
+#[ORM\Entity]
+#[ORM\Table]
+class ProcessSchedule extends BaseProcessSchedule
+{
+}
+```
+
+```php
+// src/Entity/User.php
+
+use Doctrine\ORM\Mapping as ORM;
+use CleverAge\UiProcessBundle\Entity\User as BaseUser;
+
+#[ORM\Entity]
+#[ORM\Table(name: 'process_user')]
+class User extends BaseUser
+{
+}
+```
+
+So, update your schema:
+
+```bash
+bin/console doctrine:schema:update --force
+```
+or use [DoctrineMigrationsBundle](https://github.com/doctrine/DoctrineMigrationsBundle)
+
+And create a User using `cleverage:ui-process:user-create` console.
+
 ## Import routes
 
 ```yaml
@@ -26,8 +102,6 @@ ui-process-bundle:
   resource: '@CleverAgeUiProcessBundle/src/Controller'
   type: attribute
 ```
-* Run doctrine migration
-* Create a user using `cleverage:ui-process:user-create` console.
 
 Now you can access UI Process via http://your-domain.com/process
 
diff --git a/src/CleverAgeUiProcessBundle.php b/src/CleverAgeUiProcessBundle.php
index 8acf1d7..d507312 100644
--- a/src/CleverAgeUiProcessBundle.php
+++ b/src/CleverAgeUiProcessBundle.php
@@ -13,10 +13,20 @@
 
 namespace CleverAge\UiProcessBundle;
 
+use CleverAge\UiProcessBundle\DependencyInjection\Compiler\ResolveTargetEntityPass;
+use Symfony\Component\DependencyInjection\Compiler\PassConfig;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
 
 class CleverAgeUiProcessBundle extends Bundle
 {
+    public function build(ContainerBuilder $container)
+    {
+        parent::build($container);
+
+        $container->addCompilerPass(new ResolveTargetEntityPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1000);
+    }
+
     public function getPath(): string
     {
         return \dirname(__DIR__);
diff --git a/src/Command/UserCreateCommand.php b/src/Command/UserCreateCommand.php
index 806cc0c..c73f056 100644
--- a/src/Command/UserCreateCommand.php
+++ b/src/Command/UserCreateCommand.php
@@ -13,7 +13,7 @@
 
 namespace CleverAge\UiProcessBundle\Command;
 
-use CleverAge\UiProcessBundle\Entity\User;
+use CleverAge\UiProcessBundle\Entity\UserInterface;
 use Doctrine\ORM\EntityManagerInterface;
 use Symfony\Component\Console\Attribute\AsCommand;
 use Symfony\Component\Console\Command\Command;
@@ -35,7 +35,11 @@
 )]
 class UserCreateCommand extends Command
 {
+    /**
+     * @param class-string<UserInterface> $userClassName
+     */
     public function __construct(
+        private readonly string $userClassName,
         private readonly ValidatorInterface $validator,
         private readonly UserPasswordHasherInterface $passwordEncoder,
         private readonly EntityManagerInterface $em,
@@ -54,7 +58,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
             $output
         );
 
-        $user = new User();
+        /** @var UserInterface $user */
+        $user = new $this->userClassName();
         $user->setEmail($username);
         $user->setRoles(['ROLE_USER', 'ROLE_ADMIN']);
         $user->setPassword($this->passwordEncoder->hashPassword($user, $password));
diff --git a/src/Controller/Admin/Process/LaunchAction.php b/src/Controller/Admin/Process/LaunchAction.php
index 8f6bed5..340a508 100644
--- a/src/Controller/Admin/Process/LaunchAction.php
+++ b/src/Controller/Admin/Process/LaunchAction.php
@@ -14,7 +14,7 @@
 namespace CleverAge\UiProcessBundle\Controller\Admin\Process;
 
 use CleverAge\ProcessBundle\Exception\MissingProcessException;
-use CleverAge\UiProcessBundle\Entity\User;
+use CleverAge\UiProcessBundle\Entity\UserInterface;
 use CleverAge\UiProcessBundle\Form\Type\LaunchType;
 use CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager;
 use CleverAge\UiProcessBundle\Message\ProcessExecuteMessage;
@@ -130,9 +130,9 @@ protected function dispatch(string $processCode, mixed $input = null, array $con
         $this->messageBus->dispatch($message);
     }
 
-    protected function getUser(): ?User
+    protected function getUser(): ?UserInterface
     {
-        /** @var User $user */
+        /** @var UserInterface $user */
         $user = parent::getUser();
 
         return $user;
diff --git a/src/Controller/Admin/ProcessExecutionCrudController.php b/src/Controller/Admin/ProcessExecutionCrudController.php
index 5e60a60..8071bfb 100644
--- a/src/Controller/Admin/ProcessExecutionCrudController.php
+++ b/src/Controller/Admin/ProcessExecutionCrudController.php
@@ -16,7 +16,7 @@
 use CleverAge\UiProcessBundle\Admin\Field\ContextField;
 use CleverAge\UiProcessBundle\Admin\Field\EnumField;
 use CleverAge\UiProcessBundle\Entity\ProcessExecution;
-use CleverAge\UiProcessBundle\Repository\ProcessExecutionRepository;
+use CleverAge\UiProcessBundle\Repository\ProcessExecutionRepositoryInterface;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
@@ -36,7 +36,7 @@
 class ProcessExecutionCrudController extends AbstractCrudController
 {
     public function __construct(
-        private readonly ProcessExecutionRepository $processExecutionRepository,
+        private readonly ProcessExecutionRepositoryInterface $processExecutionRepository,
         private readonly string $logDirectory,
     ) {
     }
@@ -123,9 +123,8 @@ public function showLogs(AdminContext $adminContext): RedirectResponse
         return $this->redirect($url);
     }
 
-    public function downloadLogFile(
-        AdminContext $context,
-    ): Response {
+    public function downloadLogFile(AdminContext $context): Response
+    {
         /** @var ProcessExecution $processExecution */
         $processExecution = $context->getEntity()->getInstance();
         $filepath = $this->getLogFilePath($processExecution);
@@ -149,7 +148,7 @@ public function configureFilters(Filters $filters): Filters
     private function getLogFilePath(ProcessExecution $processExecution): string
     {
         return $this->logDirectory.
-            \DIRECTORY_SEPARATOR.$processExecution->code.
+            \DIRECTORY_SEPARATOR.$processExecution->getCode().
             \DIRECTORY_SEPARATOR.$processExecution->logFilename
         ;
     }
diff --git a/src/DependencyInjection/CleverAgeUiProcessExtension.php b/src/DependencyInjection/CleverAgeUiProcessExtension.php
index 2b7b302..fb0011e 100644
--- a/src/DependencyInjection/CleverAgeUiProcessExtension.php
+++ b/src/DependencyInjection/CleverAgeUiProcessExtension.php
@@ -33,12 +33,18 @@ public function load(array $configs, ContainerBuilder $container): void
 
         $configuration = new Configuration();
         $config = $this->processConfiguration($configuration, $configs);
+
         $container->getDefinition(UserCrudController::class)
             ->setArgument('$roles', array_combine($config['security']['roles'], $config['security']['roles']));
         $container->getDefinition('cleverage_ui_process.monolog_handler.process')
             ->addMethodCall('setReportIncrementLevel', [$config['logs']['report_increment_level']]);
         $container->getDefinition(ProcessDashboardController::class)
             ->setArgument('$logoPath', $config['design']['logo_path']);
+
+        $container->setParameter('cleverage_ui_process.entity.log_record.class', $config['class']['log_record']);
+        $container->setParameter('cleverage_ui_process.entity.process_execution.class', $config['class']['process_execution']);
+        $container->setParameter('cleverage_ui_process.entity.process_schedule.class', $config['class']['process_schedule']);
+        $container->setParameter('cleverage_ui_process.entity.user.class', $config['class']['user']);
     }
 
     /**
diff --git a/src/DependencyInjection/Compiler/ResolveTargetEntityPass.php b/src/DependencyInjection/Compiler/ResolveTargetEntityPass.php
new file mode 100644
index 0000000..c8ff1ae
--- /dev/null
+++ b/src/DependencyInjection/Compiler/ResolveTargetEntityPass.php
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\DependencyInjection\Compiler;
+
+use CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface;
+use Doctrine\ORM\Events;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+class ResolveTargetEntityPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        $container->findDefinition('doctrine.orm.listeners.resolve_target_entity')
+            ->addMethodCall(
+                'addResolveTargetEntity',
+                [ProcessExecutionInterface::class, $container->getParameter('cleverage_ui_process.entity.process_execution.class'), []],
+            )
+            ->addTag('doctrine.event_listener', ['event' => Events::loadClassMetadata])
+            ->addTag('doctrine.event_listener', ['event' => Events::onClassMetadataNotFound])
+        ;
+    }
+}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 4dd50a6..4b15261 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -25,12 +25,23 @@ public function getConfigTreeBuilder(): TreeBuilder
         $tb = new TreeBuilder('clever_age_ui_process');
         /** @var ArrayNodeDefinition $rootNode */
         $rootNode = $tb->getRootNode();
+        $rootNode
+            ->children()
+                ->arrayNode('class')
+                ->addDefaultsIfNotSet()
+                    ->children()
+                        ->scalarNode('log_record')->defaultValue('App\Entity\LogRecord')->end()
+                        ->scalarNode('process_execution')->defaultValue('App\Entity\ProcessExecution')->end()
+                        ->scalarNode('process_schedule')->defaultValue('App\Entity\ProcessSchedule')->end()
+                        ->scalarNode('user')->defaultValue('App\Entity\User')->end()
+            ->end();
         $rootNode
             ->children()
                 ->arrayNode('security')
                 ->addDefaultsIfNotSet()
                     ->children()
-                        ->arrayNode('roles')->defaultValue(['ROLE_ADMIN'])->scalarPrototype()->end(); // Roles displayed inside user edit form
+                        ->arrayNode('roles')->defaultValue(['ROLE_ADMIN'])->scalarPrototype() // Roles displayed inside user edit form
+            ->end();
         $rootNode
             ->children()
                 ->arrayNode('logs')
diff --git a/src/Entity/LogRecord.php b/src/Entity/LogRecord.php
index 2a9c25f..2a46c84 100644
--- a/src/Entity/LogRecord.php
+++ b/src/Entity/LogRecord.php
@@ -13,47 +13,25 @@
 
 namespace CleverAge\UiProcessBundle\Entity;
 
-use Doctrine\DBAL\Types\Types;
-use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\String\UnicodeString;
 
-#[ORM\Entity]
-#[ORM\Index(name: 'idx_log_record_level', columns: ['level'])]
-#[ORM\Index(name: 'idx_log_record_created_at', columns: ['created_at'])]
-class LogRecord
+class LogRecord implements LogRecordInterface
 {
-    #[ORM\Id]
-    #[ORM\GeneratedValue]
-    #[ORM\Column]
-    private ?int $id = null;
+    protected ?int $id = null;
 
-    #[ORM\Column(type: Types::STRING, length: 64)]
     public readonly string $channel;
 
-    #[ORM\Column(type: Types::INTEGER)]
     public readonly int $level;
 
-    #[ORM\Column(type: Types::STRING, length: 512)]
     public readonly string $message;
 
-    /** @var array<string, mixed> $context */
-    #[ORM\Column(type: Types::JSON)]
+    /** @var array<string, mixed> */
     public readonly array $context;
 
-    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
     public readonly \DateTimeImmutable $createdAt;
 
-    public function getId(): ?int
+    public function __construct(\Monolog\LogRecord $record, protected readonly ProcessExecutionInterface $processExecution)
     {
-        return $this->id;
-    }
-
-    public function __construct(
-        \Monolog\LogRecord $record,
-        #[ORM\ManyToOne(targetEntity: ProcessExecution::class, cascade: ['all'])]
-        #[ORM\JoinColumn(name: 'process_execution_id', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)]
-        private readonly ProcessExecution $processExecution,
-    ) {
         $this->channel = (string) (new UnicodeString($record->channel))->truncate(64);
         $this->level = $record->level->value;
         $this->message = (string) (new UnicodeString($record->message))->truncate(512);
@@ -61,6 +39,11 @@ public function __construct(
         $this->createdAt = \DateTimeImmutable::createFromMutable(new \DateTime());
     }
 
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
     public function contextIsEmpty(): bool
     {
         return [] !== $this->context;
diff --git a/src/Entity/LogRecordInterface.php b/src/Entity/LogRecordInterface.php
new file mode 100644
index 0000000..e0954a7
--- /dev/null
+++ b/src/Entity/LogRecordInterface.php
@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Entity;
+
+interface LogRecordInterface
+{
+    public function getId(): ?int;
+
+    public function contextIsEmpty(): bool;
+}
diff --git a/src/Entity/ProcessExecution.php b/src/Entity/ProcessExecution.php
index a924eda..a36b427 100644
--- a/src/Entity/ProcessExecution.php
+++ b/src/Entity/ProcessExecution.php
@@ -14,57 +14,35 @@
 namespace CleverAge\UiProcessBundle\Entity;
 
 use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus;
-use Doctrine\DBAL\Types\Types;
-use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\String\UnicodeString;
 
-#[ORM\Entity]
-#[ORM\Index(name: 'idx_process_execution_code', columns: ['code'])]
-#[ORM\Index(name: 'idx_process_execution_start_date', columns: ['start_date'])]
-class ProcessExecution implements \Stringable
+class ProcessExecution implements ProcessExecutionInterface, \Stringable
 {
-    #[ORM\Id]
-    #[ORM\GeneratedValue]
-    #[ORM\Column]
-    private ?int $id = null;
+    protected ?int $id = null;
 
-    #[ORM\Column(type: Types::STRING, length: 255)]
-    public readonly string $code;
+    protected readonly string $code;
 
-    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
     public readonly \DateTimeImmutable $startDate;
 
-    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
     public ?\DateTimeImmutable $endDate = null;
 
-    #[ORM\Column(type: Types::STRING, enumType: ProcessExecutionStatus::class)]
     public ProcessExecutionStatus $status = ProcessExecutionStatus::Started;
 
     /**
      * @var array<string, mixed>
      */
-    #[ORM\Column(type: Types::JSON)]
-    private array $report = [];
+    protected array $report = [];
 
     /**
      * @var array<string|int, mixed>
      */
-    #[ORM\Column(type: Types::JSON, nullable: true)]
-    private ?array $context = [];
-
-    public function getId(): ?int
-    {
-        return $this->id;
-    }
+    protected array $context;
 
     /**
-     * @param array<string|int, mixed> $context
+     * @param ?array<string|int, mixed> $context
      */
-    public function __construct(
-        string $code,
-        #[ORM\Column(type: Types::STRING, length: 255)] public readonly string $logFilename,
-        ?array $context = [],
-    ) {
+    public function __construct(string $code, public readonly string $logFilename, ?array $context = [])
+    {
         $this->code = (string) (new UnicodeString($code))->truncate(255);
         $this->startDate = \DateTimeImmutable::createFromMutable(new \DateTime());
         $this->context = $context ?? [];
@@ -75,28 +53,19 @@ public function __toString(): string
         return \sprintf('%s (%s)', $this->id, $this->code);
     }
 
-    public function setStatus(ProcessExecutionStatus $status): void
-    {
-        $this->status = $status;
-    }
-
-    public function end(): void
+    public function getId(): ?int
     {
-        $this->endDate = \DateTimeImmutable::createFromMutable(new \DateTime());
+        return $this->id;
     }
 
-    public function addReport(string $key, mixed $value): void
+    public function getCode(): string
     {
-        $this->report[$key] = $value;
+        return $this->code;
     }
 
-    public function getReport(?string $key = null, mixed $default = null): mixed
+    public function end(): void
     {
-        if (null === $key) {
-            return $this->report;
-        }
-
-        return $this->report[$key] ?? $default;
+        $this->endDate = \DateTimeImmutable::createFromMutable(new \DateTime());
     }
 
     public function duration(string $format = '%H hour(s) %I min(s) %S s'): ?string
@@ -109,24 +78,36 @@ public function duration(string $format = '%H hour(s) %I min(s) %S s'): ?string
         return $diff->format($format);
     }
 
-    public function getCode(): string
+    public function setStatus(ProcessExecutionStatus $status): static
     {
-        return $this->code;
+        $this->status = $status;
+
+        return $this;
     }
 
-    /**
-     * @return array<string|int, mixed>
-     */
-    public function getContext(): ?array
+    public function addReport(string $key, mixed $value): void
+    {
+        $this->report[$key] = $value;
+    }
+
+    public function getReport(?string $key = null, mixed $default = null): mixed
+    {
+        if (null === $key) {
+            return $this->report;
+        }
+
+        return $this->report[$key] ?? $default;
+    }
+
+    public function getContext(): array
     {
         return $this->context;
     }
 
-    /**
-     * @param array<string|int, mixed> $context
-     */
-    public function setContext(array $context): void
+    public function setContext(array $context): static
     {
         $this->context = $context;
+
+        return $this;
     }
 }
diff --git a/src/Entity/ProcessExecutionInterface.php b/src/Entity/ProcessExecutionInterface.php
new file mode 100644
index 0000000..fd23bbb
--- /dev/null
+++ b/src/Entity/ProcessExecutionInterface.php
@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Entity;
+
+use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus;
+
+interface ProcessExecutionInterface
+{
+    public function getId(): ?int;
+
+    public function getCode(): string;
+
+    public function end(): void;
+
+    public function duration(string $format = '%H hour(s) %I min(s) %S s'): ?string;
+
+    public function setStatus(ProcessExecutionStatus $status): static;
+
+    public function addReport(string $key, mixed $value): void;
+
+    public function getReport(?string $key = null, mixed $default = null): mixed;
+
+    /**
+     * @return array<string|int, mixed>
+     */
+    public function getContext(): array;
+
+    /**
+     * @param array<string|int, mixed> $context
+     */
+    public function setContext(array $context): static;
+}
diff --git a/src/Entity/ProcessSchedule.php b/src/Entity/ProcessSchedule.php
index 8eea928..e8d5045 100644
--- a/src/Entity/ProcessSchedule.php
+++ b/src/Entity/ProcessSchedule.php
@@ -14,45 +14,23 @@
 namespace CleverAge\UiProcessBundle\Entity;
 
 use CleverAge\UiProcessBundle\Entity\Enum\ProcessScheduleType;
-use CleverAge\UiProcessBundle\Repository\ProcessScheduleRepository;
-use CleverAge\UiProcessBundle\Validator\CronExpression;
-use CleverAge\UiProcessBundle\Validator\EveryExpression;
-use CleverAge\UiProcessBundle\Validator\IsValidProcessCode;
-use Doctrine\DBAL\Types\Types;
-use Doctrine\ORM\Mapping as ORM;
-use Symfony\Component\Validator\Constraints as Assert;
-
-#[ORM\Entity(repositoryClass: ProcessScheduleRepository::class)]
-class ProcessSchedule
+
+class ProcessSchedule implements ProcessScheduleInterface
 {
-    #[ORM\Id]
-    #[ORM\GeneratedValue]
-    #[ORM\Column]
-    private ?int $id = null;
-
-    #[ORM\Column(length: 255)]
-    #[IsValidProcessCode]
-    private string $process;
-
-    #[ORM\Column(length: 6)]
-    private ProcessScheduleType $type;
-    #[ORM\Column(length: 255)]
-    #[Assert\When(
-        expression: 'this.getType().value == "cron"', constraints: [new CronExpression()]
-    )]
-    #[Assert\When(
-        expression: 'this.getType().value == "every"', constraints: [new EveryExpression()]
-    )]
-    private string $expression;
-
-    #[ORM\Column(type: Types::TEXT, nullable: true)]
-    private ?string $input = null;
+    protected ?int $id = null;
+
+    protected string $process;
+
+    protected ProcessScheduleType $type;
+
+    protected string $expression;
+
+    protected ?string $input = null;
 
     /**
-     * @var string|array<string|int, mixed>
+     * @var array<string|int, mixed>
      */
-    #[ORM\Column(type: Types::JSON)]
-    private string|array $context = [];
+    protected array $context = [];
 
     public function getId(): ?int
     {
@@ -71,22 +49,6 @@ public function setProcess(string $process): static
         return $this;
     }
 
-    /**
-     * @return array<string|int, mixed>
-     */
-    public function getContext(): array
-    {
-        return \is_array($this->context) ? $this->context : json_decode($this->context);
-    }
-
-    /**
-     * @param array<string|int, mixed> $context
-     */
-    public function setContext(array $context): void
-    {
-        $this->context = $context;
-    }
-
     public function getNextExecution(): null
     {
         return null;
@@ -97,7 +59,7 @@ public function getType(): ProcessScheduleType
         return $this->type;
     }
 
-    public function setType(ProcessScheduleType $type): self
+    public function setType(ProcessScheduleType $type): static
     {
         $this->type = $type;
 
@@ -109,7 +71,7 @@ public function getExpression(): ?string
         return $this->expression;
     }
 
-    public function setExpression(string $expression): self
+    public function setExpression(string $expression): static
     {
         $this->expression = $expression;
 
@@ -121,10 +83,22 @@ public function getInput(): ?string
         return $this->input;
     }
 
-    public function setInput(?string $input): self
+    public function setInput(?string $input): static
     {
         $this->input = $input;
 
         return $this;
     }
+
+    public function getContext(): array
+    {
+        return $this->context;
+    }
+
+    public function setContext(array $context): static
+    {
+        $this->context = $context;
+
+        return $this;
+    }
 }
diff --git a/src/Entity/ProcessScheduleInterface.php b/src/Entity/ProcessScheduleInterface.php
new file mode 100644
index 0000000..6187b7b
--- /dev/null
+++ b/src/Entity/ProcessScheduleInterface.php
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Entity;
+
+use CleverAge\UiProcessBundle\Entity\Enum\ProcessScheduleType;
+
+interface ProcessScheduleInterface
+{
+    public function getId(): ?int;
+
+    public function getProcess(): ?string;
+
+    public function setProcess(string $process): static;
+
+    public function getNextExecution(): null;
+
+    public function getType(): ProcessScheduleType;
+
+    public function setType(ProcessScheduleType $type): static;
+
+    public function getExpression(): ?string;
+
+    public function setExpression(string $expression): static;
+
+    public function getInput(): ?string;
+
+    public function setInput(?string $input): static;
+
+    /**
+     * @return array<string|int, mixed>
+     */
+    public function getContext(): array;
+
+    /**
+     * @param array<string|int, mixed> $context
+     */
+    public function setContext(array $context): static;
+}
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 11532f7..84c1df4 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -13,47 +13,28 @@
 
 namespace CleverAge\UiProcessBundle\Entity;
 
-use Doctrine\DBAL\Types\Types;
-use Doctrine\ORM\Mapping as ORM;
-use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
-use Symfony\Component\Security\Core\User\UserInterface;
-
-#[ORM\Entity]
-#[ORM\Table(name: 'process_user')]
-#[ORM\Index(name: 'idx_process_user_email', columns: ['email'])]
-class User implements UserInterface, PasswordAuthenticatedUserInterface
+class User implements UserInterface
 {
-    #[ORM\Id]
-    #[ORM\GeneratedValue]
-    #[ORM\Column]
-    private ?int $id = null;
+    protected ?int $id = null;
 
-    #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
-    private string $email;
+    protected string $email;
 
-    #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
-    private ?string $firstname = null;
+    protected ?string $firstname = null;
 
-    #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
-    private ?string $lastname = null;
+    protected ?string $lastname = null;
 
     /**
      * @var string[]
      */
-    #[ORM\Column(type: Types::JSON)]
-    private array $roles = [];
+    protected array $roles = [];
 
-    #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
-    private ?string $password = null;
+    protected ?string $password = null;
 
-    #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
-    private ?string $timezone = null;
+    protected ?string $timezone = null;
 
-    #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
-    private ?string $locale = null;
+    protected ?string $locale = null;
 
-    #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
-    private ?string $token = null;
+    protected ?string $token = null;
 
     public function getId(): ?int
     {
@@ -65,19 +46,24 @@ public function getEmail(): ?string
         return $this->email;
     }
 
-    public function setEmail(string $email): self
+    public function setEmail(string $email): static
     {
         $this->email = $email;
 
         return $this;
     }
 
+    public function getUsername(): string
+    {
+        return $this->getUserIdentifier();
+    }
+
     public function getFirstname(): ?string
     {
         return $this->firstname;
     }
 
-    public function setFirstname(?string $firstname): self
+    public function setFirstname(?string $firstname): static
     {
         $this->firstname = $firstname;
 
@@ -89,74 +75,57 @@ public function getLastname(): ?string
         return $this->lastname;
     }
 
-    public function setLastname(?string $lastname): self
+    public function setLastname(?string $lastname): static
     {
         $this->lastname = $lastname;
 
         return $this;
     }
 
-    public function getUserIdentifier(): string
-    {
-        if ('' === $this->email) {
-            throw new \LogicException('The User class must have an email.');
-        }
-
-        return $this->email;
-    }
-
-    public function getUsername(): string
-    {
-        return $this->getUserIdentifier();
-    }
-
-    public function getTimezone(): ?string
+    public function getRoles(): array
     {
-        return $this->timezone;
+        return array_merge(['ROLE_USER'], $this->roles);
     }
 
-    public function setTimezone(?string $timezone): self
+    public function setRoles(array $roles): static
     {
-        $this->timezone = $timezone;
+        $this->roles = $roles;
 
         return $this;
     }
 
-    public function getLocale(): ?string
+    public function getPassword(): ?string
     {
-        return $this->locale;
+        return $this->password;
     }
 
-    public function setLocale(?string $locale): self
+    public function setPassword(string $password): static
     {
-        $this->locale = $locale;
+        $this->password = $password;
 
         return $this;
     }
 
-    public function getRoles(): array
+    public function getTimezone(): ?string
     {
-        return array_merge(['ROLE_USER'], $this->roles);
+        return $this->timezone;
     }
 
-    /**
-     * @param array<int, string> $roles
-     */
-    public function setRoles(array $roles): self
+    public function setTimezone(?string $timezone): static
     {
-        $this->roles = $roles;
+        $this->timezone = $timezone;
 
         return $this;
     }
 
-    public function getPassword(): ?string
+    public function getLocale(): ?string
     {
-        return $this->password;
+        return $this->locale;
     }
 
-    public function setPassword(string $password): self
+    public function setLocale(?string $locale): static
     {
-        $this->password = $password;
+        $this->locale = $locale;
 
         return $this;
     }
@@ -166,7 +135,7 @@ public function getToken(): ?string
         return $this->token;
     }
 
-    public function setToken(?string $token): self
+    public function setToken(?string $token): static
     {
         $this->token = $token;
 
@@ -178,4 +147,13 @@ public function eraseCredentials(): void
         // If you store any temporary, sensitive data on the user, clear it here
         // $this->plainPassword = null;
     }
+
+    public function getUserIdentifier(): string
+    {
+        if ('' === $this->email) {
+            throw new \LogicException('The User class must have an email.');
+        }
+
+        return $this->email;
+    }
 }
diff --git a/src/Entity/UserInterface.php b/src/Entity/UserInterface.php
new file mode 100644
index 0000000..b654054
--- /dev/null
+++ b/src/Entity/UserInterface.php
@@ -0,0 +1,61 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Entity;
+
+use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
+use Symfony\Component\Security\Core\User\UserInterface as CoreUserInterface;
+
+interface UserInterface extends CoreUserInterface, PasswordAuthenticatedUserInterface
+{
+    public function getId(): ?int;
+
+    public function getEmail(): ?string;
+
+    public function setEmail(string $email): static;
+
+    public function getUsername(): string;
+
+    public function getFirstname(): ?string;
+
+    public function setFirstname(?string $firstname): static;
+
+    public function getLastname(): ?string;
+
+    public function setLastname(?string $lastname): static;
+
+    public function getRoles(): array;
+
+    /**
+     * @param array<int, string> $roles
+     */
+    public function setRoles(array $roles): static;
+
+    public function getPassword(): ?string;
+
+    public function setPassword(string $password): static;
+
+    public function getTimezone(): ?string;
+
+    public function setTimezone(?string $timezone): static;
+
+    public function getLocale(): ?string;
+
+    public function setLocale(?string $locale): static;
+
+    public function getToken(): ?string;
+
+    public function setToken(?string $token): static;
+
+    public function eraseCredentials(): void;
+
+    public function getUserIdentifier(): string;
+}
diff --git a/src/EventSubscriber/ProcessEventSubscriber.php b/src/EventSubscriber/ProcessEventSubscriber.php
index ed90faa..26e4759 100644
--- a/src/EventSubscriber/ProcessEventSubscriber.php
+++ b/src/EventSubscriber/ProcessEventSubscriber.php
@@ -15,7 +15,7 @@
 
 use CleverAge\ProcessBundle\Event\ProcessEvent;
 use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus;
-use CleverAge\UiProcessBundle\Entity\ProcessExecution;
+use CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface;
 use CleverAge\UiProcessBundle\Manager\ProcessExecutionManager;
 use CleverAge\UiProcessBundle\Monolog\Handler\DoctrineProcessHandler;
 use CleverAge\UiProcessBundle\Monolog\Handler\ProcessHandler;
@@ -24,7 +24,11 @@
 
 final readonly class ProcessEventSubscriber implements EventSubscriberInterface
 {
+    /**
+     * @param class-string<ProcessExecutionInterface> $processExecutionClassName
+     */
     public function __construct(
+        private string $processExecutionClassName,
         private ProcessHandler $processHandler,
         private DoctrineProcessHandler $doctrineProcessHandler,
         private ProcessExecutionManager $processExecutionManager,
@@ -36,8 +40,8 @@ public function onProcessStart(ProcessEvent $event): void
         if (false === $this->processHandler->hasFilename()) {
             $this->processHandler->setFilename(\sprintf('%s/%s.log', $event->getProcessCode(), Uuid::v4()));
         }
-        if (!$this->processExecutionManager->getCurrentProcessExecution() instanceof ProcessExecution) {
-            $processExecution = new ProcessExecution(
+        if (!$this->processExecutionManager->getCurrentProcessExecution() instanceof ProcessExecutionInterface) {
+            $processExecution = new $this->processExecutionClassName(
                 $event->getProcessCode(),
                 basename((string) $this->processHandler->getFilename()),
                 $event->getProcessContext()
diff --git a/src/Manager/ProcessExecutionManager.php b/src/Manager/ProcessExecutionManager.php
index 84a6041..66e0b37 100644
--- a/src/Manager/ProcessExecutionManager.php
+++ b/src/Manager/ProcessExecutionManager.php
@@ -13,34 +13,34 @@
 
 namespace CleverAge\UiProcessBundle\Manager;
 
-use CleverAge\UiProcessBundle\Entity\ProcessExecution;
-use CleverAge\UiProcessBundle\Repository\ProcessExecutionRepository;
+use CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface;
+use CleverAge\UiProcessBundle\Repository\ProcessExecutionRepositoryInterface;
 
 class ProcessExecutionManager
 {
-    private ?ProcessExecution $currentProcessExecution = null;
+    private ?ProcessExecutionInterface $currentProcessExecution = null;
 
-    public function __construct(private readonly ProcessExecutionRepository $processExecutionRepository)
+    public function __construct(private readonly ProcessExecutionRepositoryInterface $processExecutionRepository)
     {
     }
 
-    public function setCurrentProcessExecution(ProcessExecution $processExecution): self
+    public function setCurrentProcessExecution(ProcessExecutionInterface $processExecution): self
     {
-        if (!$this->currentProcessExecution instanceof ProcessExecution) {
+        if (!$this->currentProcessExecution instanceof ProcessExecutionInterface) {
             $this->currentProcessExecution = $processExecution;
         }
 
         return $this;
     }
 
-    public function getCurrentProcessExecution(): ?ProcessExecution
+    public function getCurrentProcessExecution(): ?ProcessExecutionInterface
     {
         return $this->currentProcessExecution;
     }
 
     public function unsetProcessExecution(string $processCode): self
     {
-        if ($this->currentProcessExecution?->code === $processCode) {
+        if ($this->currentProcessExecution?->getCode() === $processCode) {
             $this->currentProcessExecution = null;
         }
 
@@ -49,7 +49,7 @@ public function unsetProcessExecution(string $processCode): self
 
     public function save(): self
     {
-        if ($this->currentProcessExecution instanceof ProcessExecution) {
+        if ($this->currentProcessExecution instanceof ProcessExecutionInterface) {
             $this->processExecutionRepository->save($this->currentProcessExecution);
         }
 
diff --git a/src/Message/CronProcessMessage.php b/src/Message/CronProcessMessage.php
index 8e94c09..11a0bfe 100644
--- a/src/Message/CronProcessMessage.php
+++ b/src/Message/CronProcessMessage.php
@@ -13,11 +13,11 @@
 
 namespace CleverAge\UiProcessBundle\Message;
 
-use CleverAge\UiProcessBundle\Entity\ProcessSchedule;
+use CleverAge\UiProcessBundle\Entity\ProcessScheduleInterface;
 
 final readonly class CronProcessMessage
 {
-    public function __construct(public ProcessSchedule $processSchedule)
+    public function __construct(public ProcessScheduleInterface $processSchedule)
     {
     }
 }
diff --git a/src/Monolog/Handler/DoctrineProcessHandler.php b/src/Monolog/Handler/DoctrineProcessHandler.php
index 07c4a0a..28c1eef 100644
--- a/src/Monolog/Handler/DoctrineProcessHandler.php
+++ b/src/Monolog/Handler/DoctrineProcessHandler.php
@@ -13,7 +13,8 @@
 
 namespace CleverAge\UiProcessBundle\Monolog\Handler;
 
-use CleverAge\UiProcessBundle\Entity\ProcessExecution;
+use CleverAge\UiProcessBundle\Entity\LogRecordInterface;
+use CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface;
 use CleverAge\UiProcessBundle\Manager\ProcessExecutionManager;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\EntityManagerInterface;
@@ -27,6 +28,8 @@ class DoctrineProcessHandler extends AbstractProcessingHandler
     private ArrayCollection $records;
     private ?ProcessExecutionManager $processExecutionManager = null;
     private ?EntityManagerInterface $em = null;
+    /** @var string class-string<LogRecordInterface> */
+    private string $logRecordClassName;
 
     public function __construct(int|string|Level $level = Level::Debug, bool $bubble = true)
     {
@@ -44,6 +47,11 @@ public function setProcessExecutionManager(ProcessExecutionManager $processExecu
         $this->processExecutionManager = $processExecutionManager;
     }
 
+    public function setLogRecordClassName(string $logRecordClassName): void
+    {
+        $this->logRecordClassName = $logRecordClassName;
+    }
+
     public function __destruct()
     {
         $this->flush();
@@ -53,8 +61,8 @@ public function __destruct()
     public function flush(): void
     {
         foreach ($this->records as $record) {
-            if (($currentProcessExecution = $this->processExecutionManager?->getCurrentProcessExecution()) instanceof ProcessExecution) {
-                $entity = new \CleverAge\UiProcessBundle\Entity\LogRecord($record, $currentProcessExecution);
+            if (($currentProcessExecution = $this->processExecutionManager?->getCurrentProcessExecution()) instanceof ProcessExecutionInterface) {
+                $entity = new $this->logRecordClassName($record, $currentProcessExecution);
                 $this->em?->persist($entity);
             }
         }
diff --git a/src/Repository/ProcessExecutionRepository.php b/src/Repository/ProcessExecutionRepository.php
index e5d09e0..13da738 100644
--- a/src/Repository/ProcessExecutionRepository.php
+++ b/src/Repository/ProcessExecutionRepository.php
@@ -13,33 +13,32 @@
 
 namespace CleverAge\UiProcessBundle\Repository;
 
-use CleverAge\UiProcessBundle\Entity\LogRecord;
-use CleverAge\UiProcessBundle\Entity\ProcessExecution;
+use CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\EntityRepository;
 
 /**
- * @extends EntityRepository<ProcessExecution>
+ * @template T of ProcessExecutionInterface
  *
- * @method ProcessExecution|null find($id, $lockMode = null, $lockVersion = null)
- * @method ProcessExecution|null findOneBy(mixed[] $criteria, string[] $orderBy = null)
- * @method ProcessExecution[]    findAll()
- * @method ProcessExecution[]    findBy(mixed[] $criteria, string[] $orderBy = null, $limit = null, $offset = null)
+ * @template-extends EntityRepository<ProcessExecutionInterface>
  */
-class ProcessExecutionRepository extends EntityRepository
+class ProcessExecutionRepository extends EntityRepository implements ProcessExecutionRepositoryInterface
 {
-    public function __construct(EntityManagerInterface $em)
+    /**
+     * @param class-string<ProcessExecutionInterface> $className
+     */
+    public function __construct(EntityManagerInterface $em, string $className, private readonly string $logRecordClassName)
     {
-        parent::__construct($em, $em->getClassMetadata(ProcessExecution::class));
+        parent::__construct($em, $em->getClassMetadata($className));
     }
 
-    public function save(ProcessExecution $processExecution): void
+    public function save(ProcessExecutionInterface $processExecution): void
     {
         $this->getEntityManager()->persist($processExecution);
         $this->getEntityManager()->flush();
     }
 
-    public function getLastProcessExecution(string $code): ?ProcessExecution
+    public function getLastProcessExecution(string $code): ?ProcessExecutionInterface
     {
         $qb = $this->createQueryBuilder('pe');
 
@@ -51,11 +50,11 @@ public function getLastProcessExecution(string $code): ?ProcessExecution
             ->getOneOrNullResult();
     }
 
-    public function hasLogs(ProcessExecution $processExecution): bool
+    public function hasLogs(ProcessExecutionInterface $processExecution): bool
     {
         $qb = $this->createQueryBuilder('pe')
             ->select('count(lr.id)')
-            ->join(LogRecord::class, 'lr', 'WITH', 'lr.processExecution = pe')
+            ->join($this->logRecordClassName, 'lr', 'WITH', 'lr.processExecution = pe')
             ->where('pe.id = :id')
             ->setParameter('id', $processExecution->getId()
             );
diff --git a/src/Repository/ProcessExecutionRepositoryInterface.php b/src/Repository/ProcessExecutionRepositoryInterface.php
new file mode 100644
index 0000000..ae980d3
--- /dev/null
+++ b/src/Repository/ProcessExecutionRepositoryInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Repository;
+
+use CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface;
+use Doctrine\Persistence\ObjectRepository;
+
+/**
+ * @extends ObjectRepository<ProcessExecutionInterface>
+ */
+interface ProcessExecutionRepositoryInterface extends ObjectRepository
+{
+    public function save(ProcessExecutionInterface $processExecution): void;
+
+    public function getLastProcessExecution(string $code): ?ProcessExecutionInterface;
+
+    public function hasLogs(ProcessExecutionInterface $processExecution): bool;
+}
diff --git a/src/Repository/ProcessScheduleRepository.php b/src/Repository/ProcessScheduleRepository.php
index 6c8e3ea..5e6be50 100644
--- a/src/Repository/ProcessScheduleRepository.php
+++ b/src/Repository/ProcessScheduleRepository.php
@@ -13,22 +13,22 @@
 
 namespace CleverAge\UiProcessBundle\Repository;
 
-use CleverAge\UiProcessBundle\Entity\ProcessSchedule;
-use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
-use Doctrine\Persistence\ManagerRegistry;
+use CleverAge\UiProcessBundle\Entity\ProcessScheduleInterface;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\EntityRepository;
 
 /**
- * @extends ServiceEntityRepository<ProcessSchedule>
+ * @template T of ProcessScheduleInterface
  *
- * @method ProcessSchedule|null find($id, $lockMode = null, $lockVersion = null)
- * @method ProcessSchedule|null findOneBy(mixed[] $criteria, string[] $orderBy = null)
- * @method ProcessSchedule[]    findAll()
- * @method ProcessSchedule[]    findBy(mixed[] $criteria, string[] $orderBy = null, $limit = null, $offset = null)
+ * @template-extends EntityRepository<ProcessScheduleInterface>
  */
-class ProcessScheduleRepository extends ServiceEntityRepository
+class ProcessScheduleRepository extends EntityRepository implements ProcessScheduleRepositoryInterface
 {
-    public function __construct(ManagerRegistry $registry)
+    /**
+     * @param class-string<ProcessScheduleInterface> $className
+     */
+    public function __construct(EntityManagerInterface $em, string $className)
     {
-        parent::__construct($registry, ProcessSchedule::class);
+        parent::__construct($em, $em->getClassMetadata($className));
     }
 }
diff --git a/src/Repository/ProcessScheduleRepositoryInterface.php b/src/Repository/ProcessScheduleRepositoryInterface.php
new file mode 100644
index 0000000..443a046
--- /dev/null
+++ b/src/Repository/ProcessScheduleRepositoryInterface.php
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Repository;
+
+use CleverAge\UiProcessBundle\Entity\ProcessScheduleInterface;
+use Doctrine\Persistence\ObjectRepository;
+
+/**
+ * @extends ObjectRepository<ProcessScheduleInterface>
+ */
+interface ProcessScheduleRepositoryInterface extends ObjectRepository
+{
+}
diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php
new file mode 100644
index 0000000..4a5a9a3
--- /dev/null
+++ b/src/Repository/UserRepository.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Repository;
+
+use CleverAge\UiProcessBundle\Entity\UserInterface;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\EntityRepository;
+
+/**
+ * @template T of UserInterface
+ *
+ * @template-extends EntityRepository<UserInterface>
+ */
+class UserRepository extends EntityRepository implements UserRepositoryInterface
+{
+    /**
+     * @param class-string<UserInterface> $className
+     */
+    public function __construct(EntityManagerInterface $em, string $className)
+    {
+        parent::__construct($em, $em->getClassMetadata($className));
+    }
+}
diff --git a/src/Repository/UserRepositoryInterface.php b/src/Repository/UserRepositoryInterface.php
new file mode 100644
index 0000000..9fb3f40
--- /dev/null
+++ b/src/Repository/UserRepositoryInterface.php
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\Repository;
+
+use CleverAge\UiProcessBundle\Entity\UserInterface;
+use Doctrine\Persistence\ObjectRepository;
+
+/**
+ * @extends ObjectRepository<UserInterface>
+ */
+interface UserRepositoryInterface extends ObjectRepository
+{
+}
diff --git a/src/Scheduler/CronScheduler.php b/src/Scheduler/CronScheduler.php
index b12fd20..518309d 100644
--- a/src/Scheduler/CronScheduler.php
+++ b/src/Scheduler/CronScheduler.php
@@ -15,7 +15,7 @@
 
 use CleverAge\UiProcessBundle\Entity\Enum\ProcessScheduleType;
 use CleverAge\UiProcessBundle\Message\CronProcessMessage;
-use CleverAge\UiProcessBundle\Repository\ProcessScheduleRepository;
+use CleverAge\UiProcessBundle\Repository\ProcessScheduleRepositoryInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\Scheduler\RecurringMessage;
 use Symfony\Component\Scheduler\Schedule;
@@ -25,7 +25,7 @@
 readonly class CronScheduler implements ScheduleProviderInterface
 {
     public function __construct(
-        private ProcessScheduleRepository $repository,
+        private ProcessScheduleRepositoryInterface $processScheduleRepository,
         private ValidatorInterface $validator,
         private LoggerInterface $logger,
     ) {
@@ -35,7 +35,7 @@ public function getSchedule(): Schedule
     {
         $schedule = new Schedule();
         try {
-            foreach ($this->repository->findAll() as $processSchedule) {
+            foreach ($this->processScheduleRepository->findAll() as $processSchedule) {
                 $violations = $this->validator->validate($processSchedule);
                 if (0 !== $violations->count()) {
                     foreach ($violations as $violation) {
diff --git a/src/Security/HttpProcessExecutionAuthenticator.php b/src/Security/HttpProcessExecutionAuthenticator.php
index 85734f9..5ce8962 100644
--- a/src/Security/HttpProcessExecutionAuthenticator.php
+++ b/src/Security/HttpProcessExecutionAuthenticator.php
@@ -13,8 +13,7 @@
 
 namespace CleverAge\UiProcessBundle\Security;
 
-use CleverAge\UiProcessBundle\Entity\User;
-use Doctrine\ORM\EntityManagerInterface;
+use CleverAge\UiProcessBundle\Repository\UserRepositoryInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -28,7 +27,7 @@
 
 class HttpProcessExecutionAuthenticator extends AbstractAuthenticator
 {
-    public function __construct(private readonly EntityManagerInterface $entityManager)
+    public function __construct(private readonly UserRepositoryInterface $userRepository)
     {
     }
 
@@ -44,7 +43,7 @@ public function authenticate(Request $request): Passport
         }
         $token = $request->headers->get('Authorization');
         $token = str_replace('Bearer ', '', $token ?? '');
-        $user = $this->entityManager->getRepository(User::class)->findOneBy(
+        $user = $this->userRepository->findOneBy(
             ['token' => (new Pbkdf2PasswordHasher())->hash($token)]
         );
         if (null === $user) {
diff --git a/src/Twig/Runtime/ProcessExecutionExtensionRuntime.php b/src/Twig/Runtime/ProcessExecutionExtensionRuntime.php
index 592f1e4..e0e43fd 100644
--- a/src/Twig/Runtime/ProcessExecutionExtensionRuntime.php
+++ b/src/Twig/Runtime/ProcessExecutionExtensionRuntime.php
@@ -13,20 +13,20 @@
 
 namespace CleverAge\UiProcessBundle\Twig\Runtime;
 
-use CleverAge\UiProcessBundle\Entity\ProcessExecution;
+use CleverAge\UiProcessBundle\Entity\ProcessExecutionInterface;
 use CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager;
-use CleverAge\UiProcessBundle\Repository\ProcessExecutionRepository;
+use CleverAge\UiProcessBundle\Repository\ProcessExecutionRepositoryInterface;
 use Twig\Extension\RuntimeExtensionInterface;
 
 readonly class ProcessExecutionExtensionRuntime implements RuntimeExtensionInterface
 {
     public function __construct(
-        private ProcessExecutionRepository $processExecutionRepository,
+        private ProcessExecutionRepositoryInterface $processExecutionRepository,
         private ProcessConfigurationsManager $processConfigurationsManager,
     ) {
     }
 
-    public function getLastExecutionDate(string $code): ?ProcessExecution
+    public function getLastExecutionDate(string $code): ?ProcessExecutionInterface
     {
         return $this->processExecutionRepository->getLastProcessExecution($code);
     }