瀏覽代碼

implement insert logic ORM

phpdevcommunity 7 月之前
父節點
當前提交
214119bdc0

+ 2 - 1
composer.json

@@ -25,7 +25,8 @@
     "ext-ctype": "*",
     "phpdevcommunity/relational-query": "^1.0",
     "phpdevcommunity/php-console": "^1.0",
-    "phpdevcommunity/michel-package-starter": "^1.0"
+    "phpdevcommunity/michel-package-starter": "^1.0",
+    "phpdevcommunity/php-filesystem": "^1.0"
   },
   "require-dev": {
     "phpdevcommunity/unitester": "^0.1.0@alpha"

+ 128 - 0
src/Command/MigrationDiffCommand.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Command;
+
+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\Migration\PaperMigration;
+
+class MigrationDiffCommand implements CommandInterface
+{
+    private PaperMigration $paperMigration;
+    private ?string $defaultEntitiesDir;
+
+    public function __construct(PaperMigration $paperMigration, ?string $defaultEntitiesDir = null)
+    {
+        $this->paperMigration = $paperMigration;
+        $this->defaultEntitiesDir = $defaultEntitiesDir;
+    }
+
+    public function getName(): string
+    {
+        return 'paper:migration:diff';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Generate a migration diff for the SQL database';
+    }
+
+    public function getOptions(): array
+    {
+        return [
+            new CommandOption('entities-dir', null, 'The directory where the entities are', false),
+            new CommandOption('output', 'o', 'The output file', true)
+        ];
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = ConsoleOutput::create($output);
+
+        $entitiesDir = $this->defaultEntitiesDir;
+        $output = $input->hasOption('output');
+        if ($input->hasOption('entities-dir')) {
+            $entitiesDir = $input->getOptionValue('entities-dir');
+        }
+
+        if ($entitiesDir === null) {
+            throw new \LogicException('The --entities-dir option is required');
+        }
+
+        $platform = $this->paperMigration->getEntityManager()->createDatabasePlatform();
+
+        $io->title('Starting migration diff on ' . $platform->getDatabaseName());
+        $io->list([
+            'Database : ' . $platform->getDatabaseName(),
+            'Entities directory : ' . $entitiesDir
+        ]);
+
+        $explorer = new FileExplorer($entitiesDir);
+        $files = $explorer->searchByExtension('php', true);
+        $entities = [];
+        foreach ($files as $file) {
+            $entityClass = self::getFullClassName($file['path']);
+            if ($entityClass !== null) {
+                $entities[$file['path']] = $entityClass;
+            }
+        }
+
+        $io->title('Number of entities detected: ' . count($entities));
+        $io->listKeyValues($entities);
+
+        $file = $this->paperMigration->diffEntities($entities);
+        if ($file === null) {
+            $io->info('No migration file was generated — all entities are already in sync with the database schema.');
+            return;
+        }
+
+        if ($output === true) {
+            $splFile = new \SplFileObject($file);
+            $lines = [];
+            while (!$splFile->eof()) {
+                $lines[] = $splFile->fgets();
+            }
+            unset($splFile);
+            $io->listKeyValues($lines);
+        }
+
+        $io->success('Migration file successfully generated: ' . $file);
+    }
+
+    private static function getFullClassName($file): ?string
+    {
+        $content = file_get_contents($file);
+        $tokens = token_get_all($content);
+        $namespace = $className = '';
+
+        foreach ($tokens as $i => $token) {
+            if ($token[0] === T_NAMESPACE) {
+                for ($j = $i + 1; isset($tokens[$j]); $j++) {
+                    if ($tokens[$j] === ';') break;
+                    if (is_array($tokens[$j]) && in_array($tokens[$j][0], [T_STRING, T_NS_SEPARATOR])) {
+                        $namespace .= $tokens[$j][1];
+                    }
+                }
+            }
+
+            if ($token[0] === T_CLASS && isset($tokens[$i + 2][1])) {
+                $className = $tokens[$i + 2][1];
+                break;
+            }
+        }
+        if (empty($className)) {
+            return null;
+        }
+
+        return trim($namespace . '\\' . $className, '\\');
+    }
+}

+ 71 - 0
src/Command/MigrationMigrateCommand.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Command;
+
+use PhpDevCommunity\Console\Command\CommandInterface;
+use PhpDevCommunity\Console\InputInterface;
+use PhpDevCommunity\Console\Output\ConsoleOutput;
+use PhpDevCommunity\Console\OutputInterface;
+use PhpDevCommunity\PaperORM\Migration\PaperMigration;
+
+class MigrationMigrateCommand implements CommandInterface
+{
+    private PaperMigration $paperMigration;
+
+    public function __construct(PaperMigration $paperMigration)
+    {
+        $this->paperMigration = $paperMigration;
+    }
+
+    public function getName(): string
+    {
+        return 'paper:migration:migrate';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Execute all migrations';
+    }
+
+    public function getOptions(): array
+    {
+        return [
+        ];
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): void
+    {
+        $io = ConsoleOutput::create($output);
+
+        $platform = $this->paperMigration->getEntityManager()->createDatabasePlatform();
+
+        $io->title('Starting migration migrate on ' . $platform->getDatabaseName());
+
+        $successList = [];
+        $error = null;
+        try {
+            $this->paperMigration->migrate();
+            $successList = $this->paperMigration->getSuccessList();
+        }catch (\Throwable $exception){
+            $error = $exception->getMessage();
+        }
+
+
+        foreach ($successList as $version) {
+            $io->success('Migration successfully executed: version ' . $version);
+        }
+
+        if (empty($successList)) {
+            $io->info('No migrations to run. The database is already up to date.');
+        }
+
+        if ($error !== null) {
+            throw new \RuntimeException('An error occurred during the migration process: ' . $error);
+        }
+    }
+}

+ 8 - 4
src/Command/QueryExecuteCommand.php

@@ -19,7 +19,7 @@ class QueryExecuteCommand implements CommandInterface
 
     public function getName(): string
     {
-       return 'paper:query:execute';
+        return 'paper:query:execute';
     }
 
     public function getDescription(): string
@@ -42,17 +42,21 @@ class QueryExecuteCommand implements CommandInterface
     public function execute(InputInterface $input, OutputInterface $output): void
     {
         $io = ConsoleOutput::create($output);
-        $query = $input->getOptionValue("query");
+        $query = $input->hasArgument("query") ? $input->getArgumentValue("query") : null;
         if ($query === null) {
             throw new \LogicException("SQL query is required");
         }
+        $io->title('Starting query on ' . $this->entityManager->createDatabasePlatform()->getDatabaseName());
+
         $data = $this->entityManager->getConnection()->fetchAll($query);
+        $io->listKeyValues([
+            'query' => $query,
+            'rows' => count($data),
+        ]);
         if ($data === []) {
             $io->info('The query yielded an empty result set.');
             return;
         }
-
-        $io->title('Database : ' . $this->entityManager->createDatabasePlatform()->getDatabaseName());
         $io->table(array_keys($data[0]), $data);
     }
 }

+ 6 - 1
src/Command/ShowTablesCommand.php

@@ -75,11 +75,16 @@ class ShowTablesCommand implements CommandInterface
                 $io->title(sprintf('Table : %s', $table));
                 if ($withColumns === true) {
                     $columns = array_map(function (ColumnMetadata $column) {
-                        return $column->toArray();
+                        $data =  $column->toArray();
+                        foreach ($data as $key => $value) {
+                            $data[$key] = is_array($value) ? json_encode($value) : $value;
+                        }
+                        return $data;
                     }, $platform->listTableColumns($table));
                     $io->table(array_keys($columns[0]), $columns);
                 }
             }
+            $io->writeln('');
         }
     }
 }

+ 5 - 5
src/Hydrator/EntityHydrator.php

@@ -24,16 +24,16 @@ final class EntityHydrator
 
     public function hydrate($objectOrClass, array $data): object
     {
-        if (!class_exists($objectOrClass)) {
-            throw new LogicException('Class ' . $objectOrClass . ' does not exist');
-        }
         if (!is_subclass_of($objectOrClass, EntityInterface::class)) {
             throw new LogicException('Class ' . $objectOrClass . ' is not an PhpDevCommunity\PaperORM\Entity\EntityInterface');
         }
         $object = $objectOrClass;
-        if (!is_object($object)) {
+        if (is_string($objectOrClass)) {
+            if (!class_exists($objectOrClass)) {
+                throw new LogicException('Class ' . $objectOrClass . ' does not exist');
+            }
             $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($object);
-            $object = $this->cache->get($objectOrClass, $data[$primaryKeyColumn]) ?: $this->createProxyObject($object);
+            $object = $this->cache->get($objectOrClass, $data[$primaryKeyColumn]) ?: $this->createProxyObject($objectOrClass);
             $this->cache->set($objectOrClass, $data[$primaryKeyColumn], $object);
         }
         $reflection = new ReflectionClass($object);

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

@@ -15,6 +15,6 @@ final class TextColumn extends Column
         string $defaultValue = null
     )
     {
-        parent::__construct($property, $name, StringType::class, false, $nullable, $defaultValue);
+        parent::__construct($property, $name, StringType::class, $nullable, $defaultValue);
     }
 }

+ 35 - 2
src/Michel/Package/MichelPaperORMPackage.php

@@ -2,13 +2,16 @@
 
 namespace PhpDevCommunity\PaperORM\Michel\Package;
 
-use Exception;
+use LogicException;
 use PhpDevCommunity\Michel\Package\PackageInterface;
 use PhpDevCommunity\PaperORM\Command\DatabaseCreateCommand;
 use PhpDevCommunity\PaperORM\Command\DatabaseDropCommand;
+use PhpDevCommunity\PaperORM\Command\MigrationDiffCommand;
+use PhpDevCommunity\PaperORM\Command\MigrationMigrateCommand;
 use PhpDevCommunity\PaperORM\Command\QueryExecuteCommand;
 use PhpDevCommunity\PaperORM\Command\ShowTablesCommand;
 use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\Migration\PaperMigration;
 use PhpDevCommunity\PaperORM\Parser\DSNParser;
 use Psr\Container\ContainerInterface;
 
@@ -20,10 +23,23 @@ class MichelPaperORMPackage implements PackageInterface
             EntityManager::class => static function (ContainerInterface $container) {
                 $dsn = $container->get('database.dsn');
                 if (!is_string($dsn) || empty($dsn)) {
-                    throw new \LogicException('Database DSN not found, please set DATABASE_DSN in .env file or database.dsn in config');
+                    throw new LogicException('Database DSN not found, please set DATABASE_DSN in .env file or database.dsn in config');
                 }
                 $params = DSNParser::parse($container->get('database.dsn'));
                 return new EntityManager($params);
+            },
+            PaperMigration::class => static function (ContainerInterface $container) {
+                return PaperMigration::create(
+                    $container->get(EntityManager::class),
+                    $container->get('paper.migration.table'),
+                    $container->get('paper.migration.dir')
+                );
+            },
+            MigrationDiffCommand::class => static function (ContainerInterface $container) {
+                return new MigrationDiffCommand($container->get(PaperMigration::class), $container->get('paper.entity.dir'));
+            },
+            DatabaseDropCommand::class => static function (ContainerInterface $container) {
+                return new DatabaseDropCommand($container->get(EntityManager::class), $container->get('michel.environment'));
             }
         ];
     }
@@ -32,6 +48,21 @@ class MichelPaperORMPackage implements PackageInterface
     {
         return [
             'database.dsn' => getenv('DATABASE_DSN') ?? '',
+            'paper.migration.dir' => getenv('PAPER_MIGRATION_DIR') ?: function (ContainerInterface $container) {
+                $folder = $container->get('michel.project_dir') . DIRECTORY_SEPARATOR . 'migrations';
+                if (!is_dir($folder)) {
+                    mkdir($folder, 0777, true);
+                }
+                return $folder;
+            },
+            'paper.migration.table' => getenv('PAPER_MIGRATION_TABLE') ?: 'mig_versions',
+            'paper.entity.dir' => getenv('PAPER_ENTITY_DIR') ?: function (ContainerInterface $container) {
+                $folder = $container->get('michel.project_dir') . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Entity';
+                if (!is_dir($folder)) {
+                    mkdir($folder, 0777, true);
+                }
+                return $folder;
+            },
         ];
     }
 
@@ -50,6 +81,8 @@ class MichelPaperORMPackage implements PackageInterface
         return [
             DatabaseCreateCommand::class,
             DatabaseDropCommand::class,
+            MigrationDiffCommand::class,
+            MigrationMigrateCommand::class,
             QueryExecuteCommand::class,
             ShowTablesCommand::class,
         ];

+ 6 - 2
src/Migration/PaperMigration.php

@@ -176,8 +176,7 @@ SQL;
             foreach (explode(';' . PHP_EOL, self::contentDown($migration)) as $query) {
                 $this->getConnection()->executeStatement(rtrim($query, ';') . ';');
             }
-
-            $rows = $this->getConnection()->executeStatement('DELETE FROM ' . $this->tableName . ' WHERE version = :version', ['version' => $version]);
+            $this->getConnection()->executeStatement('DELETE FROM ' . $this->tableName . ' WHERE version = :version', ['version' => $version]);
 
             $pdo->commit();
 
@@ -224,4 +223,9 @@ SQL;
     {
         return $this->successList;
     }
+
+    public function getEntityManager(): EntityManager
+    {
+        return $this->em;
+    }
 }

+ 9 - 4
src/Platform/SqlitePlatform.php

@@ -2,6 +2,7 @@
 
 namespace PhpDevCommunity\PaperORM\Platform;
 
+use http\Exception\RuntimeException;
 use LogicException;
 use PhpDevCommunity\PaperORM\Mapping\Column\BoolColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\Column;
@@ -116,16 +117,16 @@ class SqlitePlatform extends AbstractPlatform
         }
 
         if (empty($database)) {
-            throw new LogicException(sprintf("The database name cannot be empty. %s::createDatabase()", __CLASS__));
+            throw new RuntimeException(sprintf("The database name cannot be empty. %s::createDatabase()", __CLASS__));
         }
 
         $databaseFile = pathinfo($database);
         if (empty($databaseFile['extension'])) {
-            throw new LogicException(sprintf("The database name '%s' must have an extension.", $database));
+            throw new RuntimeException(sprintf("The database name '%s' must have an extension.", $database));
         }
 
         if (file_exists($database)) {
-            return;
+            throw new LogicException(sprintf("The database '%s' already exists.", $database));
         }
 
         touch($database);
@@ -133,7 +134,11 @@ class SqlitePlatform extends AbstractPlatform
 
     public function createDatabaseIfNotExists(): void
     {
-        $this->createDatabase();
+        try {
+            $this->createDatabase();
+        } catch (LogicException $e) {
+            return;
+        }
     }
 
     public function dropDatabase(): void

+ 21 - 4
src/Repository/Repository.php

@@ -6,6 +6,7 @@ use LogicException;
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
 use PhpDevCommunity\PaperORM\EntityManager;
 use PhpDevCommunity\PaperORM\Expression\Expr;
+use PhpDevCommunity\PaperORM\Hydrator\EntityHydrator;
 use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
 use PhpDevCommunity\PaperORM\Mapper\EntityMapper;
 use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
@@ -62,12 +63,28 @@ abstract class Repository
         return (new Fetcher($this->qb(), true))->where(...$expressions);
     }
 
-    public function insert(object $entity): int
+    public function insert(object $entityToInsert): int
     {
-        $this->checkEntity($entity);
-        if ($entity->getPrimaryKeyValue() !== null) {
-            throw new LogicException(static::class . sprintf(' Cannot insert an entity %s with a primary key ', get_class($entity)));
+        $this->checkEntity($entityToInsert);
+        if ($entityToInsert->getPrimaryKeyValue() !== null) {
+            throw new LogicException(static::class . sprintf(' Cannot insert an entity %s with a primary key ', get_class($entityToInsert)));
         }
+
+        $qb = \PhpDevCommunity\Sql\QueryBuilder::insert($this->getTableName());
+
+        $values = [];
+        foreach ((new SerializerToDb($entityToInsert))->serialize() as $key => $value) {
+            $keyWithoutBackticks = str_replace("`", "", $key);
+            $qb->setValue($key, ":$keyWithoutBackticks");
+            $values[$keyWithoutBackticks] = $value;
+        }
+        $rows = $this->em->getConnection()->executeStatement($qb, $values);
+        $lastInsertId = $this->em->getConnection()->getPdo()->lastInsertId();
+        if ($rows > 0) {
+            $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($entityToInsert);
+            (new EntityHydrator($this->em->getCache()))->hydrate($entityToInsert, [$primaryKeyColumn => $lastInsertId]);
+        }
+        return $rows;
     }
 
     public function update(object $entityToUpdate): int

+ 18 - 0
tests/PersistAndFlushTest.php

@@ -38,11 +38,29 @@ class PersistAndFlushTest extends TestCase
 
     protected function execute(): void
     {
+        $this->testInsert();
         $this->testUpdate();
         $this->testUpdateJoinColumn();
         $this->testDelete();
     }
 
+
+
+    private function testInsert(): void
+    {
+        $user = new UserTest();
+        $this->assertNull($user->getId());
+        $user->setFirstname('John');
+        $user->setLastname('Doe');
+        $user->setPassword('secret');
+        $user->setEmail('Xq5qI@example.com');
+        $user->setActive(true);
+        $this->em->persist($user);
+        $this->em->flush();
+        $this->assertNotNull($user->getId());
+        $this->em->clear();
+    }
+
     private function testUpdate(): void
     {
         $userRepository = $this->em->getRepository(UserTest::class);