From 16a7bafdcdb8c3ee5a68b323b07ca5ed6998028d Mon Sep 17 00:00:00 2001 From: Jakub Kulhan Date: Fri, 24 May 2024 15:19:00 +0200 Subject: [PATCH] FindMethodCompiler generates SQL and passes it to SQLMethodCompiler --- data-access-kit/src/Registry.php | 18 ++- data-access-kit/src/Repository/Compiler.php | 4 +- .../Repository/Method/FindMethodCompiler.php | 104 +++++++----------- .../CompilerTest.testCompile.findArray.txt | 7 +- .../CompilerTest.testCompile.findIterable.txt | 7 +- .../Snapshot/CompilerTest.testCompile.get.txt | 11 +- .../CompilerTest.testCompile.nullableGet.txt | 9 +- ...rTest.testCompile.passMethodAttributes.txt | 11 +- 8 files changed, 65 insertions(+), 106 deletions(-) diff --git a/data-access-kit/src/Registry.php b/data-access-kit/src/Registry.php index f6998de..6f1334a 100644 --- a/data-access-kit/src/Registry.php +++ b/data-access-kit/src/Registry.php @@ -25,6 +25,9 @@ public function get(object|string $objectOrClass, bool $requireTable = false): T $className = is_object($objectOrClass) ? get_class($objectOrClass) : $objectOrClass; if (isset($this->tablesByClassName[$className])) { + if ($requireTable && $this->tablesByClassName[$className]->name === null) { + throw static::missingTableException($className); + } return $this->tablesByClassName[$className]; } @@ -32,11 +35,7 @@ public function get(object|string $objectOrClass, bool $requireTable = false): T $tableRA = $rc->getAttributes(Table::class)[0] ?? null; if ($tableRA === null) { if ($requireTable) { - throw new LogicException(sprintf( - "Class %s is missing #[\\%s] attribute.", - $className, - Table::class, - )); + throw static::missingTableException($className); } $table = new Table(); @@ -78,4 +77,13 @@ public function get(object|string $objectOrClass, bool $requireTable = false): T return $this->tablesByClassName[$className] = $table; } + private static function missingTableException(object|string $className): LogicException + { + return new LogicException(sprintf( + "Class %s is missing #[\\%s] attribute.", + $className, + Table::class, + )); + } + } diff --git a/data-access-kit/src/Repository/Compiler.php b/data-access-kit/src/Repository/Compiler.php index 9456142..eefc747 100644 --- a/data-access-kit/src/Repository/Compiler.php +++ b/data-access-kit/src/Repository/Compiler.php @@ -43,9 +43,9 @@ public function __construct( Registry $registry, ) { - $this->registerMethodCompiler(Find::class, new FindMethodCompiler($registry)); + $this->registerMethodCompiler(SQL::class, $sqlMethodCompiler = new SQLMethodCompiler()); + $this->registerMethodCompiler(Find::class, new FindMethodCompiler($registry, $sqlMethodCompiler)); $this->registerMethodCompiler(Count::class, new CountMethodCompiler()); - $this->registerMethodCompiler(SQL::class, new SQLMethodCompiler()); $this->registerMethodCompiler(Delegate::class, new DelegateMethodCompiler()); } diff --git a/data-access-kit/src/Repository/Method/FindMethodCompiler.php b/data-access-kit/src/Repository/Method/FindMethodCompiler.php index 431a78d..9df7290 100644 --- a/data-access-kit/src/Repository/Method/FindMethodCompiler.php +++ b/data-access-kit/src/Repository/Method/FindMethodCompiler.php @@ -5,6 +5,7 @@ use DataAccessKit\PersistenceInterface; use DataAccessKit\Registry; use DataAccessKit\Repository\Attribute\Find; +use DataAccessKit\Repository\Attribute\SQL; use DataAccessKit\Repository\Compiler; use DataAccessKit\Repository\MethodCompilerInterface; use DataAccessKit\Repository\Result; @@ -25,82 +26,57 @@ class FindMethodCompiler implements MethodCompilerInterface public function __construct( private readonly Registry $registry, + private readonly SQLMethodCompiler $sqlMethodCompiler, ) { } public function compile(Result $result, ResultMethod $method, $attribute): void { - $constructor = $result->method("__construct"); - $constructor->parameter("persistence") - ->setVisibility("private readonly") - ->setType($result->use(PersistenceInterface::class)); - $constructor->parameter("registry") - ->setVisibility("private readonly") - ->setType($result->use(Registry::class)); - $constructor->parameter("valueConverter") - ->setVisibility("private readonly") - ->setType($result->use(ValueConverterInterface::class)); - - $alias = $result->use($result->repository->class); - $table = $this->registry->get($result->repository->class, true); - $where = []; - $parameters = []; - foreach ($method->reflection->getParameters() as $parameter) { - $column = null; - foreach ($table->columns as $candidate) { - if ($candidate->reflection->getName() === $parameter->getName()) { - $column = $candidate; - break; + $select = $attribute->select; + if ($select === null) { + $select = implode(", ", array_map(fn($column) => $attribute->alias . "." . $column->name, $table->columns)); + } + $from = $attribute->from ?? $table->name; + $where = $attribute->where; + if ($where === null) { + $conditions = []; + foreach ($method->reflection->getParameters() as $parameter) { + $column = null; + foreach ($table->columns as $candidate) { + if ($candidate->reflection->getName() === $parameter->getName()) { + $column = $candidate; + break; + } + } + if ($column === null) { + throw new LogicException(sprintf( + "Method [%s::%s] parameter [%s] does not match any property of [%s], and therefore cannot be used as a query condition.", + $result->reflection->getName(), + $method->reflection->getName(), + $parameter->getName(), + $result->repository->class, + )); } - } - if ($column === null) { - throw new LogicException(sprintf( - "Method [%s::%s] parameter [%s] does not match any property of [%s], and therefore cannot be used as a query condition.", - $result->reflection->getName(), - $method->reflection->getName(), - $parameter->getName(), - $result->repository->class, - )); - } - $where[] = "t.{$column->name} = ?"; - $parameters[] = "\$this->valueConverter->objectToDatabase(\$_table, \$_table->columns[" . Compiler::varExport($column->name) . "], \${$parameter->getName()})"; + $conditions[] = "{$attribute->alias}.{$column->name} = @{$parameter->getName()}"; + } + $where = implode(" AND ", $conditions); } - $query = "SELECT " . - implode(", ", array_map(fn($column) => "t.{$column->name}", $table->columns)) . - " FROM {$table->name} t" . - " WHERE " . implode(" AND ", $where); - $method - ->line("\$_table = \$this->registry->get({$alias}::class);") - ->line("\$result = \$this->persistence->query({$alias}::class, " . Compiler::varExport($query) . ", [" . implode(", ", $parameters) . "]);"); - - $returnType = $method->reflection->getReturnType(); - assert($returnType instanceof ReflectionNamedType); - - if ($returnType->getName() === "iterable") { - $method->line("return \$result;"); - } else if ($returnType->getName() === "array") { - $method->line("return iterator_to_array(\$result);"); - } else if ($returnType->getName() === $result->repository->class) { - $method - ->line("\$objects = iterator_to_array(\$result);") - ->line("if (count(\$objects) === 0) {"); - if ($returnType->allowsNull()) { - $method->indent()->line("return null;")->dedent(); - } else { - $method->indent()->line("throw new \\RuntimeException(\"Entity not found.\");")->dedent(); - } - $method - ->line("} else if (count(\$objects) > 1) {") - ->indent()->line("throw new \\RuntimeException(\"Multiple entities found.\");")->dedent() - ->line("}") - ->line("return \$objects[0];"); - } else { - throw new LogicException("Unexpected return type: {$returnType->getName()}."); + $sql = "SELECT {$select} FROM {$from} {$attribute->alias} WHERE {$where}"; + if ($attribute->orderBy !== null) { + $sql .= " ORDER BY {$attribute->orderBy}"; } + if ($attribute->limit !== null) { + $sql .= " LIMIT {$attribute->limit}"; + } + if ($attribute->offset !== null) { + $sql .= " OFFSET {$attribute->offset}"; + } + + $this->sqlMethodCompiler->compile($result, $method, new SQL($sql)); } } diff --git a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findArray.txt b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findArray.txt index d14951c..f3e5217 100644 --- a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findArray.txt +++ b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findArray.txt @@ -3,15 +3,11 @@ namespace DataAccessKit\Repository\Fixture; use DataAccessKit\PersistenceInterface; -use DataAccessKit\Registry; -use DataAccessKit\ValueConverterInterface; final class FindArrayRepository implements FindArrayRepositoryInterface { public function __construct( private readonly PersistenceInterface $persistence, - private readonly Registry $registry, - private readonly ValueConverterInterface $valueConverter, ) { } @@ -20,8 +16,7 @@ final class FindArrayRepository implements FindArrayRepositoryInterface string $title, ): array { - $_table = $this->registry->get(Foo::class); - $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.title = ?', [$this->valueConverter->objectToDatabase($_table, $_table->columns['title'], $title)]); + $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.title = ?', [$title]); return iterator_to_array($result); } } diff --git a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findIterable.txt b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findIterable.txt index 6c35ad3..ce90114 100644 --- a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findIterable.txt +++ b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.findIterable.txt @@ -3,15 +3,11 @@ namespace DataAccessKit\Repository\Fixture; use DataAccessKit\PersistenceInterface; -use DataAccessKit\Registry; -use DataAccessKit\ValueConverterInterface; final class FindIterableRepository implements FindIterableRepositoryInterface { public function __construct( private readonly PersistenceInterface $persistence, - private readonly Registry $registry, - private readonly ValueConverterInterface $valueConverter, ) { } @@ -20,8 +16,7 @@ final class FindIterableRepository implements FindIterableRepositoryInterface string $title, ): iterable { - $_table = $this->registry->get(Foo::class); - $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.title = ?', [$this->valueConverter->objectToDatabase($_table, $_table->columns['title'], $title)]); + $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.title = ?', [$title]); return $result; } } diff --git a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.get.txt b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.get.txt index f808200..8cad9fa 100644 --- a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.get.txt +++ b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.get.txt @@ -3,15 +3,11 @@ namespace DataAccessKit\Repository\Fixture; use DataAccessKit\PersistenceInterface; -use DataAccessKit\Registry; -use DataAccessKit\ValueConverterInterface; final class GetRepository implements GetRepositoryInterface { public function __construct( private readonly PersistenceInterface $persistence, - private readonly Registry $registry, - private readonly ValueConverterInterface $valueConverter, ) { } @@ -20,13 +16,12 @@ final class GetRepository implements GetRepositoryInterface int $id, ): Foo { - $_table = $this->registry->get(Foo::class); - $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.id = ?', [$this->valueConverter->objectToDatabase($_table, $_table->columns['id'], $id)]); + $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.id = ?', [$id]); $objects = iterator_to_array($result); if (count($objects) === 0) { - throw new \RuntimeException("Entity not found."); + throw new LogicException("Not found"); } else if (count($objects) > 1) { - throw new \RuntimeException("Multiple entities found."); + throw new LogicException("Multiple objects found"); } return $objects[0]; } diff --git a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.nullableGet.txt b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.nullableGet.txt index 446e6ba..413c56c 100644 --- a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.nullableGet.txt +++ b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.nullableGet.txt @@ -3,15 +3,11 @@ namespace DataAccessKit\Repository\Fixture; use DataAccessKit\PersistenceInterface; -use DataAccessKit\Registry; -use DataAccessKit\ValueConverterInterface; final class NullableGetRepository implements NullableGetRepositoryInterface { public function __construct( private readonly PersistenceInterface $persistence, - private readonly Registry $registry, - private readonly ValueConverterInterface $valueConverter, ) { } @@ -20,13 +16,12 @@ final class NullableGetRepository implements NullableGetRepositoryInterface int $id, ): ?Foo { - $_table = $this->registry->get(Foo::class); - $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.id = ?', [$this->valueConverter->objectToDatabase($_table, $_table->columns['id'], $id)]); + $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.id = ?', [$id]); $objects = iterator_to_array($result); if (count($objects) === 0) { return null; } else if (count($objects) > 1) { - throw new \RuntimeException("Multiple entities found."); + throw new LogicException("Multiple objects found"); } return $objects[0]; } diff --git a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.passMethodAttributes.txt b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.passMethodAttributes.txt index 7aae072..cb40160 100644 --- a/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.passMethodAttributes.txt +++ b/data-access-kit/test/Repository/Snapshot/CompilerTest.testCompile.passMethodAttributes.txt @@ -3,15 +3,11 @@ namespace DataAccessKit\Repository\Fixture; use DataAccessKit\PersistenceInterface; -use DataAccessKit\Registry; -use DataAccessKit\ValueConverterInterface; final class PassMethodAttributesRepository implements PassMethodAttributesRepositoryInterface { public function __construct( private readonly PersistenceInterface $persistence, - private readonly Registry $registry, - private readonly ValueConverterInterface $valueConverter, ) { } @@ -21,13 +17,12 @@ final class PassMethodAttributesRepository implements PassMethodAttributesReposi int $id, ): Foo { - $_table = $this->registry->get(Foo::class); - $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.id = ?', [$this->valueConverter->objectToDatabase($_table, $_table->columns['id'], $id)]); + $result = $this->persistence->query(Foo::class, 'SELECT t.id, t.title FROM foos t WHERE t.id = ?', [$id]); $objects = iterator_to_array($result); if (count($objects) === 0) { - throw new \RuntimeException("Entity not found."); + throw new LogicException("Not found"); } else if (count($objects) > 1) { - throw new \RuntimeException("Multiple entities found."); + throw new LogicException("Multiple objects found"); } return $objects[0]; }