Sfoglia il codice sorgente

automatic snake_case naming and new column types

phpdevcommunity 1 mese fa
parent
commit
2375383b9e
65 ha cambiato i file con 2100 aggiunte e 301 eliminazioni
  1. 44 20
      README.md
  2. 1 0
      composer.json
  3. 81 0
      src/Assigner/AutoIncrementAssigner.php
  4. 10 0
      src/Assigner/CommitAssignerInterface.php
  5. 47 0
      src/Assigner/SlugAssigner.php
  6. 26 0
      src/Assigner/TimestampAssigner.php
  7. 27 0
      src/Assigner/TokenAssigner.php
  8. 26 0
      src/Assigner/UuidAssigner.php
  9. 10 0
      src/Assigner/ValueAssignerInterface.php
  10. 73 0
      src/Collector/EntityDirCollector.php
  11. 40 12
      src/Command/DatabaseSyncCommand.php
  12. 61 35
      src/Command/Migration/MigrationDiffCommand.php
  13. 7 0
      src/Entity/SystemEntityInterface.php
  14. 48 60
      src/EntityManager.php
  15. 5 7
      src/EntityManagerInterface.php
  16. 39 0
      src/Event/PostCreateEvent.php
  17. 12 2
      src/Event/PreCreateEvent.php
  18. 0 35
      src/EventListener/CreatedAtListener.php
  19. 24 0
      src/EventListener/PostCreateEventListener.php
  20. 44 0
      src/EventListener/PreCreateEventListener.php
  21. 24 0
      src/EventListener/PreUpdateEventListener.php
  22. 0 34
      src/EventListener/UpdatedAtListener.php
  23. 2 2
      src/Generator/SchemaDiffGenerator.php
  24. 104 0
      src/Internal/Entity/PaperKeyValue.php
  25. 49 0
      src/Manager/PaperKeyValueManager.php
  26. 58 0
      src/Manager/PaperSequenceManager.php
  27. 2 2
      src/Mapper/ColumnMapper.php
  28. 19 0
      src/Mapping/Column/AnyColumn.php
  29. 60 0
      src/Mapping/Column/AutoIncrementColumn.php
  30. 10 1
      src/Mapping/Column/Column.php
  31. 45 0
      src/Mapping/Column/SlugColumn.php
  32. 0 4
      src/Mapping/Column/StringColumn.php
  33. 34 0
      src/Mapping/Column/TokenColumn.php
  34. 18 0
      src/Mapping/Column/UuidColumn.php
  35. 18 9
      src/Michel/Package/MichelPaperORMPackage.php
  36. 0 1
      src/Migration/PaperMigration.php
  37. 98 0
      src/PaperConfiguration.php
  38. 5 13
      src/PaperConnection.php
  39. 11 3
      src/Persistence/EntityPersistence.php
  40. 1 1
      src/Platform/AbstractPlatform.php
  41. 25 0
      src/Platform/MariaDBPlatform.php
  42. 25 0
      src/Platform/SqlitePlatform.php
  43. 1 3
      src/Repository/Repository.php
  44. 46 0
      src/Tools/EntityAccessor.php
  45. 25 4
      src/Tools/EntityExplorer.php
  46. 58 0
      src/Tools/IDBuilder.php
  47. 17 0
      src/Tools/NamingStrategy.php
  48. 28 0
      src/Tools/Slugger.php
  49. 159 0
      src/Types/AnyType.php
  50. 76 0
      tests/Common/NamingStrategyTest.php
  51. 2 1
      tests/Common/OrmTestMemory.php
  52. 78 0
      tests/Common/SluggerTest.php
  53. 2 1
      tests/DatabaseShowTablesCommandTest.php
  54. 11 9
      tests/DatabaseSyncCommandTest.php
  55. 15 0
      tests/Entity/CommentTest.php
  56. 76 0
      tests/Entity/InvoiceTest.php
  57. 18 2
      tests/Entity/PostTest.php
  58. 19 3
      tests/Entity/UserTest.php
  59. 40 8
      tests/Helper/DataBaseHelperTest.php
  60. 35 25
      tests/MigrationTest.php
  61. 87 1
      tests/PersistAndFlushTest.php
  62. 2 1
      tests/PlatformDiffTest.php
  63. 2 1
      tests/PlatformTest.php
  64. 68 0
      tests/RegistryTest.php
  65. 2 1
      tests/RepositoryTest.php

+ 44 - 20
README.md

@@ -21,7 +21,7 @@ PaperORM is available via **Composer** and installs in seconds.
 
 ### 📦 Via Composer (recommended)
 ```bash
-composer require phpdevcommunity/paper-orm:1.0.11-alpha
+composer require phpdevcommunity/paper-orm:1.0.14-alpha
 ```  
 
 ### 🔧 Minimal Configuration
@@ -32,16 +32,18 @@ Create a simple configuration file to connect PaperORM to your database:
 require_once 'vendor/autoload.php';
 
 use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
+
+// --- Basic SQLite configuration ---
+$configuration = PaperConfiguration::fromArray([
+    'driver' => 'sqlite',
+    'user' => null,
+    'password' => null,
+    'memory' => true
+], false); // Set to true to enable debug mode (logs queries and ORM operations)
 
-// Basic configuration SQLite
-$entityManager = new EntityManager([
-            'driver' => 'sqlite',
-            'user' => null,
-            'password' => null,
-            'memory' => true,
-]);
 // Basic configuration MySQL/Mariadb
-$entityManager = new EntityManager([
+$configuration = PaperConfiguration::fromArray([
             'driver' => 'mariadb',
             'host' => '127.0.0.1',
             'port' => 3306,
@@ -49,7 +51,17 @@ $entityManager = new EntityManager([
             'user' => 'root',
             'password' => 'root',
             'charset' => 'utf8mb4',
-]);
+], false);  // Set to true to enable debug mode (logs queries and ORM operations)
+
+// --- Optional event listener registration ---
+// Called automatically before any entity creation
+$configuration->withListener(PreCreateEvent::class, new App\Listener\PreCreateListener());
+
+// --- Optional SQL logger ---
+// Use any PSR-3 compatible logger (e.g. Monolog) to log all executed queries
+$configuration->withLogger(new Monolog());
+
+$em = EntityManager::createFromConfig($configuration);
 ```
 
 ✅ **PaperORM is now ready to use!**
@@ -245,7 +257,7 @@ PaperORM est disponible via **Composer** et s'installe en quelques secondes.
 
 ### 📦 Via Composer (recommandé)
 ```bash
-composer require phpdevcommunity/paper-orm:1.0.11-alpha
+composer require phpdevcommunity/paper-orm:1.0.14-alpha
 ```  
 
 ### 🔧 Configuration minimale
@@ -256,16 +268,18 @@ Créez un fichier de configuration simple pour connecter PaperORM à votre base
 require_once 'vendor/autoload.php';
 
 use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
+
+// --- Basic SQLite configuration ---
+$configuration = PaperConfiguration::fromArray([
+    'driver' => 'sqlite',
+    'user' => null,
+    'password' => null,
+    'memory' => true
+], false); // Mettre à true pour activer le mode debug (journalisation des requêtes et opérations ORM)
 
-// Basic configuration SQLite
-$entityManager = new EntityManager([
-            'driver' => 'sqlite',
-            'user' => null,
-            'password' => null,
-            'memory' => true,
-]);
 // Basic configuration MySQL/Mariadb
-$entityManager = new EntityManager([
+$configuration = PaperConfiguration::fromArray([
             'driver' => 'mariadb',
             'host' => '127.0.0.1',
             'port' => 3306,
@@ -273,7 +287,17 @@ $entityManager = new EntityManager([
             'user' => 'root',
             'password' => 'root',
             'charset' => 'utf8mb4',
-]);
+], false);  // Set to true to enable debug mode (logs queries and ORM operations)
+
+// --- Enregistrement optionnel d’un écouteur d’événement ---
+// Appelé automatiquement avant chaque création d’entité
+$configuration->withListener(PreCreateEvent::class, new App\Listener\PreCreateListener());
+
+// --- Journalisation SQL optionnelle ---
+// Permet de journaliser toutes les requêtes exécutées via un logger compatible PSR-3 (ex. Monolog
+$configuration->withLogger(new Monolog());
+
+$em = EntityManager::createFromConfig($configuration);
 ```
 
 ✅ **PaperORM est maintenant prêt à être utilisé !**  

+ 1 - 0
composer.json

@@ -23,6 +23,7 @@
     "ext-pdo": "*",
     "ext-json": "*",
     "ext-ctype": "*",
+    "ext-iconv": "*",
     "phpdevcommunity/relational-query": "^1.0",
     "phpdevcommunity/php-console": "^1.0",
     "phpdevcommunity/michel-package-starter": "^1.0",

+ 81 - 0
src/Assigner/AutoIncrementAssigner.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Assigner;
+
+use PhpDevCommunity\PaperORM\Manager\PaperSequenceManager;
+use PhpDevCommunity\PaperORM\Mapping\Column\AutoIncrementColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+use PhpDevCommunity\PaperORM\Tools\IDBuilder;
+use PhpDevCommunity\PaperORM\Tools\EntityAccessor;
+
+final class AutoIncrementAssigner implements ValueAssignerInterface, CommitAssignerInterface
+{
+    private PaperSequenceManager $sequenceManager;
+
+    public function __construct(PaperSequenceManager $sequenceManager)
+    {
+        $this->sequenceManager = $sequenceManager;
+    }
+
+    public function assign(object $entity, Column $column): void
+    {
+        if (!$column instanceof AutoIncrementColumn) {
+            throw new \InvalidArgumentException(sprintf(
+                'AutoIncrementAssigner::assign(): expected instance of %s, got %s.',
+                AutoIncrementColumn::class,
+                get_class($column)
+            ));
+        }
+
+        $result = self::determineSequenceIdentifiers($column);
+        $prefix = $result['sequence'];
+        $counter = $this->sequenceManager->peek($result['key']);
+        $formatted = sprintf(
+            '%s%s',
+            $prefix,
+            str_pad((string)$counter, $column->getPad(), '0', STR_PAD_LEFT)
+        );
+        $property = $column->getProperty();
+        EntityAccessor::setValue($entity, $property, $formatted);
+    }
+    public function commit(Column $column): void
+    {
+        if (!$column instanceof AutoIncrementColumn) {
+            throw new \InvalidArgumentException(sprintf(
+                'AutoIncrementAssigner::commit(): expected instance of %s, got %s.',
+                AutoIncrementColumn::class,
+                get_class($column)
+            ));
+        }
+
+        $this->sequenceManager->increment(self::determineSequenceIdentifiers($column)['key']);
+    }
+
+    /**
+     * @param AutoIncrementColumn $column
+     * @return array{sequence: string, key: string}
+     * @throws \RandomException
+     */
+    private static function determineSequenceIdentifiers(AutoIncrementColumn $column): array
+    {
+        $key = $column->getKey();
+
+        if (empty($key)) {
+            throw new \LogicException(sprintf(
+                'AutoIncrementColumn "%s": a non-empty key (sequence or table.sequence) must be defined.',
+                $column->getProperty()
+            ));
+        }
+
+        $prefix = $column->getPrefix();
+        $sequenceName = !empty($prefix) ? IDBuilder::generate($prefix) : '';
+        if (!empty($sequenceName)) {
+            $key = sprintf('%s.%s', $key, $sequenceName);
+        }
+        return [
+            'sequence' => $sequenceName,
+            'key' => $key,
+        ];
+    }
+
+}

+ 10 - 0
src/Assigner/CommitAssignerInterface.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Assigner;
+
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+
+interface CommitAssignerInterface
+{
+    public function commit(Column $column): void;
+}

+ 47 - 0
src/Assigner/SlugAssigner.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Assigner;
+
+use InvalidArgumentException;
+use LogicException;
+use PhpDevCommunity\PaperORM\Collection\ObjectStorage;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
+use PhpDevCommunity\PaperORM\Tools\EntityAccessor;
+use PhpDevCommunity\PaperORM\Tools\Slugger;
+
+final class SlugAssigner implements ValueAssignerInterface
+{
+    public function assign(object $entity, Column $column): void
+    {
+        if (!$column instanceof SlugColumn) {
+            throw new InvalidArgumentException(sprintf(
+                'SlugAssigner::assign(): expected instance of %s, got %s.',
+                SlugColumn::class,
+                get_class($column)
+            ));
+        }
+        if (EntityAccessor::getValue($entity, $column->getProperty()) !== null) {
+            return;
+        }
+
+        $storage = new ObjectStorage(ColumnMapper::getColumns($entity));
+        $from = $column->getFrom();
+        $separator = $column->getSeparator();
+        $values = [];
+        foreach ($from as $field) {
+            $col = $storage->findOneByMethod('getProperty', $field);
+            if (!$col instanceof Column) {
+                throw new LogicException(sprintf(
+                    'Cannot set slug: expected column "%s" in entity "%s".',
+                    $field,
+                    get_class($entity)
+                ));
+            }
+            $values[$field] = EntityAccessor::getValue($entity, $field);
+        }
+        EntityAccessor::setValue($entity, $column->getProperty(), Slugger::slugify($values, $separator));
+    }
+
+}

+ 26 - 0
src/Assigner/TimestampAssigner.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Assigner;
+
+use DateTimeImmutable;
+use InvalidArgumentException;
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Tools\EntityAccessor;
+
+final class TimestampAssigner implements ValueAssignerInterface
+{
+    public function assign(object $entity, Column $column): void
+    {
+        if (!$column instanceof TimestampColumn) {
+            throw new InvalidArgumentException(sprintf(
+                'TimestampAssigner::assign(): expected instance of %s, got %s.',
+                TimestampColumn::class,
+                get_class($column)
+            ));
+        }
+
+        $property = $column->getProperty();
+        EntityAccessor::setValue($entity, $property, new DateTimeImmutable('now'));
+    }
+}

+ 27 - 0
src/Assigner/TokenAssigner.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Assigner;
+
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TokenColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\UuidColumn;
+use PhpDevCommunity\PaperORM\Tools\EntityAccessor;
+use PhpDevCommunity\PaperORM\Tools\IDBuilder;
+
+final class TokenAssigner implements ValueAssignerInterface
+{
+    public function assign(object $entity, Column $column): void
+    {
+        if (!$column instanceof TokenColumn) {
+            throw new \InvalidArgumentException(sprintf(
+                'TokenAssigner::assign(): expected instance of %s, got %s.',
+                TokenColumn::class,
+                get_class($column)
+            ));
+        }
+
+        $property = $column->getProperty();
+        EntityAccessor::setValue($entity, $property, IDBuilder::generate(sprintf("{TOKEN%s}", $column->getLength())));
+    }
+}

+ 26 - 0
src/Assigner/UuidAssigner.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Assigner;
+
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\UuidColumn;
+use PhpDevCommunity\PaperORM\Tools\EntityAccessor;
+use PhpDevCommunity\PaperORM\Tools\IDBuilder;
+
+final class UuidAssigner implements ValueAssignerInterface
+{
+    public function assign(object $entity, Column $column): void
+    {
+        if (!$column instanceof UuidColumn) {
+            throw new \InvalidArgumentException(sprintf(
+                'UuidAssigner::assign(): expected instance of %s, got %s.',
+                UuidColumn::class,
+                get_class($column)
+            ));
+        }
+
+        $property = $column->getProperty();
+        EntityAccessor::setValue($entity, $property, IDBuilder::generate('{UUID}'));
+    }
+}

+ 10 - 0
src/Assigner/ValueAssignerInterface.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Assigner;
+
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+
+interface ValueAssignerInterface
+{
+    public function assign(object $entity, Column $column): void;
+}

+ 73 - 0
src/Collector/EntityDirCollector.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Collector;
+
+final class EntityDirCollector
+{
+    
+    /** @var string[] */
+    private array $dirs = [];
+
+    public static function bootstrap(array $dirs = []): EntityDirCollector
+    {
+        // Core entity directories of the ORM (always loaded first)
+        $coreDirs = [
+            dirname(__DIR__) . '/Internal/Entity',
+        ];
+        $dirs = array_merge($coreDirs, $dirs);
+        return new EntityDirCollector($dirs);
+    }
+
+    /**
+     * @param string|string[] $dirs
+     */
+    private function __construct(array $dirs = [])
+    {
+        foreach ($dirs as $index => $dir) {
+            if (!is_string($dir)) {
+                $given = gettype($dir);
+                throw new \InvalidArgumentException(sprintf(
+                    'EntityDirCollector::__construct(): each directory must be a string, %s given at index %d.',
+                    $given,
+                    $index
+                ));
+            }
+
+            if (empty($dir)) {
+                throw new \InvalidArgumentException(sprintf(
+                    'EntityDirCollector::__construct(): directory at index %d is an empty string.',
+                    $index
+                ));
+            }
+
+            $this->add($dir);
+        }
+    }
+
+    public function add(string $dir): void
+    {
+        if (!is_dir($dir)) {
+            throw new \InvalidArgumentException(sprintf(
+                'EntityDirCollector::add(): directory "%s" does not exist.',
+                $dir
+            ));
+        }
+        $dir = rtrim($dir, '/');
+        if (!in_array($dir, $this->dirs, true)) {
+            $this->dirs[] = $dir;
+        }
+    }
+
+    /**
+     * @return string[]
+     */
+    public function all(): array
+    {
+        return $this->dirs;
+    }
+
+    public function count(): int
+    {
+        return count($this->dirs);
+    }
+}

+ 40 - 12
src/Command/DatabaseSyncCommand.php

@@ -2,33 +2,34 @@
 
 namespace PhpDevCommunity\PaperORM\Command;
 
+use LogicException;
 use PhpDevCommunity\Console\Command\CommandInterface;
 use PhpDevCommunity\Console\InputInterface;
 use PhpDevCommunity\Console\Option\CommandOption;
 use PhpDevCommunity\Console\Output\ConsoleOutput;
 use PhpDevCommunity\Console\OutputInterface;
+use PhpDevCommunity\PaperORM\Collector\EntityDirCollector;
 use PhpDevCommunity\PaperORM\Migration\PaperMigration;
 use PhpDevCommunity\PaperORM\Tools\EntityExplorer;
 
 class DatabaseSyncCommand implements CommandInterface
 {
-
     private PaperMigration $paperMigration;
 
-    private ?string $env;
+    private EntityDirCollector $entityDirCollector;
 
-    private string $entityDir;
+    private ?string $env;
 
     /**
      * @param PaperMigration $paperMigration
-     * @param string $entityDir
+     * @param EntityDirCollector $entityDirCollector
      * @param string|null $env
      */
-    public function __construct(PaperMigration $paperMigration, string $entityDir, ?string $env = null)
+    public function __construct(PaperMigration $paperMigration, EntityDirCollector $entityDirCollector, ?string $env = null)
     {
         $this->paperMigration = $paperMigration;
+        $this->entityDirCollector = $entityDirCollector;
         $this->env = $env;
-        $this->entityDir = $entityDir;
     }
 
     public function getName(): string
@@ -56,22 +57,49 @@ class DatabaseSyncCommand implements CommandInterface
     public function execute(InputInterface $input, OutputInterface $output): void
     {
         $io = ConsoleOutput::create($output);
+        $verbose = $input->getOptionValue('verbose');
         if (!$this->isEnabled()) {
-            throw new \LogicException('This command is only available in `dev` environment.');
+            throw new LogicException('This command is only available in `dev` environment.');
+        }
+
+        if ($this->entityDirCollector->count() === 0) {
+            $suggested = getcwd() . '/src/Entity';
+
+            throw new LogicException(sprintf(
+                "No entity directories registered in %s.\n" .
+                "You must register at least one directory when building the application.\n\n" .
+                "Example:\n" .
+                "    \$collector = new EntityDirCollector(['%s']);\n" .
+                "    \$command = new %s(\$paperMigration, \$collector);",
+                static::class,
+                $suggested,
+                static::class
+            ));
         }
 
         $noExecute = $input->getOptionValue('no-execute');
         $platform = $this->paperMigration->getEntityManager()->getPlatform();
-
         $io->title('Starting database sync on ' . $platform->getDatabaseName());
         $io->list([
             'Database : ' . $platform->getDatabaseName(),
-            'Entities directory : ' . $this->entityDir
+            'Entities directories : ' . count($this->entityDirCollector->all())
         ]);
+        if ($verbose) {
+            $io->listKeyValues($this->entityDirCollector->all());
+        }
 
-        $entities = EntityExplorer::getEntities([$this->entityDir]);
-        $io->title('Number of entities detected: ' . count($entities));
-        $io->listKeyValues($entities);
+        $entities = EntityExplorer::getEntities($this->entityDirCollector->all());
+        $normalEntities = $entities['normal'];
+        $systemEntities = $entities['system'];
+        $entities = array_merge($normalEntities, $systemEntities);
+        $io->title('Detected entities');
+        $io->list([
+            'Normal entities : ' . count($normalEntities),
+            'System entities : ' . count($systemEntities),
+        ]);
+        if ($verbose) {
+            $io->listKeyValues($entities);
+        }
 
         $updates = $this->paperMigration->getSqlDiffFromEntities($entities);
         if (empty($updates)) {

+ 61 - 35
src/Command/Migration/MigrationDiffCommand.php

@@ -2,25 +2,26 @@
 
 namespace PhpDevCommunity\PaperORM\Command\Migration;
 
+use LogicException;
 use PhpDevCommunity\Console\Command\CommandInterface;
 use PhpDevCommunity\Console\InputInterface;
-use PhpDevCommunity\Console\Option\CommandOption;
 use PhpDevCommunity\Console\Output\ConsoleOutput;
 use PhpDevCommunity\Console\OutputInterface;
-use PhpDevCommunity\FileSystem\Tools\FileExplorer;
-use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\Collector\EntityDirCollector;
 use PhpDevCommunity\PaperORM\Migration\PaperMigration;
 use PhpDevCommunity\PaperORM\Tools\EntityExplorer;
+use SplFileObject;
 
 class MigrationDiffCommand implements CommandInterface
 {
     private PaperMigration $paperMigration;
-    private ?string $defaultEntitiesDir;
 
-    public function __construct(PaperMigration $paperMigration, ?string $defaultEntitiesDir = null)
+    private EntityDirCollector $entityDirCollector;
+
+    public function __construct(PaperMigration $paperMigration, EntityDirCollector $entityDirCollector)
     {
         $this->paperMigration = $paperMigration;
-        $this->defaultEntitiesDir = $defaultEntitiesDir;
+        $this->entityDirCollector = $entityDirCollector;
     }
 
     public function getName(): string
@@ -36,7 +37,6 @@ class MigrationDiffCommand implements CommandInterface
     public function getOptions(): array
     {
         return [
-            new CommandOption('entities-dir', null, 'The directory where the entities are', false)
         ];
     }
 
@@ -48,46 +48,72 @@ class MigrationDiffCommand implements CommandInterface
     public function execute(InputInterface $input, OutputInterface $output): void
     {
         $io = ConsoleOutput::create($output);
-
-        $entitiesDir = $this->defaultEntitiesDir;
-        $printOutput = $input->getOptionValue('verbose');
-        if ($input->hasOption('entities-dir')) {
-            $entitiesDir = $input->getOptionValue('entities-dir');
-        }
-
-        if ($entitiesDir === null) {
-            throw new \LogicException('The --entities-dir option is required');
+        $verbose = $input->getOptionValue('verbose');
+
+        if ($this->entityDirCollector->count() === 0) {
+            $suggested = getcwd() . '/src/Entity';
+
+            throw new LogicException(sprintf(
+                "No entity directories registered in %s.\n" .
+                "You must register at least one directory when building the application.\n\n" .
+                "Example:\n" .
+                "    \$collector = new EntityDirCollector(['%s']);\n" .
+                "    \$command = new %s(\$paperMigration, \$collector);",
+                static::class,
+                $suggested,
+                static::class
+            ));
         }
 
         $platform = $this->paperMigration->getEntityManager()->getPlatform();
-
         $io->title('Starting migration diff on ' . $platform->getDatabaseName());
         $io->list([
             'Database : ' . $platform->getDatabaseName(),
-            'Entities directory : ' . $entitiesDir
+            'Entities directories : ' . implode(', ', $this->entityDirCollector->all())
+        ]);
+
+        $entities = EntityExplorer::getEntities($this->entityDirCollector->all());
+        $normalEntities = $entities['normal'];
+        $systemEntities = $entities['system'];
+        $io->title('Detected entities');
+        $io->list([
+            'Normal entities : ' . count($normalEntities),
+            'System entities : ' . count($systemEntities),
         ]);
+        if ($verbose) {
+            $io->listKeyValues(array_merge($normalEntities, $systemEntities));
+        }
 
-        $entities = EntityExplorer::getEntities([$entitiesDir]);
-        $io->title('Number of entities detected: ' . count($entities));
-        $io->listKeyValues($entities);
+        $fileApp = $this->paperMigration->generateMigrationFromEntities($normalEntities);
+        if ($fileApp === null) {
+            $io->info('No application migration file was generated — schema already in sync.');
+        } else {
+            $io->success('✔ Application migration file generated: ' . $fileApp);
+        }
 
-        $file = $this->paperMigration->generateMigrationFromEntities($entities);
-        if ($file === null) {
-            $io->info('No migration file was generated — all entities are already in sync with the database schema.');
-            return;
+        $fileSystem = $this->paperMigration->generateMigrationFromEntities($systemEntities);
+        if ($fileSystem === null) {
+            $io->info('No system migration changes detected.');
+        } else {
+            $io->success('✔ System migration file generated: ' . $fileSystem);
         }
 
-        if ($printOutput === true) {
-            $splFile = new \SplFileObject($file);
-            $lines = [];
-            while (!$splFile->eof()) {
-                $lines[] = $splFile->fgets();
+        if ($verbose === true) {
+            foreach ([$fileSystem, $fileApp] as $file) {
+                if ($file === null || !is_file($file)) {
+                    continue;
+                }
+
+                $io->title('Contents of: ' . basename($file));
+                $splFile = new SplFileObject($file);
+                $lines = [];
+                while (!$splFile->eof()) {
+                    $lines[] = $splFile->fgets();
+                }
+                unset($splFile);
+                $io->listKeyValues($lines);
             }
-            unset($splFile);
-            $io->listKeyValues($lines);
         }
-
-        $io->success('Migration file successfully generated: ' . $file);
+        $io->success('Migration diff process completed successfully.');
     }
-
 }

+ 7 - 0
src/Entity/SystemEntityInterface.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Entity;
+
+interface SystemEntityInterface
+{
+}

+ 48 - 60
src/EntityManager.php

@@ -3,25 +3,16 @@
 namespace PhpDevCommunity\PaperORM;
 
 use PhpDevCommunity\Listener\EventDispatcher;
-use PhpDevCommunity\Listener\ListenerProvider;
 use PhpDevCommunity\PaperORM\Cache\EntityMemcachedCache;
-use PhpDevCommunity\PaperORM\Driver\DriverManager;
-use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
-use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
-use PhpDevCommunity\PaperORM\EventListener\CreatedAtListener;
-use PhpDevCommunity\PaperORM\EventListener\UpdatedAtListener;
+use PhpDevCommunity\PaperORM\Manager\PaperKeyValueManager;
+use PhpDevCommunity\PaperORM\Manager\PaperSequenceManager;
 use PhpDevCommunity\PaperORM\Mapper\EntityMapper;
-use PhpDevCommunity\PaperORM\Parser\DSNParser;
 use PhpDevCommunity\PaperORM\Platform\PlatformInterface;
-use PhpDevCommunity\PaperORM\Proxy\ProxyFactory;
 use PhpDevCommunity\PaperORM\Repository\Repository;
 use Psr\EventDispatcher\EventDispatcherInterface;
-use Psr\EventDispatcher\ListenerProviderInterface;
-use Psr\Log\LoggerInterface;
 
 class EntityManager implements EntityManagerInterface
 {
-
     private PaperConnection $connection;
 
     private UnitOfWork $unitOfWork;
@@ -33,44 +24,23 @@ class EntityManager implements EntityManagerInterface
 
     private EntityMemcachedCache $cache;
 
-    private ListenerProviderInterface $listener;
     private EventDispatcherInterface $dispatcher;
     private ?PlatformInterface $platform = null;
+    private PaperKeyValueManager $keyValueManager;
+    private PaperSequenceManager $sequenceManager;
 
-    public static function createFromDsn(string $dsn, bool $debug = false, LoggerInterface $logger = null, array $listeners = []): self
+    public static function createFromConfig(PaperConfiguration $config): self
     {
-        if (empty($dsn)) {
-            throw new \LogicException('Cannot create an EntityManager from an empty DSN.');
-        }
-        $params = DSNParser::parse($dsn);
-        $params['extra']['debug'] = $debug;
-        if ($logger !== null) {
-            $params['extra']['logger'] = $logger;
-        }
-        $params['extra']['listeners'] = $listeners;
-        return new self($params);
+        return new self($config);
     }
-
-    public function __construct(array $config = [])
+    private function __construct(PaperConfiguration $config)
     {
-        if (!isset($config['driver'])) {
-            throw new \InvalidArgumentException('Missing "driver" in EntityManager configuration.');
-        }
-
-        $this->connection = DriverManager::createConnection($config['driver'], $config);
-        $this->unitOfWork = new UnitOfWork();
-        $this->cache = new EntityMemcachedCache();
-        $this->listener = (new ListenerProvider())
-            ->addListener(PreCreateEvent::class, new CreatedAtListener())
-            ->addListener(PreUpdateEvent::class, new UpdatedAtListener());
-
-        $listeners = $config['extra']['listeners'] ?? [];
-        foreach ((array) $listeners as $event => $listener) {
-            foreach ((array) $listener as $l) {
-                $this->addEventListener($event, $l);
-            }
-        }
-        $this->dispatcher = new EventDispatcher($this->listener);
+        $this->connection = $config->getConnection();
+        $this->unitOfWork = $config->getUnitOfWork();
+        $this->cache = $config->getCache();
+        $this->dispatcher = new EventDispatcher($config->getListeners());
+        $this->keyValueManager = new PaperKeyValueManager($this);
+        $this->sequenceManager = new PaperSequenceManager($this->keyValueManager);
     }
 
     public function persist(object $entity): void
@@ -83,43 +53,67 @@ class EntityManager implements EntityManagerInterface
         $this->unitOfWork->remove($entity);
     }
 
-    public function flush(): void
+    public function flush(object $entity = null ): void
     {
-        foreach ($this->unitOfWork->getEntityInsertions() as &$entity) {
-            $repository = $this->getRepository(get_class($entity));
-            $repository->insert($entity);
-            $this->unitOfWork->unsetEntity($entity);
+        foreach ($this->unitOfWork->getEntityInsertions() as &$entityToInsert) {
+            if ($entity && $entity !== $entityToInsert) {
+                continue;
+            }
+            $repository = $this->getRepository(get_class($entityToInsert));
+            $repository->insert($entityToInsert);
+            $this->unitOfWork->unsetEntity($entityToInsert);
         }
 
         foreach ($this->unitOfWork->getEntityUpdates() as $entityToUpdate) {
+            if ($entity && $entity !== $entityToUpdate) {
+                continue;
+            }
             $repository = $this->getRepository(get_class($entityToUpdate));
             $repository->update($entityToUpdate);
             $this->unitOfWork->unsetEntity($entityToUpdate);
         }
 
         foreach ($this->unitOfWork->getEntityDeletions() as $entityToDelete) {
+            if ($entity && $entity !== $entityToDelete) {
+                continue;
+            }
             $repository = $this->getRepository(get_class($entityToDelete));
             $repository->delete($entityToDelete);
             $this->unitOfWork->unsetEntity($entityToDelete);
         }
 
+        if ($entity) {
+            $this->unitOfWork->unsetEntity($entity);
+            return;
+        }
         $this->unitOfWork->clear();
     }
 
+    public function registry(): PaperKeyValueManager
+    {
+        return $this->keyValueManager;
+    }
+
+    public function sequence(): PaperSequenceManager
+    {
+        return $this->sequenceManager;
+    }
+
     public function getRepository(string $entity): Repository
     {
         $repositoryName = EntityMapper::getRepositoryName($entity);
         if ($repositoryName === null) {
-            $repositoryName = 'ProxyRepository'.$entity;
+            $repositoryName = 'ProxyRepository' . $entity;
         }
 
         $dispatcher = $this->dispatcher;
         if (!isset($this->repositories[$repositoryName])) {
             if (!class_exists($repositoryName)) {
-                $repository = new class($entity, $this, $dispatcher) extends Repository
-                {
+                $repository = new class($entity, $this, $dispatcher) extends Repository {
                     private string $entityName;
-                    public function __construct($entityName, EntityManager $em, EventDispatcherInterface $dispatcher = null)  {
+
+                    public function __construct($entityName, EntityManager $em, EventDispatcherInterface $dispatcher = null)
+                    {
                         $this->entityName = $entityName;
                         parent::__construct($em, $dispatcher);
                     }
@@ -129,13 +123,13 @@ class EntityManager implements EntityManagerInterface
                         return $this->entityName;
                     }
                 };
-            }else {
+            } else {
                 $repository = new $repositoryName($this, $dispatcher);
             }
             $this->repositories[$repositoryName] = $repository;
         }
 
-        return  $this->repositories[$repositoryName];
+        return $this->repositories[$repositoryName];
     }
 
 
@@ -164,10 +158,4 @@ class EntityManager implements EntityManagerInterface
         $this->getCache()->clear();
     }
 
-    public function addEventListener(string $eventType, callable $callable): self
-    {
-        $this->listener->addListener($eventType, $callable);
-        return $this;
-    }
-
 }

+ 5 - 7
src/EntityManagerInterface.php

@@ -3,23 +3,21 @@
 namespace PhpDevCommunity\PaperORM;
 
 use PhpDevCommunity\PaperORM\Cache\EntityMemcachedCache;
+use PhpDevCommunity\PaperORM\Manager\PaperKeyValueManager;
+use PhpDevCommunity\PaperORM\Manager\PaperSequenceManager;
 use PhpDevCommunity\PaperORM\Platform\PlatformInterface;
 use PhpDevCommunity\PaperORM\Repository\Repository;
 
 interface EntityManagerInterface
 {
     public function persist(object $entity): void;
-
     public function remove(object $entity): void;
-
-    public function flush(): void;
-
+    public function flush(object $entity = null ): void;
+    public function registry(): PaperKeyValueManager;
+    public function sequence(): PaperSequenceManager;
     public function getRepository(string $entity): Repository;
-
     public function getPlatform(): PlatformInterface;
-
     public function getConnection(): PaperConnection;
     public function getCache(): EntityMemcachedCache;
-
     public function clear(): void;
 }

+ 39 - 0
src/Event/PostCreateEvent.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Event;
+
+use PhpDevCommunity\Listener\Event;
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\EntityManagerInterface;
+
+class PostCreateEvent extends Event
+{
+    private EntityManagerInterface $em;
+    private EntityInterface $entity;
+
+    /**
+     * PreCreateEvent constructor.
+     *
+     * @param EntityManagerInterface $em
+     * @param EntityInterface $entity
+     */
+    public function __construct(EntityManagerInterface $em, EntityInterface $entity)
+    {
+        $this->entity = $entity;
+        $this->em = $em;
+    }
+
+
+    public function getEntity(): EntityInterface
+    {
+        return $this->entity;
+    }
+
+    /**
+     * @return EntityManagerInterface
+     */
+    public function getEm(): EntityManagerInterface
+    {
+        return $this->em;
+    }
+}

+ 12 - 2
src/Event/PreCreateEvent.php

@@ -4,20 +4,23 @@ namespace PhpDevCommunity\PaperORM\Event;
 
 use PhpDevCommunity\Listener\Event;
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\EntityManagerInterface;
 
 class PreCreateEvent extends Event
 {
-
+    private EntityManagerInterface $em;
     private EntityInterface $entity;
 
     /**
      * PreCreateEvent constructor.
      *
+     * @param EntityManagerInterface $em
      * @param EntityInterface $entity
      */
-    public function __construct(EntityInterface $entity)
+    public function __construct(EntityManagerInterface $em, EntityInterface $entity)
     {
         $this->entity = $entity;
+        $this->em = $em;
     }
 
 
@@ -26,4 +29,11 @@ class PreCreateEvent extends Event
         return $this->entity;
     }
 
+    /**
+     * @return EntityManagerInterface
+     */
+    public function getEm(): EntityManagerInterface
+    {
+        return $this->em;
+    }
 }

+ 0 - 35
src/EventListener/CreatedAtListener.php

@@ -1,35 +0,0 @@
-<?php
-
-namespace PhpDevCommunity\PaperORM\EventListener;
-
-use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
-use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
-use PhpDevCommunity\PaperORM\Mapping\Column\Column;
-use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
-
-class CreatedAtListener
-{
-    public function __invoke(PreCreateEvent $event)
-    {
-        $entity = $event->getEntity();
-
-        foreach (ColumnMapper::getColumns($entity) as $column) {
-            if ($column instanceof TimestampColumn && $column->isOnCreated()) {
-                $property = $column->getProperty();
-                $method   = "set" . ucfirst($property);
-                if (method_exists($entity, $method)) {
-                    $entity->$method(new \DateTimeImmutable('now'));
-                } elseif (array_key_exists($property, get_object_vars($entity))) {
-                    $entity->$property = new \DateTimeImmutable('now'); // OK car public
-                } else {
-                    throw new \LogicException(sprintf(
-                        'Cannot set created-at timestamp: expected setter "%s()" or a public property "%s" in entity "%s".',
-                        $method,
-                        $property,
-                        get_class($entity)
-                    ));
-                }
-            }
-        }
-    }
-}

+ 24 - 0
src/EventListener/PostCreateEventListener.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\EventListener;
+
+use PhpDevCommunity\PaperORM\Assigner\AutoIncrementAssigner;
+use PhpDevCommunity\PaperORM\Event\PostCreateEvent;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapping\Column\AutoIncrementColumn;
+
+class PostCreateEventListener
+{
+    public function __invoke(PostCreateEvent $event)
+    {
+        $entity = $event->getEntity();
+        $em = $event->getEm();
+
+        $autoIncrementAssigner = new AutoIncrementAssigner($em->sequence());
+        foreach (ColumnMapper::getColumns($entity) as $column) {
+            if ($column instanceof AutoIncrementColumn) {
+                $autoIncrementAssigner->commit($column);
+            }
+        }
+    }
+}

+ 44 - 0
src/EventListener/PreCreateEventListener.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\EventListener;
+
+use PhpDevCommunity\PaperORM\Assigner\AutoIncrementAssigner;
+use PhpDevCommunity\PaperORM\Assigner\SlugAssigner;
+use PhpDevCommunity\PaperORM\Assigner\TimestampAssigner;
+use PhpDevCommunity\PaperORM\Assigner\TokenAssigner;
+use PhpDevCommunity\PaperORM\Assigner\UuidAssigner;
+use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapping\Column\AutoIncrementColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TokenColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\UuidColumn;
+
+class PreCreateEventListener
+{
+    public function __invoke(PreCreateEvent $event)
+    {
+        $entity = $event->getEntity();
+        $em = $event->getEm();
+
+        $autoIncrementAssigner = new AutoIncrementAssigner($em->sequence());
+        $slugAssigner = new SlugAssigner();
+        $timestampAssigner = new TimestampAssigner();
+        $uuidAssigner = new UuidAssigner();
+        $tokenAssigner = new TokenAssigner();
+        foreach (ColumnMapper::getColumns($entity) as $column) {
+            if ($column instanceof TimestampColumn && $column->isOnCreated()) {
+                $timestampAssigner->assign($entity, $column);
+            } elseif ($column instanceof SlugColumn) {
+                $slugAssigner->assign($entity, $column);
+            } elseif ($column instanceof AutoIncrementColumn) {
+                $autoIncrementAssigner->assign($entity, $column);
+            }elseif ($column instanceof UuidColumn) {
+                $uuidAssigner->assign($entity, $column);
+            }elseif ($column instanceof TokenColumn) {
+                $tokenAssigner->assign($entity, $column);
+            }
+        }
+    }
+}

+ 24 - 0
src/EventListener/PreUpdateEventListener.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\EventListener;
+
+use DateTimeImmutable;
+use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Tools\EntityAccessor;
+
+class PreUpdateEventListener
+{
+
+    public function __invoke(PreUpdateEvent $event)
+    {
+        $entity = $event->getEntity();
+        foreach (ColumnMapper::getColumns($entity) as $column) {
+            if ($column instanceof TimestampColumn && $column->isOnUpdated()) {
+                $property = $column->getProperty();
+                EntityAccessor::setValue($entity, $property, new DateTimeImmutable('now'));
+            }
+        }
+    }
+}

+ 0 - 34
src/EventListener/UpdatedAtListener.php

@@ -1,34 +0,0 @@
-<?php
-
-namespace PhpDevCommunity\PaperORM\EventListener;
-
-use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
-use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
-use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
-
-class UpdatedAtListener
-{
-
-    public function __invoke(PreUpdateEvent $event)
-    {
-        $entity = $event->getEntity();
-        foreach (ColumnMapper::getColumns($entity) as $column) {
-            if ($column instanceof TimestampColumn && $column->isOnUpdated()) {
-                $property = $column->getProperty();
-                $method   = "set" . ucfirst($property);
-                if (method_exists($entity, $method)) {
-                    $entity->$method(new \DateTimeImmutable('now'));
-                } elseif (array_key_exists($property, get_object_vars($entity))) {
-                    $entity->$property = new \DateTimeImmutable('now'); // OK car public
-                } else {
-                    throw new \LogicException(sprintf(
-                        'Cannot set created-at timestamp: expected setter "%s()" or a public property "%s" in entity "%s".',
-                        $method,
-                        $property,
-                        get_class($entity)
-                    ));
-                }
-            }
-        }
-    }
-}

+ 2 - 2
src/Generator/SchemaDiffGenerator.php

@@ -38,14 +38,14 @@ final class SchemaDiffGenerator
             }
         }
 
-        list( $sqlUp, $sqlDown) = $this->createTables($tables, $schema, $tablesExist);
+        list( $sqlUp, $sqlDown) = $this->diff($tables, $schema, $tablesExist);
         return [
             'up' => $sqlUp,
             'down' => $sqlDown
         ];
     }
 
-    private function createTables(array $tables,SchemaInterface $schema, array $tablesExist): array
+    private function diff(array $tables, SchemaInterface $schema, array $tablesExist): array
     {
         $sqlUp = [];
         $sqlDown = [];

+ 104 - 0
src/Internal/Entity/PaperKeyValue.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Internal\Entity;
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\Entity\SystemEntityInterface;
+use PhpDevCommunity\PaperORM\Mapping\Column\AnyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Mapping\Entity;
+
+#[Entity(table : 'paper_key_value')]
+class PaperKeyValue implements EntityInterface, SystemEntityInterface
+{
+    #[PrimaryKeyColumn]
+    private ?int $id = null;
+
+    #[StringColumn(name: 'k', length: 100, nullable: false, unique: true)]
+    private ?string $key = null;
+
+    /**
+     * @var mixed
+     */
+    #[AnyColumn(name: 'val')]
+    private $value = null;
+
+    #[TimestampColumn(name: 'created_at', onCreated: true)]
+    private ?\DateTimeInterface $createdAt = null;
+
+    #[TimestampColumn(name: 'updated_at', onCreated: false, onUpdated: true)]
+    private ?\DateTimeInterface $updatedAt = null;
+
+    public function getPrimaryKeyValue() : ?int
+    {
+        return $this->id;
+    }
+
+    public function getKey(): ?string
+    {
+        return $this->key;
+    }
+
+    public function setKey(?string $key): PaperKeyValue
+    {
+        $this->key = $key;
+        return $this;
+    }
+
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    public function setValue($value): PaperKeyValue
+    {
+        $this->value = $value;
+        return $this;
+    }
+
+    public function getCreatedAt(): ?\DateTimeInterface
+    {
+        return $this->createdAt;
+    }
+
+    public function setCreatedAt(?\DateTimeInterface $createdAt): PaperKeyValue
+    {
+        $this->createdAt = $createdAt;
+        return $this;
+    }
+
+    public function getUpdatedAt(): ?\DateTimeInterface
+    {
+        return $this->updatedAt;
+    }
+
+    public function setUpdatedAt(?\DateTimeInterface $updatedAt): PaperKeyValue
+    {
+        $this->updatedAt = $updatedAt;
+        return $this;
+    }
+
+
+    static public function getTableName(): string
+    {
+        return 'paper_key_value';
+    }
+
+    static public function getRepositoryName(): ?string
+    {
+        return null;
+    }
+
+    static public function columnsMapping(): array
+    {
+        return [
+            (new PrimaryKeyColumn())->bindProperty('id'),
+            (new StringColumn('k', 100, false, null, true))->bindProperty('key'),
+            (new AnyColumn('val'))->bindProperty('value'),
+            (new TimestampColumn('created_at', true))->bindProperty('createdAt'),
+            (new TimestampColumn('updated_at', false, true))->bindProperty('updatedAt'),
+        ];
+    }
+
+}

+ 49 - 0
src/Manager/PaperKeyValueManager.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Manager;
+
+use PhpDevCommunity\PaperORM\EntityManagerInterface;
+use PhpDevCommunity\PaperORM\Internal\Entity\PaperKeyValue;
+use PhpDevCommunity\PaperORM\Repository\Repository;
+
+final class PaperKeyValueManager
+{
+    private EntityManagerInterface $em;
+    private Repository $repository;
+    public function __construct(EntityManagerInterface $em)
+    {
+        $this->em = $em;
+        $this->repository = $em->getRepository(PaperKeyValue::class);
+    }
+
+    public function get(string $key)
+    {
+        $kv = $this->repository->findOneBy(['key' => strtolower($key)])->toArray();
+        if (empty($kv)) {
+            return null;
+        }
+        return $kv['value'];
+    }
+
+    public function set(string $key, $value): void
+    {
+        $kv = $this->repository->findOneBy(['key' => strtolower($key)])->toObject();
+        if (!$kv instanceof PaperKeyValue) {
+            $kv = new PaperKeyValue();
+            $kv->setKey(strtolower($key));
+        }
+        $kv->setValue($value);
+
+        $this->em->persist($kv);
+        $this->em->flush($kv);
+    }
+
+    public function remove(string $key): void
+    {
+        if ($kv = $this->repository->findOneBy(['key' => $key])->toObject()) {
+            $this->em->remove($kv);
+            $this->em->flush($kv);
+        }
+    }
+
+}

+ 58 - 0
src/Manager/PaperSequenceManager.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Manager;
+
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+
+final class PaperSequenceManager
+{
+    private PaperKeyValueManager $keyValueManager;
+    private array $cache = [];
+
+    public function __construct(PaperKeyValueManager $keyValueManager)
+    {
+        $this->keyValueManager = $keyValueManager;
+    }
+
+    public function peek(string $key): int
+    {
+        $key = strtolower($key);
+        $next =  $this->getNext($key);
+        $this->cache[$key] = $next;
+        return $next;
+    }
+    public function increment(string $key): void
+    {
+        $key = strtolower($key);
+        $cached = $this->cache[$key] ?? null;
+        $expectedNext = $this->getNext($key);
+        if ($cached !== null && $cached !== $expectedNext) {
+            throw new \RuntimeException(sprintf(
+                'Sequence conflict for key "%s": expected next %d but found %d in storage.',
+                $key,
+                $cached,
+                $expectedNext
+            ));
+        }
+
+        $this->keyValueManager->set($this->resolveKey($key), $expectedNext);
+        unset($this->cache[$key]);
+    }
+
+    public function reset(string $key): void
+    {
+        $this->keyValueManager->set($this->resolveKey($key), 0);
+    }
+
+    private function getNext(string $key) : int
+    {
+        $value = $this->keyValueManager->get($this->resolveKey($key));
+        return $value ? (int)$value + 1 : 1;
+    }
+
+
+    private function resolveKey(string $key): string
+    {
+        return 'sequence.' . $key;
+    }
+}

+ 2 - 2
src/Mapper/ColumnMapper.php

@@ -96,7 +96,7 @@ final class ColumnMapper
     {
         $columns = self::getColumns($class);
         foreach ($columns as $column) {
-            if ($column->getProperty() === $property || $column->getName() === $property) {
+            if ($column->getProperty() === $property) {
                 return $column;
             }
         }
@@ -112,7 +112,7 @@ final class ColumnMapper
     {
         $columns = array_merge(self::getColumns($class) , self::getOneToManyRelations($class));
         foreach ($columns as $column) {
-            if ($column->getProperty() === $property || $column->getName() === $property) {
+            if ($column->getProperty() === $property) {
                 if ($column instanceof JoinColumn) {
                     return $column;
                 }

+ 19 - 0
src/Mapping/Column/AnyColumn.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Mapping\Column;
+
+use Attribute;
+use PhpDevCommunity\PaperORM\Types\AnyType;
+
+#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
+final class AnyColumn extends Column
+{
+    public function __construct(
+        string $name = null,
+        bool   $nullable = false,
+        string $defaultValue = null
+    )
+    {
+        parent::__construct('', $name, AnyType::class, $nullable, $defaultValue);
+    }
+}

+ 60 - 0
src/Mapping/Column/AutoIncrementColumn.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Mapping\Column;
+
+use Attribute;
+use PhpDevCommunity\PaperORM\Types\StringType;
+
+#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
+final class AutoIncrementColumn extends Column
+{
+    private string $key;
+    private int $pad;
+    private ?string $prefix;
+    public function __construct(
+        string  $name = null,
+        string $key = null,
+        int     $pad = 6,
+        ?string $prefix = null,
+        bool    $nullable = false
+    )
+    {
+        if ($pad < 1) {
+            throw new \InvalidArgumentException('AutoIncrementColumn : pad must be at least 1.');
+        }
+
+        if (empty($key)) {
+            throw new \InvalidArgumentException(
+                'AutoIncrementColumn configuration error: A non-empty key (sequence or table.sequence) must be defined.'
+            );
+        }
+
+        if (!preg_match('/^[a-zA-Z0-9_.]+$/', $key)) {
+            throw new \InvalidArgumentException(sprintf(
+                'Invalid key or sequence name "%s": only alphanumeric characters, underscores (_), and dots (.) are allowed.',
+                $key
+            ));
+        }
+
+        $length = strlen($prefix) + $pad;
+        parent::__construct('', $name, StringType::class, $nullable, null, true, $length);
+
+        $this->pad = $pad;
+        $this->prefix = $prefix;
+        $this->key = $key;
+    }
+
+    public function getPad(): int
+    {
+        return $this->pad;
+    }
+
+    public function getPrefix(): ?string
+    {
+        return $this->prefix;
+    }
+    public function getKey(): string
+    {
+        return $this->key;
+    }
+}

+ 10 - 1
src/Mapping/Column/Column.php

@@ -3,6 +3,7 @@
 namespace PhpDevCommunity\PaperORM\Mapping\Column;
 
 use PhpDevCommunity\PaperORM\Mapping\Index;
+use PhpDevCommunity\PaperORM\Tools\NamingStrategy;
 use PhpDevCommunity\PaperORM\Types\StringType;
 use PhpDevCommunity\PaperORM\Types\Type;
 use PhpDevCommunity\PaperORM\Types\TypeFactory;
@@ -34,6 +35,14 @@ abstract class Column
         if (empty($property) && !empty($name)) {
             $property = $name;
         }
+
+        if (!empty($name) && !preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
+            throw new \InvalidArgumentException(sprintf(
+                'Invalid column name "%s": only alphanumeric characters and underscores are allowed.',
+                $name
+            ));
+        }
+
         $this->property = $property;
         $this->name = $name;
         $this->type = $type;
@@ -66,7 +75,7 @@ abstract class Column
     final public function getName(): ?string
     {
         $property = $this->getProperty();
-        return $this->name ?: $property;
+        return $this->name ?:  NamingStrategy::toSnakeCase($property);
     }
 
 

+ 45 - 0
src/Mapping/Column/SlugColumn.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Mapping\Column;
+
+use Attribute;
+use PhpDevCommunity\PaperORM\Types\StringType;
+
+#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
+final class SlugColumn extends Column
+{
+    private array $from;
+    private string $separator;
+
+    public function __construct(
+        string $name = null,
+        array  $from = [],
+        string $separator = '-',
+        int    $length = 128,
+        bool   $nullable = false,
+        bool   $unique = true
+    )
+    {
+        if (empty($separator)) {
+            throw new \InvalidArgumentException('Slug separator cannot be empty.');
+        }
+
+        if (empty($from)) {
+            throw new \InvalidArgumentException('Slug "fields" must reference at least one source column.');
+        }
+
+        parent::__construct('', $name, StringType::class, $nullable, null, $unique, $length);
+        $this->from = $from;
+        $this->separator = $separator;
+    }
+
+    public function getFrom(): array
+    {
+        return $this->from;
+    }
+
+    public function getSeparator(): string
+    {
+        return $this->separator;
+    }
+}

+ 0 - 4
src/Mapping/Column/StringColumn.php

@@ -19,8 +19,4 @@ final class StringColumn extends Column
         parent::__construct('', $name, StringType::class, $nullable, $defaultValue, $unique, $length);
     }
 
-    public function getLength(): int
-    {
-        return intval($this->getFirstArgument());
-    }
 }

+ 34 - 0
src/Mapping/Column/TokenColumn.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Mapping\Column;
+
+use PhpDevCommunity\PaperORM\Types\StringType;
+
+#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
+final class TokenColumn extends Column
+{
+
+    private int $length;
+    public function __construct(
+        string $name = null,
+        int    $length = 128,
+        bool   $nullable = false,
+        ?int $defaultValue = null
+    )
+    {
+        if (!in_array($length, [16, 32, 64, 128])) {
+            throw new \InvalidArgumentException(sprintf(
+                'Token length must be 16, 32, 64 or 128, got %s.',
+                $length
+            ));
+        }
+        parent::__construct('', $name, StringType::class, $nullable, $defaultValue, true,$length);
+
+        $this->length = $length;
+    }
+
+    public function getLength(): int
+    {
+        return $this->length;
+    }
+}

+ 18 - 0
src/Mapping/Column/UuidColumn.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Mapping\Column;
+
+use PhpDevCommunity\PaperORM\Types\StringType;
+
+#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
+final class UuidColumn extends Column
+{
+    public function __construct(
+        string $name = null,
+        bool   $nullable = false,
+        ?int $defaultValue = null
+    )
+    {
+        parent::__construct('', $name, StringType::class, $nullable, $defaultValue, true, 36);
+    }
+}

+ 18 - 9
src/Michel/Package/MichelPaperORMPackage.php

@@ -2,8 +2,8 @@
 
 namespace PhpDevCommunity\PaperORM\Michel\Package;
 
-use LogicException;
 use PhpDevCommunity\Michel\Package\PackageInterface;
+use PhpDevCommunity\PaperORM\Collector\EntityDirCollector;
 use PhpDevCommunity\PaperORM\Command\DatabaseCreateCommand;
 use PhpDevCommunity\PaperORM\Command\DatabaseDropCommand;
 use PhpDevCommunity\PaperORM\Command\DatabaseSyncCommand;
@@ -14,6 +14,7 @@ use PhpDevCommunity\PaperORM\Command\ShowTablesCommand;
 use PhpDevCommunity\PaperORM\EntityManager;
 use PhpDevCommunity\PaperORM\EntityManagerInterface;
 use PhpDevCommunity\PaperORM\Migration\PaperMigration;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use Psr\Container\ContainerInterface;
 
 class MichelPaperORMPackage implements PackageInterface
@@ -21,16 +22,21 @@ class MichelPaperORMPackage implements PackageInterface
     public function getDefinitions(): array
     {
         return [
+            PaperConfiguration::class => static function (ContainerInterface $container) {
+                return PaperConfiguration::fromDsn(
+                    $container->get('paper.orm.dsn'),
+                    $container->get('paper.orm.debug')
+                )
+                    ->withLogger($container->get('paper.orm.logger'));
+            },
+            EntityDirCollector::class => static function (ContainerInterface $container) {
+                return EntityDirCollector::bootstrap([$container->get('paper.entity_dir')]);
+            },
             EntityManagerInterface::class => static function (ContainerInterface $container) {
                 return $container->get(EntityManager::class);
             },
             EntityManager::class => static function (ContainerInterface $container) {
-                return EntityManager::createFromDsn(
-                    $container->get('paper.orm.dsn'),
-                    $container->get('paper.orm.debug'),
-                    $container->get('paper.orm.logger'),
-                    []
-                );
+                return EntityManager::createFromConfig($container->get(PaperConfiguration::class));
             },
             PaperMigration::class => static function (ContainerInterface $container) {
                 return PaperMigration::create(
@@ -40,10 +46,13 @@ class MichelPaperORMPackage implements PackageInterface
                 );
             },
             MigrationDiffCommand::class => static function (ContainerInterface $container) {
-                return new MigrationDiffCommand($container->get(PaperMigration::class), $container->get('paper.entity_dir'));
+                return new MigrationDiffCommand($container->get(PaperMigration::class), $container->get(EntityDirCollector::class));
             },
             DatabaseDropCommand::class => static function (ContainerInterface $container) {
                 return new DatabaseDropCommand($container->get(EntityManagerInterface::class), $container->get('michel.environment'));
+            },
+            DatabaseSyncCommand::class => static function (ContainerInterface $container) {
+                return new DatabaseSyncCommand($container->get(PaperMigration::class), $container->get(EntityDirCollector::class), $container->get('michel.environment'));
             }
         ];
     }
@@ -70,7 +79,7 @@ class MichelPaperORMPackage implements PackageInterface
                 }
                 return $folder;
             },
-            'paper.orm.migrations_table' => getenv('PAPER_ORM_MIGRATIONS_TABLE') ?: 'mig_versions',
+            'paper.orm.migrations_table' => getenv('PAPER_ORM_MIGRATIONS_TABLE') ?: 'paper_mig_version',
         ];
     }
 

+ 0 - 1
src/Migration/PaperMigration.php

@@ -247,7 +247,6 @@ SQL;
     private function getConnection(): PaperConnection
     {
         return $this->em->getConnection();
-
     }
 
     private function executeQuery(string $query): bool

+ 98 - 0
src/PaperConfiguration.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM;
+
+use LogicException;
+use PhpDevCommunity\Listener\ListenerProvider;
+use PhpDevCommunity\PaperORM\Cache\EntityMemcachedCache;
+use PhpDevCommunity\PaperORM\Driver\DriverManager;
+use PhpDevCommunity\PaperORM\Event\PostCreateEvent;
+use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
+use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
+use PhpDevCommunity\PaperORM\EventListener\PostCreateEventListener;
+use PhpDevCommunity\PaperORM\EventListener\PreCreateEventListener;
+use PhpDevCommunity\PaperORM\EventListener\PreUpdateEventListener;
+use PhpDevCommunity\PaperORM\Parser\DSNParser;
+use Psr\EventDispatcher\ListenerProviderInterface;
+use Psr\Log\LoggerInterface;
+
+final class PaperConfiguration
+{
+    private PaperConnection $connection;
+    private UnitOfWork $unitOfWork;
+    private EntityMemcachedCache $cache;
+    private ListenerProviderInterface $listeners;
+    private function __construct(
+        PaperConnection           $connection,
+        UnitOfWork                $unitOfWork,
+        EntityMemcachedCache      $cache,
+        ListenerProviderInterface $listeners
+    )
+    {
+        $this->connection = $connection;
+        $this->unitOfWork = $unitOfWork;
+        $this->cache = $cache;
+        $this->listeners = $listeners;
+        $this->registerDefaultListeners();
+    }
+
+    public static function fromDsn(string $dsn, bool $debug = false): self
+    {
+        if ($dsn === '') {
+            throw new LogicException('PaperConfiguration::fromDsn(): DSN cannot be empty.');
+        }
+
+        $params = DSNParser::parse($dsn);
+        return self::fromArray($params, $debug);
+    }
+
+    public static function fromArray(array $params, bool $debug = false): self
+    {
+        $params['extra']['debug'] = $debug;
+        $connection = DriverManager::createConnection($params['driver'], $params);
+        return new self($connection, new UnitOfWork(), new EntityMemcachedCache(), new ListenerProvider());
+    }
+
+    public function withLogger(LoggerInterface $logger): self
+    {
+        $this->connection->withLogger($logger);
+        return $this;
+    }
+
+    public function withListener(string $event, callable $listener): self
+    {
+        $provider = $this->listeners;
+        $provider->addListener($event, $listener);
+
+        return $this;
+    }
+
+    public function getConnection(): PaperConnection
+    {
+        return $this->connection;
+    }
+
+    public function getUnitOfWork(): UnitOfWork
+    {
+        return $this->unitOfWork;
+    }
+
+    public function getCache(): EntityMemcachedCache
+    {
+        return $this->cache;
+    }
+
+    public function getListeners(): ListenerProviderInterface
+    {
+        return $this->listeners;
+    }
+
+    private function registerDefaultListeners(): void
+    {
+        $this->listeners
+            ->addListener(PreCreateEvent::class, new PreCreateEventListener())
+            ->addListener(PostCreateEvent::class, new PostCreateEventListener())
+            ->addListener(PreUpdateEvent::class, new PreUpdateEventListener())
+        ;
+    }
+}

+ 5 - 13
src/PaperConnection.php

@@ -27,18 +27,6 @@ final class PaperConnection
         $this->driver = $driver;
         $extra = $params['extra'] ?? [];
         $this->debug = (bool)($extra['debug'] ?? false);
-
-        if (array_key_exists('logger', $extra)) {
-            $logger = $extra['logger'];
-            if (!$logger instanceof LoggerInterface) {
-                $given = is_object($logger) ? get_class($logger) : gettype($logger);
-                throw new LogicException(sprintf(
-                    'The logger must be an instance of Psr\Log\LoggerInterface, %s given.',
-                    $given
-                ));
-            }
-            $this->logger = $logger;
-        }
     }
 
 
@@ -119,7 +107,6 @@ final class PaperConnection
         $this->pdo = null;
     }
 
-
     public function cloneConnectionWithoutDbname(): self
     {
         $params = $this->params;
@@ -127,4 +114,9 @@ final class PaperConnection
         return new self($this->driver, $params);
     }
 
+    public function withLogger(LoggerInterface $logger): self
+    {
+        $this->logger = $logger;
+        return $this;
+    }
 }

+ 11 - 3
src/Persistence/EntityPersistence.php

@@ -3,6 +3,8 @@
 namespace PhpDevCommunity\PaperORM\Persistence;
 
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\EntityManagerInterface;
+use PhpDevCommunity\PaperORM\Event\PostCreateEvent;
 use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
 use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
 use PhpDevCommunity\PaperORM\Hydrator\ReadOnlyEntityHydrator;
@@ -16,13 +18,16 @@ use Psr\EventDispatcher\EventDispatcherInterface;
 
 class EntityPersistence
 {
+    private EntityManagerInterface $em;
+
     private PlatformInterface $platform;
     private \SplObjectStorage $managed;
 
     private ?EventDispatcherInterface $dispatcher;
-    public function __construct(PlatformInterface $platform, EventDispatcherInterface $dispatcher = null)
+    public function __construct(EntityManagerInterface $em, EventDispatcherInterface $dispatcher = null)
     {
-        $this->platform = $platform;
+        $this->em = $em;
+        $this->platform = $em->getPlatform();
         $this->dispatcher = $dispatcher;
         $this->managed = new \SplObjectStorage();
     }
@@ -42,7 +47,7 @@ class EntityPersistence
         }
 
         if ($this->dispatcher) {
-            $this->dispatcher->dispatch(new PreCreateEvent($entity));
+            $this->dispatcher->dispatch(new PreCreateEvent($this->em, $entity));
         }
         $schema = $this->platform->getSchema();
         $tableName = EntityMapper::getTable($entity);
@@ -60,6 +65,9 @@ class EntityPersistence
             $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($entity);
             (new ReadOnlyEntityHydrator())->hydrate($entity, [$primaryKeyColumn => $lastInsertId]);
             $this->managed->attach($entity);
+            if ($this->dispatcher) {
+                $this->dispatcher->dispatch(new PostCreateEvent($this->em, $entity));
+            }
         }
         return $rows;
     }

+ 1 - 1
src/Platform/AbstractPlatform.php

@@ -31,7 +31,7 @@ abstract class AbstractPlatform implements PlatformInterface
         $mappings = $this->getColumnTypeMappings();
         $className = get_class($column);
         if (!array_key_exists($className, $mappings)) {
-            throw new LogicException(sprintf("The column type '%s' is not supported.", $column->getType()));
+            throw new LogicException(sprintf("The column type '%s' is not supported.", $className));
         }
 
         $mapping = $mappings[$className];

+ 25 - 0
src/Platform/MariaDBPlatform.php

@@ -4,6 +4,8 @@ namespace PhpDevCommunity\PaperORM\Platform;
 
 use InvalidArgumentException;
 use LogicException;
+use PhpDevCommunity\PaperORM\Mapping\Column\AnyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\AutoIncrementColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\BinaryColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\BoolColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\Column;
@@ -15,9 +17,12 @@ use PhpDevCommunity\PaperORM\Mapping\Column\IntColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JsonColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TextColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TokenColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\UuidColumn;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
 use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\IndexMetadata;
@@ -273,10 +278,30 @@ class MariaDBPlatform extends AbstractPlatform
                 'type' => 'VARCHAR',
                 'args' => [255]
             ],
+            SlugColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [128]
+            ],
             BinaryColumn::class => [
                 'type' => 'BLOB',
                 'args' => []
             ],
+            AnyColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [150],
+            ],
+            UuidColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [36],
+            ],
+            AutoIncrementColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [150],
+            ],
+            TokenColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [128],
+            ]
         ];
     }
 

+ 25 - 0
src/Platform/SqlitePlatform.php

@@ -4,6 +4,8 @@ namespace PhpDevCommunity\PaperORM\Platform;
 
 use InvalidArgumentException;
 use LogicException;
+use PhpDevCommunity\PaperORM\Mapping\Column\AnyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\AutoIncrementColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\BinaryColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\BoolColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\Column;
@@ -15,9 +17,12 @@ use PhpDevCommunity\PaperORM\Mapping\Column\IntColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JsonColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TextColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TokenColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\UuidColumn;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
 use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\IndexMetadata;
@@ -276,6 +281,10 @@ class SqlitePlatform extends AbstractPlatform
                 'type' => 'VARCHAR',
                 'args' => [255],
             ],
+            SlugColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [128]
+            ],
             TimestampColumn::class => [
                 'type' => 'DATETIME',
                 'args' => [],
@@ -283,6 +292,22 @@ class SqlitePlatform extends AbstractPlatform
             BinaryColumn::class => [
                 'type' => 'BLOB',
                 'args' => [],
+            ],
+            AnyColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [150],
+            ],
+            UuidColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [36],
+            ],
+            AutoIncrementColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [150],
+            ],
+            TokenColumn::class => [
+                'type' => 'VARCHAR',
+                'args' => [128],
             ]
         ];
     }

+ 1 - 3
src/Repository/Repository.php

@@ -18,14 +18,12 @@ use Psr\EventDispatcher\EventDispatcherInterface;
 abstract class Repository
 {
     private EntityManagerInterface $em;
-    private PlatformInterface $platform;
     private EntityPersistence $ep;
 
     public function __construct(EntityManagerInterface $em, EventDispatcherInterface $dispatcher = null)
     {
         $this->em = $em;
-        $this->platform = $em->getPlatform();
-        $this->ep = new EntityPersistence($this->platform, $dispatcher);
+        $this->ep = new EntityPersistence($em, $dispatcher);
     }
 
     /**

+ 46 - 0
src/Tools/EntityAccessor.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Tools;
+
+class EntityAccessor
+{
+    public static function getValue(object $entity, string $property)
+    {
+        $methods   = ["get" . ucfirst($property), "is" . ucfirst($property)];
+        foreach ($methods as $method) {
+            if (method_exists($entity, $method)) {
+                return $entity->$method();
+            }
+        }
+
+        if (array_key_exists($property, get_object_vars($entity))) {
+            return $entity->$property;
+        }
+
+        throw new \LogicException(sprintf(
+            'Cannot get value: expected getter "%s()" or a public property "%s" in entity "%s".',
+            $method,
+            $property,
+            get_class($entity)
+        ));
+
+    }
+
+    public static function setValue(object $entity, string $property, $value)
+    {
+        $method   = "set" . ucfirst($property);
+        if (method_exists($entity, $method)) {
+            $entity->$method($value);
+        } elseif (array_key_exists($property, get_object_vars($entity))) {
+            $entity->$property = $value;
+        } else {
+            throw new \LogicException(sprintf(
+                'Cannot set value: expected setter "%s()" or a public property "%s" in entity "%s".',
+                $method,
+                $property,
+                get_class($entity)
+            ));
+        }
+    }
+
+}

+ 25 - 4
src/Tools/EntityExplorer.php

@@ -4,24 +4,45 @@ namespace PhpDevCommunity\PaperORM\Tools;
 
 use PhpDevCommunity\FileSystem\Tools\FileExplorer;
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\Entity\SystemEntityInterface;
 
 final class EntityExplorer
 {
 
+    /**
+     * @param string[] $dirs
+     * @return array{normal: string[], system: string[]}
+     */
     public static function getEntities(array $dirs): array
     {
-        $entities = [];
+        $normal = [];
+        $system = [];
+
         foreach ($dirs as $dir) {
             $explorer = new FileExplorer($dir);
             $files = $explorer->searchByExtension('php', true);
+
             foreach ($files as $file) {
                 $entityClass = self::extractNamespaceAndClass($file['path']);
-                if ($entityClass !== null && class_exists($entityClass) && is_subclass_of($entityClass, EntityInterface::class)) {
-                    $entities[$file['path']] = $entityClass;
+                if ($entityClass === null || !class_exists($entityClass)) {
+                    continue;
+                }
+                if (!is_subclass_of($entityClass, EntityInterface::class)) {
+                    continue;
+                }
+
+                if (is_subclass_of($entityClass, SystemEntityInterface::class)) {
+                    $system[$file['path']] = $entityClass;
+                } else {
+                    $normal[$file['path']] = $entityClass;
                 }
             }
         }
-        return $entities;
+
+        return [
+            'normal' => $normal,
+            'system' => $system,
+        ];
     }
 
     private static function extractNamespaceAndClass(string $filePath): ?string

+ 58 - 0
src/Tools/IDBuilder.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Tools;
+
+
+final class IDBuilder
+{
+    /**
+     * Generates a formatted unique or sequential identifier string.
+     *
+     * It substitutes dynamic placeholders (tokens) like date/time components,
+     * timestamps, and random values (UUID, tokens) with their actual values
+     * based on the provided format string.
+     *
+     * @param string $format The pattern string containing placeholders (e.g., 'INV-{YYYY}-{TOKEN16}').
+     * @return string The final generated and formatted identifier.
+     */
+    public static function generate(string $format): string
+    {
+        $format = trim($format);
+        $format = strtoupper($format);
+        $now = new \DateTimeImmutable();
+
+        // Note: I've included the PHP native logic for the tokens you provided
+        // and fixed the minute/second token naming for clarity (using {ii} and {ss}).
+
+        $tokens = [
+            // Date/Time Tokens
+            '{YYYY}'     => $now->format('Y'),
+            '{YY}'       => $now->format('y'),
+            '{MM}'       => $now->format('m'),
+            '{DD}'       => $now->format('d'),
+            '{HH}'       => $now->format('H'),
+            '{ii}'       => $now->format('i'), // Renamed from {M} to {ii} for minutes
+            '{ss}'       => $now->format('s'), // Renamed from {S} to {ss} for seconds
+            '{YMD}'      => $now->format('Ymd'),
+            '{YMDH}'     => $now->format('YmdH'),
+            '{DATE}'     => $now->format('Y-m-d'),
+            '{TIME}'     => $now->format('H:i:s'),
+
+            // Sequential/Unique Tokens
+            '{TS}'       => (string) $now->getTimestamp(),
+            '{UNIQ}'     => uniqid(),
+
+            // UUID V4 (Generated natively using random_bytes)
+            // 36 characters (32 hex digits + 4 hyphens)
+            '{UUID}'     => vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)),
+
+            // Random Hex Tokens (using cryptographically secure random_bytes)
+            '{TOKEN16}'  => bin2hex(random_bytes(8)),   // 8 bytes * 2 = 16 hex characters
+            '{TOKEN32}'  => bin2hex(random_bytes(16)),  // 16 bytes * 2 = 32 hex characters
+            '{TOKEN64}'  => bin2hex(random_bytes(32)),  // 32 bytes * 2 = 64 hex characters
+            '{TOKEN128}' => bin2hex(random_bytes(64)),  // 64 bytes * 2 = 128 hex characters
+        ];
+
+        return trim(strtr($format, $tokens));
+    }
+}

+ 17 - 0
src/Tools/NamingStrategy.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Tools;
+
+class NamingStrategy
+{
+
+    public static function toSnakeCase(string $input): string
+    {
+        $input = preg_replace('/(\p{Lu})(\p{Lu}\p{Ll})/u', '$1_$2', $input);
+        $input = preg_replace('/(?<=\p{Ll}|\d)(\p{Lu})/u', '_$1', $input);
+        $input = preg_replace('/(\p{Lu})(?=\d)/u', '$1_', $input);
+
+        return strtolower($input);
+    }
+
+}

+ 28 - 0
src/Tools/Slugger.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Tools;
+
+final class Slugger
+{
+    public static function slugify(array $parts, string $separator = '-'): string
+    {
+        $parts = array_filter($parts, function ($part) {
+            if ($part === null) {
+                return false;
+            }
+            return trim($part) !== '';
+        });
+
+        if (empty($parts)) {
+            throw new \InvalidArgumentException('Slug cannot be empty.');
+        }
+
+        $slug = implode(' ', $parts);
+        $slug = trim($slug);
+        $slug = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $slug);
+        $slug = strtolower($slug);
+        $slug = preg_replace('/[^a-z0-9]+/', $separator, $slug);
+
+        return trim($slug, $separator);
+    }
+}

+ 159 - 0
src/Types/AnyType.php

@@ -0,0 +1,159 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Types;
+
+use InvalidArgumentException;
+
+final class AnyType extends Type
+{
+
+    /** @var int Max allowed serialized length */
+    private int $maxLength = 150;
+
+    /**
+     * Converts a PHP value into a database-storable string.
+     *
+     * @param mixed $value
+     * @return string
+     * @throws \InvalidArgumentException
+     */
+    public function convertToDatabase($value) : string
+    {
+        $type = strtolower(gettype($value));
+
+        switch ($type) {
+            case 'null':
+                $encoded = 'null:';
+                break;
+
+            case 'boolean':
+                $encoded = 'boolean:' . ($value ? '1' : '0');
+                break;
+
+            case 'integer':
+            case 'double':
+            case 'float':
+                $encoded = $type . ':' . (string) $value;
+                break;
+
+            case 'string':
+                $encoded = 'string:' . trim($value);
+                break;
+
+            case 'array':
+                $json = json_encode($value);
+                if (json_last_error() !== JSON_ERROR_NONE) {
+                    throw new \InvalidArgumentException(
+                        'JSON encoding error while serializing array: ' . json_last_error_msg()
+                    );
+                }
+                $encoded = 'array:' . $json;
+                break;
+
+            case 'object':
+                $serialized = @serialize($value);
+                if (empty($serialized)) {
+                    throw new \InvalidArgumentException('Failed to serialize object.');
+                }
+                $encoded = 'object:' . $serialized;
+                break;
+
+            default:
+                throw new \InvalidArgumentException(sprintf(
+                    'Unsupported type "%s" for database conversion.',
+                    $type
+                ));
+        }
+
+        if (strlen($encoded) > $this->maxLength) {
+            throw new \InvalidArgumentException(sprintf(
+                'AnyColumn value too long (%d bytes, max %d). Use a dedicated column type for large data.',
+                strlen($encoded),
+                $this->maxLength
+            ));
+        }
+
+        return $encoded;
+    }
+
+    /**
+     * Converts a database value back to its original PHP representation.
+     *
+     * @param string|null $value
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function convertToPHP($value)
+    {
+        if ($value === null || $value === '') {
+            return null;
+        }
+
+        // Expected format: "<type>:<value>"
+        $parts = explode(':', $value, 2);
+        if (count($parts) !== 2) {
+            throw new \InvalidArgumentException(
+                sprintf('Invalid data format for AnyColumn: "%s"', $value)
+            );
+        }
+
+        list($type, $raw) = $parts;
+        $type = strtolower(trim($type));
+
+        switch ($type) {
+            case 'null':
+                return null;
+
+            case 'boolean':
+                $val = strtolower(trim($raw));
+                if ($val === '1') {
+                    return true;
+                }
+                if ($val === '0') {
+                    return false;
+                }
+                throw new \InvalidArgumentException("Invalid boolean value: '{$raw}'");
+
+            case 'integer':
+            case 'int':
+                if (!is_numeric($raw)) {
+                    throw new \InvalidArgumentException("Invalid integer value: '{$raw}'");
+                }
+                return (int) $raw;
+
+            case 'double':
+            case 'float':
+                if (!is_numeric($raw)) {
+                    throw new \InvalidArgumentException("Invalid numeric value: '{$raw}'");
+                }
+                return (float) $raw;
+
+            case 'string':
+                return (string) $raw;
+
+            case 'array':
+                $decoded = json_decode($raw, true);
+                if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
+                    throw new \InvalidArgumentException(
+                        'JSON decoding error while converting to array: ' . json_last_error_msg()
+                    );
+                }
+                return $decoded;
+
+            case 'object':
+                $obj = @unserialize($raw);
+                if ($obj === false && $raw !== 'b:0;') {
+                    throw new \InvalidArgumentException(
+                        'Object deserialization failed: invalid or corrupted data.'
+                    );
+                }
+                return $obj;
+
+            default:
+                throw new \InvalidArgumentException(sprintf(
+                    'Unsupported type "%s" during PHP conversion.',
+                    $type
+                ));
+        }
+    }
+}

+ 76 - 0
tests/Common/NamingStrategyTest.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Test\PhpDevCommunity\PaperORM\Common;
+
+use PhpDevCommunity\PaperORM\Parser\DSNParser;
+use PhpDevCommunity\PaperORM\Tools\NamingStrategy;
+use PhpDevCommunity\PaperORM\Tools\Slugger;
+use PhpDevCommunity\UniTester\TestCase;
+
+class NamingStrategyTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $tests = [
+            'id'                 => 'id',
+            'name'               => 'name',
+            'emailAddress'       => 'email_address',
+            'userId'             => 'user_id',
+            'createdAt'          => 'created_at',
+            'updatedAt'          => 'updated_at',
+            'invoiceNumber'      => 'invoice_number',
+            'shippingCost'       => 'shipping_cost',
+
+            'UUID'               => 'uuid',
+            'UUIDValue'          => 'uuid_value',
+            'userUUID'           => 'user_uuid',
+            'HTMLParser'         => 'html_parser',
+            'XMLHttpRequest'     => 'xml_http_request',
+            'APIResponseCode'    => 'api_response_code',
+            'HTTPRequestTime'    => 'http_request_time',
+            'PaperORMVersion'    => 'paper_orm_version',
+
+            'user2Id'            => 'user_2_id',
+            'version1Name'       => 'version_1_name',
+            'HTTP2Server'        => 'http_2_server',
+            'Order2Item'         => 'order_2_item',
+            'ApiV2Endpoint'      => 'api_v2_endpoint',
+            'Invoice2025Count'   => 'invoice_2025_count',
+
+            'User'               => 'user',
+            'UserProfile'        => 'user_profile',
+            'InvoiceDetail'      => 'invoice_detail',
+            'OrderLine'          => 'order_line',
+            'ClientAddressBook'  => 'client_address_book',
+
+            'userIDNumber'       => 'user_id_number',
+            'userIPAddress'      => 'user_ip_address',
+            'userHTTPResponse'   => 'user_http_response',
+            'productSKUCode'     => 'product_sku_code',
+            'fileMD5Hash'        => 'file_md5_hash',
+            'dataJSONEncoded'    => 'data_json_encoded',
+
+            'PaperXMLParser'     => 'paper_xml_parser',
+            'PaperURLGenerator'  => 'paper_url_generator',
+            'DBConnectionName'   => 'db_connection_name',
+            'SQLQueryTime'       => 'sql_query_time',
+        ];
+
+        foreach ($tests as $test) {
+            $out = NamingStrategy::toSnakeCase($test);
+            $this->assertStrictEquals($out, $test);
+        }
+    }
+
+}

+ 2 - 1
tests/Common/OrmTestMemory.php

@@ -3,6 +3,7 @@
 namespace Test\PhpDevCommunity\PaperORM\Common;
 
 use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\UniTester\TestCase;
 use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
 use Test\PhpDevCommunity\PaperORM\Helper\DataBaseHelperTest;
@@ -23,7 +24,7 @@ class OrmTestMemory extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as  $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             DataBaseHelperTest::init($em, 1000, false);
             $memory = memory_get_usage();
             $users = $em->getRepository(UserTest::class)

+ 78 - 0
tests/Common/SluggerTest.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace Test\PhpDevCommunity\PaperORM\Common;
+
+use PhpDevCommunity\PaperORM\Parser\DSNParser;
+use PhpDevCommunity\PaperORM\Tools\Slugger;
+use PhpDevCommunity\UniTester\TestCase;
+
+class SluggerTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $slugTests = [
+            ['input' => ['Hello', 'World'], 'expected' => 'hello-world'],
+            ['input' => ['Jean', 'Dupont'], 'expected' => 'jean-dupont'],
+            ['input' => ['PHP', 'ORM', 'Slug'], 'expected' => 'php-orm-slug'],
+
+            ['input' => ['   Hello   ', '   World   '], 'expected' => 'hello-world'],
+            ['input' => ['   Multi   Space  ', '  Test  '], 'expected' => 'multi-space-test'],
+            ['input' => ['  Hello  ', '', 'World  '], 'expected' => 'hello-world'],
+
+            ['input' => ['À l\'ombre', 'du cœur'], 'expected' => 'a-l-ombre-du-coeur'],
+            ['input' => ['Café', 'Crème'], 'expected' => 'cafe-creme'],
+            ['input' => ['École', 'publique'], 'expected' => 'ecole-publique'],
+            ['input' => ['Über', 'mensch'], 'expected' => 'uber-mensch'],
+
+            ['input' => ['Hello!', 'World?'], 'expected' => 'hello-world'],
+            ['input' => ['Slug@Email.com'], 'expected' => 'slug-email-com'],
+            ['input' => ['Dollar$', 'Sign$'], 'expected' => 'dollar-sign'],
+            ['input' => ['Hash#Tag'], 'expected' => 'hash-tag'],
+            ['input' => ['C++', 'Language'], 'expected' => 'c-language'],
+            ['input' => ['Node.js', 'Framework'], 'expected' => 'node-js-framework'],
+            ['input' => ['100%', 'Working'], 'expected' => '100-working'],
+
+            ['input' => ['Hello', null, 'World'], 'expected' => 'hello-world'],
+            ['input' => ['0', 'Value'], 'expected' => '0-value'],
+            ['input' => ['False', 'Start'], 'expected' => 'false-start'],
+            ['input' => ['Hello', 123, 'World'], 'expected' => 'hello-123-world'],
+
+            ['input' => ['snake_case_example'], 'expected' => 'snake-case-example'],
+            ['input' => ['kebab-case-example'], 'expected' => 'kebab-case-example'],
+            ['input' => ['mix_case_Example'], 'expected' => 'mix-case-example'],
+
+            ['input' => ['Hello___World'], 'expected' => 'hello-world'],
+            ['input' => ['Hello   ---   World'], 'expected' => 'hello-world'],
+            ['input' => ['___Hello', 'World___'], 'expected' => 'hello-world'],
+
+            ['input' => ['123', '456'], 'expected' => '123-456'],
+            ['input' => ['Version', '2.0'], 'expected' => 'version-2-0'],
+            ['input' => ['A+B=C'], 'expected' => 'a-b-c'],
+
+            ['input' => ['NULL'], 'expected' => 'null'],
+            ['input' => ['Éléphant', '', ''], 'expected' => 'elephant'],
+            ['input' => [null, null, 'Test'], 'expected' => 'test'],
+            ['input' => ['Long   String   With   Many   Spaces'], 'expected' => 'long-string-with-many-spaces'],
+            ['input' => ['---Leading', 'And', 'Trailing---'], 'expected' => 'leading-and-trailing'],
+            ['input' => ['This_is_a_very_long_text_with_many_parts'], 'expected' => 'this-is-a-very-long-text-with-many-parts'],
+        ];
+
+
+        foreach ($slugTests as $test) {
+            $slug = Slugger::slugify($test['input']);
+            $this->assertStrictEquals($test['expected'], $slug);
+        }
+    }
+
+}

+ 2 - 1
tests/DatabaseShowTablesCommandTest.php

@@ -13,6 +13,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\IntColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\UniTester\TestCase;
 use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
 use Test\PhpDevCommunity\PaperORM\Helper\DataBaseHelperTest;
@@ -32,7 +33,7 @@ class DatabaseShowTablesCommandTest extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             $this->executeTest($em);
             $em->getConnection()->close();
         }

+ 11 - 9
tests/DatabaseSyncCommandTest.php

@@ -5,6 +5,7 @@ namespace Test\PhpDevCommunity\PaperORM;
 use PhpDevCommunity\Console\CommandParser;
 use PhpDevCommunity\Console\CommandRunner;
 use PhpDevCommunity\Console\Output;
+use PhpDevCommunity\PaperORM\Collector\EntityDirCollector;
 use PhpDevCommunity\PaperORM\Command\DatabaseSyncCommand;
 use PhpDevCommunity\PaperORM\Command\ShowTablesCommand;
 use PhpDevCommunity\PaperORM\EntityManager;
@@ -14,6 +15,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Migration\PaperMigration;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\UniTester\TestCase;
 use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
 use Test\PhpDevCommunity\PaperORM\Helper\DataBaseHelperTest;
@@ -33,7 +35,7 @@ class DatabaseSyncCommandTest extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             $this->executeTest($em);
             $em->getConnection()->close();
         }
@@ -48,7 +50,7 @@ class DatabaseSyncCommandTest extends TestCase
 
         $paperMigration = PaperMigration::create($em, 'mig_versions', __DIR__ . '/migrations');
         $runner = new CommandRunner([
-            new DatabaseSyncCommand($paperMigration, __DIR__ . '/Entity', 'test'),
+            new DatabaseSyncCommand($paperMigration, EntityDirCollector::bootstrap([__DIR__ . '/Entity']), 'test'),
         ]);
 
         $out = [];
@@ -66,14 +68,14 @@ class DatabaseSyncCommandTest extends TestCase
         $this->assertEquals(0, $code);
         $this->assertStringContains( implode(' ', $out), "✔ Executed:");
 //
-//        $out = [];
-//        $code = $runner->run(new CommandParser(['', 'paper:show:tables', 'post', '--columns']), new Output(function ($message) use(&$out) {
-//            $out[] = $message;
-//        }));
-//
-//        $this->assertEquals(0, $code);
-//        $this->assertEquals(62, count($out));
+        $out = [];
+        $code = $runner->run(new CommandParser(['', 'paper:database:sync']), new Output(function ($message) use(&$out) {
+            $out[] = $message;
+        }));
+
 
+        $this->assertEquals(0, $code);
+        $this->assertStringContains( implode(' ', $out), "No differences detected — all entities are already in sync with the database schema.");
 
         $platform->dropDatabase();
     }

+ 15 - 0
tests/Entity/CommentTest.php

@@ -7,6 +7,7 @@ use PhpDevCommunity\PaperORM\Entity\TableMetadataInterface;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\UuidColumn;
 use PhpDevCommunity\PaperORM\Mapping\Entity;
 use Test\PhpDevCommunity\PaperORM\Repository\TagTestRepository;
 
@@ -16,6 +17,7 @@ class CommentTest implements EntityInterface, TableMetadataInterface
 
     private ?int $id = null;
     private ?string $body = null;
+    private ?string $uuid = null;
     private ?PostTest $post = null;
 
     static public function getTableName(): string
@@ -33,6 +35,7 @@ class CommentTest implements EntityInterface, TableMetadataInterface
         return [
             (new PrimaryKeyColumn())->bindProperty('id'),
             (new StringColumn())->bindProperty('body'),
+            (new UuidColumn())->bindProperty('uuid'),
             (new JoinColumn('post_id', PostTest::class))->bindProperty('post'),
         ];
     }
@@ -58,6 +61,18 @@ class CommentTest implements EntityInterface, TableMetadataInterface
         return $this;
     }
 
+    public function getUuid(): ?string
+    {
+        return $this->uuid;
+    }
+
+    public function setUuid(?string $uuid): CommentTest
+    {
+        $this->uuid = $uuid;
+        return $this;
+    }
+
+
     public function getPost(): ?PostTest
     {
         return $this->post;

+ 76 - 0
tests/Entity/InvoiceTest.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Test\PhpDevCommunity\PaperORM\Entity;
+
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\Mapping\Column\AutoIncrementColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Entity;
+
+#[Entity(table: 'invoice', repository: null)]
+class InvoiceTest implements EntityInterface
+{
+    #[PrimaryKeyColumn]
+    private ?int $id = null;
+    #[AutoIncrementColumn(pad: 8, prefix: 'INV-{YYYY}-', key: 'invoice.number')]
+    private ?string $number = null;
+
+    #[AutoIncrementColumn(pad: 8, key: 'invoice.code')]
+    private ?string $code = null;
+
+
+    static public function getTableName(): string
+    {
+        return 'invoice';
+    }
+
+    static public function getRepositoryName(): ?string
+    {
+        return null;
+    }
+
+    static public function columnsMapping(): array
+    {
+        return [
+            (new PrimaryKeyColumn())->bindProperty('id'),
+            (new AutoIncrementColumn(null, 'invoice.number', 6, 'INV-{YYYY}-'))->bindProperty('number'),
+            (new AutoIncrementColumn(null, 'invoice.code', 8, null))->bindProperty('code'),
+            (new JoinColumn('post_id', PostTest::class))->bindProperty('post'),
+        ];
+    }
+
+    public function getPrimaryKeyValue(): ?int
+    {
+        return $this->getId();
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getNumber(): ?string
+    {
+        return $this->number;
+    }
+
+    public function setNumber(?string $number): InvoiceTest
+    {
+        $this->number = $number;
+        return $this;
+    }
+
+    public function getCode(): ?string
+    {
+        return $this->code;
+    }
+
+    public function setCode(?string $code): InvoiceTest
+    {
+        $this->code = $code;
+        return $this;
+    }
+
+
+}

+ 18 - 2
tests/Entity/PostTest.php

@@ -5,6 +5,7 @@ namespace Test\PhpDevCommunity\PaperORM\Entity;
 use DateTime;
 use PhpDevCommunity\PaperORM\Collection\ObjectStorage;
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\DateTimeColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
@@ -26,7 +27,10 @@ class PostTest implements EntityInterface
     #[StringColumn]
     private ?string $content = null;
 
-    #[DateTimeColumn(name: 'created_at')]
+    #[SlugColumn(from : ['title'])]
+    private ?string $slug = null;
+
+    #[DateTimeColumn()]
     private ?DateTime $createdAt = null;
 
     #[JoinColumn(name: 'user_id', targetEntity:  UserTest::class, nullable: true, unique: false, onDelete: JoinColumn::SET_NULL)]
@@ -62,7 +66,8 @@ class PostTest implements EntityInterface
             (new PrimaryKeyColumn())->bindProperty('id'),
             (new StringColumn())->bindProperty('title'),
             (new StringColumn())->bindProperty('content'),
-            (new DateTimeColumn( 'created_at'))->bindProperty('createdAt'),
+            (new SlugColumn(null, ['title']))->bindProperty('slug'),
+            (new DateTimeColumn( null))->bindProperty('createdAt'),
             (new JoinColumn('user_id', UserTest::class, 'id', true, false, JoinColumn::SET_NULL))->bindProperty('user'),
             (new OneToMany( TagTest::class, 'post'))->bindProperty('tags'),
             (new OneToMany( CommentTest::class, 'post'))->bindProperty('comments'),
@@ -107,6 +112,17 @@ class PostTest implements EntityInterface
         return $this;
     }
 
+    public function getSlug(): ?string
+    {
+        return $this->slug;
+    }
+
+    public function setSlug(?string $slug): PostTest
+    {
+        $this->slug = $slug;
+        return $this;
+    }
+
     public function getCreatedAt(): ?DateTime
     {
         return $this->createdAt;

+ 19 - 3
tests/Entity/UserTest.php

@@ -11,6 +11,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\DateTimeColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TokenColumn;
 use PhpDevCommunity\PaperORM\Mapping\Entity;
 use PhpDevCommunity\PaperORM\Mapping\OneToMany;
 use Test\PhpDevCommunity\PaperORM\Repository\PostTestRepository;
@@ -25,7 +26,7 @@ class UserTest implements EntityInterface
     private ?string $firstname = null;
 
     #[StringColumn]
-    private ?string $lastname    = null;
+    private ?string $lastname  = null;
 
     #[StringColumn]
     private ?string $email = null;
@@ -33,10 +34,13 @@ class UserTest implements EntityInterface
     #[StringColumn]
     private ?string $password = null;
 
+    #[TokenColumn(length: 32)]
+    private ?string $token = null;
+
     #[BoolColumn(name: 'is_active')]
     private bool $active = false;
 
-    #[TimestampColumn(name: 'created_at', onCreated: true)]
+    #[TimestampColumn( onCreated: true)]
     private ?\DateTimeInterface $createdAt = null;
 
     #[OneToMany(targetEntity: PostTest::class, mappedBy: 'user')]
@@ -70,8 +74,9 @@ class UserTest implements EntityInterface
             (new StringColumn())->bindProperty('lastname'),
             (new StringColumn())->bindProperty('email'),
             (new StringColumn())->bindProperty('password'),
+            (new TokenColumn(null, 32))->bindProperty('token'),
             (new BoolColumn( 'is_active'))->bindProperty('active'),
-            (new TimestampColumn( 'created_at', true))->bindProperty('createdAt'),
+            (new TimestampColumn( null, true))->bindProperty('createdAt'),
             (new OneToMany( PostTest::class,  'user'))->bindProperty('posts'),
             (new JoinColumn( 'last_post_id', PostTest::class, 'id', true, true, JoinColumn::SET_NULL))->bindProperty('lastPost'),
         ];
@@ -137,6 +142,17 @@ class UserTest implements EntityInterface
         return $this;
     }
 
+    public function getToken(): ?string
+    {
+        return $this->token;
+    }
+
+    public function setToken(?string $token): UserTest
+    {
+        $this->token = $token;
+        return $this;
+    }
+
     public function isActive(): bool
     {
         return $this->active;

+ 40 - 8
tests/Helper/DataBaseHelperTest.php

@@ -5,12 +5,19 @@ namespace Test\PhpDevCommunity\PaperORM\Helper;
 use DateTime;
 use PhpDevCommunity\PaperORM\EntityManagerInterface;
 use PhpDevCommunity\PaperORM\Generator\SchemaDiffGenerator;
+use PhpDevCommunity\PaperORM\Internal\Entity\PaperKeyValue;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
 use PhpDevCommunity\PaperORM\Mapping\Column\BoolColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\DateTimeColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\SlugColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TokenColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\UuidColumn;
+use PhpDevCommunity\PaperORM\Tools\IDBuilder;
+use Test\PhpDevCommunity\PaperORM\Entity\InvoiceTest;
 use Test\PhpDevCommunity\PaperORM\Entity\PostTest;
 use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
 
@@ -46,6 +53,7 @@ class DataBaseHelperTest
             new StringColumn('lastname'),
             new StringColumn('email'),
             new StringColumn('password'),
+            new TokenColumn('token', 32),
             new BoolColumn('is_active'),
             new TimestampColumn('created_at', true),
         ];
@@ -54,6 +62,7 @@ class DataBaseHelperTest
             (new JoinColumn('user_id', UserTest::class, 'id', true, false, JoinColumn::SET_NULL)),
             new StringColumn('title'),
             new StringColumn('content'),
+            new SlugColumn('slug', ['title']),
             new TimestampColumn('created_at', true),
         ];
 
@@ -66,7 +75,10 @@ class DataBaseHelperTest
             new PrimaryKeyColumn('id'),
             (new JoinColumn('post_id', PostTest::class, 'id', true, false, JoinColumn::SET_NULL)),
             new StringColumn('body'),
+            new UuidColumn('uuid'),
         ];
+
+
         $platform = $entityManager->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
@@ -88,6 +100,14 @@ class DataBaseHelperTest
                     'columns' => $commentColumns,
                     'indexes' => [],
                 ],
+                'invoice' => [
+                    'columns' => ColumnMapper::getColumns(InvoiceTest::class),
+                    'indexes' => [],
+                ],
+                'paper_key_value' => [
+                    'columns' => ColumnMapper::getColumns(PaperKeyValue::class),
+                    'indexes' => [],
+                ]
             ]
         );
 
@@ -105,16 +125,18 @@ class DataBaseHelperTest
                 'lastname' => 'Doe' . $i,
                 'email' => $i . 'bqQpB@example.com',
                 'password' => 'password123',
+                'token' => bin2hex(random_bytes(16)),
                 'is_active' => true,
                 'created_at' => (new DateTime())->format($platform->getSchema()->getDateTimeFormatString()),
             ];
 
-            $stmt = $connection->getPdo()->prepare("INSERT INTO user (firstname, lastname, email, password, is_active, created_at) VALUES (:firstname, :lastname, :email, :password, :is_active, :created_at)");
+            $stmt = $connection->getPdo()->prepare("INSERT INTO user (firstname, lastname, email, password, token, is_active, created_at) VALUES (:firstname, :lastname, :email, :password, :token,:is_active, :created_at)");
             $stmt->execute([
                 'firstname' => $user['firstname'],
                 'lastname' => $user['lastname'],
                 'email' => $user['email'],
                 'password' => $user['password'],
+                'token' => $user['token'],
                 'is_active' => $user['is_active'],
                 'created_at' => $user['created_at']
             ]);
@@ -128,14 +150,16 @@ class DataBaseHelperTest
                     'user_id' => $i + 1,
                     'title' => 'Post ' . $id,
                     'content' => 'Content ' . $id,
+                    'slug' => 'post-' . $id,
                     'created_at' => (new DateTime())->format($platform->getSchema()->getDateTimeFormatString()),
                 ];
 
-                $stmt = $connection->getPdo()->prepare("INSERT INTO post (user_id, title, content, created_at)  VALUES (:user_id, :title, :content, :created_at)");
+                $stmt = $connection->getPdo()->prepare("INSERT INTO post (user_id, title, content, slug, created_at)  VALUES (:user_id, :title, :content, :slug, :created_at)");
                 $stmt->execute([
                     'user_id' => $post['user_id'],
                     'title' => $post['title'],
                     'content' => $post['content'],
+                    'slug' => $post['slug'],
                     'created_at' => $post['created_at']
                 ]);
                 $id = uniqid('post_', true);
@@ -143,11 +167,13 @@ class DataBaseHelperTest
                     'user_id' => $i + 1,
                     'title' => 'Post ' . $id,
                     'content' => 'Content ' . $id,
+                    'slug' => 'post-' . $id,
                 ];
-                $connection->executeStatement("INSERT INTO post (user_id, title, content) VALUES (
+                $connection->executeStatement("INSERT INTO post (user_id, title, content, slug) VALUES (
                 '{$post['user_id']}',
                 '{$post['title']}',
-                '{$post['content']}'
+                '{$post['content']}',
+                '{$post['slug']}'
             )");
 
                 $lastId = $connection->getPdo()->lastInsertId();
@@ -183,16 +209,22 @@ class DataBaseHelperTest
                 $comment = [
                     'post_id' => $i + 1,
                     'body' => 'Comment ' . $id,
+                    'uuid' => IDBuilder::generate('{UUID}')
                 ];
-                $connection->executeStatement("INSERT INTO comment (post_id, body) VALUES (
+                $connection->executeStatement("INSERT INTO comment (post_id, body, uuid) VALUES (
                 '{$comment['post_id']}',
-                '{$comment['body']}      '
+                '{$comment['body']}',
+                '{$comment['uuid']}'
             )");
 
+
                 $comment['body'] = 'Comment ' . $id . ' 2';
-                $connection->executeStatement("INSERT INTO comment (post_id, body) VALUES (
+                $comment['uuid'] = IDBuilder::generate('{UUID}');
+
+                $connection->executeStatement("INSERT INTO comment (post_id, body, uuid) VALUES (
                 '{$comment['post_id']}',
-                '{$comment['body']}      '
+                '{$comment['body']}',
+                '{$comment['uuid']}'
             )");
             }
         }

+ 35 - 25
tests/MigrationTest.php

@@ -9,6 +9,7 @@ use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
 use PhpDevCommunity\PaperORM\Mapping\Column\IntColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Migration\PaperMigration;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\UniTester\TestCase;
 use RuntimeException;
 use Test\PhpDevCommunity\PaperORM\Entity\PostTest;
@@ -34,7 +35,7 @@ class MigrationTest extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             $paperMigration = PaperMigration::create($em, 'mig_versions', $this->migrationDir);
             $platform = $em->getPlatform();
             $platform->createDatabaseIfNotExists();
@@ -63,36 +64,45 @@ class MigrationTest extends TestCase
         switch (get_class($driver)) {
             case SqliteDriver::class:
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
-                $this->assertEquals($lines, array(
+
+                $this->assertEquals($lines, array (
                     0 => '-- UP MIGRATION --',
-                    1 => 'CREATE TABLE `user` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,`firstname` VARCHAR(255) NOT NULL,`lastname` VARCHAR(255) NOT NULL,`email` VARCHAR(255) NOT NULL,`password` VARCHAR(255) NOT NULL,`is_active` BOOLEAN NOT NULL,`created_at` DATETIME,`last_post_id` INTEGER,FOREIGN KEY (`last_post_id`) REFERENCES `post` (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
-                    2 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON `user` (`last_post_id`);',
-                    3 => 'CREATE TABLE `post` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,`title` VARCHAR(255) NOT NULL,`content` VARCHAR(255) NOT NULL,`created_at` DATETIME NOT NULL,`user_id` INTEGER,FOREIGN KEY (`user_id`) REFERENCES `user` (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
-                    4 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON `post` (`user_id`);',
-                    5 => '-- DOWN MIGRATION --',
-                    6 => 'DROP INDEX IX_8D93D6492D053F64;',
-                    7 => 'DROP TABLE `user`;',
-                    8 => 'DROP INDEX IX_5A8A6C8DA76ED395;',
-                    9 => 'DROP TABLE `post`;',
+                    1 => 'CREATE TABLE `user` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,`firstname` VARCHAR(255) NOT NULL,`lastname` VARCHAR(255) NOT NULL,`email` VARCHAR(255) NOT NULL,`password` VARCHAR(255) NOT NULL,`token` VARCHAR(32) NOT NULL,`is_active` BOOLEAN NOT NULL,`created_at` DATETIME,`last_post_id` INTEGER,FOREIGN KEY (`last_post_id`) REFERENCES `post` (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
+                    2 => 'CREATE UNIQUE INDEX IX_8D93D6495F37A13B ON `user` (`token`);',
+                    3 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON `user` (`last_post_id`);',
+                    4 => 'CREATE TABLE `post` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,`title` VARCHAR(255) NOT NULL,`content` VARCHAR(255) NOT NULL,`slug` VARCHAR(128) NOT NULL,`created_at` DATETIME NOT NULL,`user_id` INTEGER,FOREIGN KEY (`user_id`) REFERENCES `user` (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
+                    5 => 'CREATE UNIQUE INDEX IX_5A8A6C8D989D9B62 ON `post` (`slug`);',
+                    6 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON `post` (`user_id`);',
+                    7 => '-- DOWN MIGRATION --',
+                    8 => 'DROP INDEX IX_8D93D6495F37A13B;',
+                    9 => 'DROP INDEX IX_8D93D6492D053F64;',
+                    10 => 'DROP TABLE `user`;',
+                    11 => 'DROP INDEX IX_5A8A6C8D989D9B62;',
+                    12 => 'DROP INDEX IX_5A8A6C8DA76ED395;',
+                    13 => 'DROP TABLE `post`;',
                 ));
                 break;
             case MariaDBDriver::class:
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
-                $this->assertEquals($lines, array(
+                $this->assertEquals($lines, array (
                     0 => '-- UP MIGRATION --',
-                    1 => 'CREATE TABLE `user` (`id` INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,`firstname` VARCHAR(255) NOT NULL,`lastname` VARCHAR(255) NOT NULL,`email` VARCHAR(255) NOT NULL,`password` VARCHAR(255) NOT NULL,`is_active` TINYINT(1) NOT NULL,`created_at` DATETIME DEFAULT NULL,`last_post_id` INT(11) DEFAULT NULL);',
-                    2 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON `user` (`last_post_id`);',
-                    3 => 'CREATE TABLE `post` (`id` INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,`title` VARCHAR(255) NOT NULL,`content` VARCHAR(255) NOT NULL,`created_at` DATETIME NOT NULL,`user_id` INT(11) DEFAULT NULL);',
-                    4 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON `post` (`user_id`);',
-                    5 => 'ALTER TABLE `user` ADD CONSTRAINT FK_8D93D6492D053F64 FOREIGN KEY (`last_post_id`) REFERENCES `post`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;',
-                    6 => 'ALTER TABLE `post` ADD CONSTRAINT FK_5A8A6C8DA76ED395 FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;',
-                    7 => '-- DOWN MIGRATION --',
-                    8 => 'ALTER TABLE `user` DROP FOREIGN KEY FK_8D93D6492D053F64;',
-                    9 => 'ALTER TABLE `post` DROP FOREIGN KEY FK_5A8A6C8DA76ED395;',
-                    10 => 'DROP INDEX IX_8D93D6492D053F64 ON `user`;',
-                    11 => 'DROP TABLE `user`;',
-                    12 => 'DROP INDEX IX_5A8A6C8DA76ED395 ON `post`;',
-                    13 => 'DROP TABLE `post`;',
+                    1 => 'CREATE TABLE `user` (`id` INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,`firstname` VARCHAR(255) NOT NULL,`lastname` VARCHAR(255) NOT NULL,`email` VARCHAR(255) NOT NULL,`password` VARCHAR(255) NOT NULL,`token` VARCHAR(32) NOT NULL,`is_active` TINYINT(1) NOT NULL,`created_at` DATETIME DEFAULT NULL,`last_post_id` INT(11) DEFAULT NULL);',
+                    2 => 'CREATE UNIQUE INDEX IX_8D93D6495F37A13B ON `user` (`token`);',
+                    3 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON `user` (`last_post_id`);',
+                    4 => 'CREATE TABLE `post` (`id` INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,`title` VARCHAR(255) NOT NULL,`content` VARCHAR(255) NOT NULL,`slug` VARCHAR(128) NOT NULL,`created_at` DATETIME NOT NULL,`user_id` INT(11) DEFAULT NULL);',
+                    5 => 'CREATE UNIQUE INDEX IX_5A8A6C8D989D9B62 ON `post` (`slug`);',
+                    6 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON `post` (`user_id`);',
+                    7 => 'ALTER TABLE `user` ADD CONSTRAINT FK_8D93D6492D053F64 FOREIGN KEY (`last_post_id`) REFERENCES `post`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;',
+                    8 => 'ALTER TABLE `post` ADD CONSTRAINT FK_5A8A6C8DA76ED395 FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;',
+                    9 => '-- DOWN MIGRATION --',
+                    10 => 'ALTER TABLE `user` DROP FOREIGN KEY FK_8D93D6492D053F64;',
+                    11 => 'ALTER TABLE `post` DROP FOREIGN KEY FK_5A8A6C8DA76ED395;',
+                    12 => 'DROP INDEX IX_8D93D6495F37A13B ON `user`;',
+                    13 => 'DROP INDEX IX_8D93D6492D053F64 ON `user`;',
+                    14 => 'DROP TABLE `user`;',
+                    15 => 'DROP INDEX IX_5A8A6C8D989D9B62 ON `post`;',
+                    16 => 'DROP INDEX IX_5A8A6C8DA76ED395 ON `post`;',
+                    17 => 'DROP TABLE `post`;',
                 ));
                 break;
             default:

+ 87 - 1
tests/PersistAndFlushTest.php

@@ -4,8 +4,12 @@ namespace Test\PhpDevCommunity\PaperORM;
 
 use PhpDevCommunity\PaperORM\EntityManager;
 use PhpDevCommunity\PaperORM\Expression\Expr;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
+use PhpDevCommunity\PaperORM\Tools\IDBuilder;
 use PhpDevCommunity\UniTester\TestCase;
+use Test\PhpDevCommunity\PaperORM\Entity\CommentTest;
+use Test\PhpDevCommunity\PaperORM\Entity\InvoiceTest;
 use Test\PhpDevCommunity\PaperORM\Entity\PostTest;
 use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
 use Test\PhpDevCommunity\PaperORM\Helper\DataBaseHelperTest;
@@ -25,7 +29,7 @@ class PersistAndFlushTest extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as  $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             DataBaseHelperTest::init($em);
             $this->testInsert($em);
             $this->testInsertAndUpdate($em);
@@ -42,6 +46,7 @@ class PersistAndFlushTest extends TestCase
     {
         $user = new UserTest();
         $this->assertNull($user->getId());
+        $this->assertNull($user->getToken());
         $user->setFirstname('John');
         $user->setLastname('Doe');
         $user->setPassword('secret');
@@ -50,16 +55,92 @@ class PersistAndFlushTest extends TestCase
         $em->persist($user);
         $em->flush();
 
+        $this->assertStringLength($user->getToken(), 32);
         $this->assertNotNull($user->getId());
         $this->assertInstanceOf(\DateTimeInterface::class, $user->getCreatedAt());
         $this->assertInstanceOf(\DateTimeInterface::class, $user->getCreatedAt());
         $em->clear();
+
+        $post = new PostTest();
+        $post->setUser($user);
+        $post->setTitle('Hello World !, it\'s me');
+        $post->setContent('Hello World !');
+        $this->assertNull($post->getSlug());
+        $em->persist($post);
+        $em->flush();
+
+        $this->assertStrictEquals('hello-world-it-s-me', $post->getSlug());
+        $em->clear();
+
+
+        $post = new PostTest();
+        $post->setUser($user);
+        $post->setTitle('Hello World !, it\'s me');
+        $post->setContent('Hello World !');
+        $post->setSlug('my-slug');
+        $em->persist($post);
+        $em->flush();
+
+        $this->assertStrictEquals('my-slug', $post->getSlug());
+        $em->clear();
+
+        $comment = new CommentTest();
+        $this->assertNull($comment->getUuid());
+        $comment->setPost($post);
+        $comment->setBody("my comment");
+        $em->persist($comment);
+        $em->flush();
+        $this->assertNotNull($comment->getUuid());
+
+        $uuid = $comment->getUuid();
+        $em->clear();
+
+        $comment = $em->getRepository(CommentTest::class)
+            ->findOneBy(['uuid' => $uuid])
+            ->with('post.user')
+            ->toReadOnlyObject()
+        ;
+        $this->assertNotNull($comment);
+        $this->assertEquals($uuid, $comment->getUuid());
+        $this->assertInstanceOf(CommentTest::class, $comment);
+        $this->assertNotNull($comment->getUuid());
+        $this->assertNotNull($comment->getPost());
+        $this->assertInstanceOf(PostTest::class, $comment->getPost());
+        $this->assertNotNull($comment->getPost()->getId());
+        $this->assertNotNull($comment->getPost()->getUser());
+        $this->assertInstanceOf(UserTest::class, $comment->getPost()->getUser());
+        $this->assertNotNull($comment->getPost()->getUser()->getId());
+
+        $keyNumber = 'invoice.number.'.IDBuilder::generate('INV-{YYYY}-');
+        $keyCode = 'invoice.code';
+        $em->sequence()->reset($keyNumber);
+        $em->sequence()->reset($keyCode);
+        $em->sequence()->increment($keyCode);
+
+        for ($i = 0; $i < 10; $i++) {
+            $peekNumber = $em->sequence()->peek($keyNumber);
+            $peekCode = $em->sequence()->peek($keyCode);
+            $invoice = new InvoiceTest();
+            $this->assertEmpty($invoice->getCode());
+            $this->assertEmpty($invoice->getNumber());
+            $em->persist($invoice);
+            $em->flush();
+            $this->assertNotEmpty($invoice->getCode());
+            $this->assertNotEmpty($invoice->getNumber());
+            $this->assertEquals($em->sequence()->peek($keyNumber), $peekNumber + 1);
+            $this->assertEquals($em->sequence()->peek($keyCode), $peekCode + 1);
+        }
+
+        $this->assertEquals(11, $em->sequence()->peek($keyNumber));
+        $this->assertEquals(12, $em->sequence()->peek($keyCode));
+
     }
 
 
     private function testInsertAndUpdate(EntityManager $em): void
     {
         $user = new UserTest();
+        $this->assertNull($user->getToken());
         $user->setFirstname('John');
         $user->setLastname('Doe');
         $user->setPassword('secret');
@@ -67,12 +148,17 @@ class PersistAndFlushTest extends TestCase
         $user->setActive(true);
         $em->persist($user);
         $em->flush();
+
+        $token = $user->getToken();
+        $this->assertNotNull($token);
         $this->assertNotNull($user->getId());
         $this->assertInstanceOf(\DateTimeInterface::class, $user->getCreatedAt());
         $user->setLastname('TOTO');
         $em->persist($user);
         $em->flush();
         $em->clear();
+
+        $this->assertEquals($token, $user->getToken());
     }
 
     private function testUpdate(EntityManager $em): void

+ 2 - 1
tests/PlatformDiffTest.php

@@ -6,6 +6,7 @@ use PhpDevCommunity\PaperORM\EntityManager;
 use PhpDevCommunity\PaperORM\Mapping\Column\BoolColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\UniTester\TestCase;
 use Test\PhpDevCommunity\PaperORM\Helper\DataBaseHelperTest;
 
@@ -25,7 +26,7 @@ class PlatformDiffTest extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as $name => $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             $this->executeTest($em);
             $em->getConnection()->close();
         }

+ 2 - 1
tests/PlatformTest.php

@@ -8,6 +8,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\IntColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\UniTester\TestCase;
 use Test\PhpDevCommunity\PaperORM\Helper\DataBaseHelperTest;
 
@@ -26,7 +27,7 @@ class PlatformTest extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             $this->testCreateTables($em);
             $this->testDropTable($em);
             $this->testDropColumn($em);

+ 68 - 0
tests/RegistryTest.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace Test\PhpDevCommunity\PaperORM;
+
+use PhpDevCommunity\PaperORM\Collector\EntityDirCollector;
+use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\Expression\Expr;
+use PhpDevCommunity\PaperORM\Migration\PaperMigration;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
+use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
+use PhpDevCommunity\PaperORM\Tools\EntityExplorer;
+use PhpDevCommunity\UniTester\TestCase;
+use Test\PhpDevCommunity\PaperORM\Entity\PostTest;
+use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
+use Test\PhpDevCommunity\PaperORM\Helper\DataBaseHelperTest;
+
+class RegistryTest extends TestCase
+{
+
+    private string $migrationDir;
+
+    protected function setUp(): void
+    {
+        $this->migrationDir = __DIR__ . '/migrations';
+        $this->tearDown();
+    }
+
+    protected function tearDown(): void
+    {
+        $folder = $this->migrationDir;
+        array_map('unlink', glob("$folder/*.*"));
+    }
+
+    protected function execute(): void
+    {
+        foreach (DataBaseHelperTest::drivers() as  $params) {
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
+            $paperMigration = PaperMigration::create($em, 'mig_versions', $this->migrationDir);
+            $entityDirCollector = EntityDirCollector::bootstrap();
+            $this->assertEquals(1, count($entityDirCollector->all()));
+            $entities = EntityExplorer::getEntities($entityDirCollector->all());
+            $this->assertEquals(1, count($entities['system']));
+            $result = $paperMigration->getSqlDiffFromEntities($entities['system']);
+            foreach ($result as $sql) {
+                $em->getConnection()->executeStatement($sql);
+            }
+            $this->test($em);
+            $em->getConnection()->close();
+        }
+    }
+
+    private function test(EntityManager $em): void
+    {
+        $em->registry()->set('test', 'test');
+        $this->assertEquals('test', $em->registry()->get('test'));
+
+        $em->registry()->remove('test');
+        $this->assertEquals(null, $em->registry()->get('test'));
+
+        $value = $em->sequence()->peek("test");
+        $this->assertEquals(1, $value);
+
+        $em->sequence()->increment("test");
+        $value = $em->sequence()->peek("test");
+        $this->assertEquals(2, $value);
+    }
+
+}

+ 2 - 1
tests/RepositoryTest.php

@@ -3,6 +3,7 @@
 namespace Test\PhpDevCommunity\PaperORM;
 
 use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\PaperConfiguration;
 use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
 use PhpDevCommunity\UniTester\TestCase;
 use Test\PhpDevCommunity\PaperORM\Entity\PostTest;
@@ -23,7 +24,7 @@ class RepositoryTest extends TestCase
     protected function execute(): void
     {
         foreach (DataBaseHelperTest::drivers() as $params) {
-            $em = new EntityManager($params);
+            $em = EntityManager::createFromConfig(PaperConfiguration::fromArray($params));
             DataBaseHelperTest::init($em);
             $this->testSelectWithoutJoin($em);
             $this->testSelectWithoutProxy($em);