From c0dd69eac0d7379e775dd7d47edbb814af979143 Mon Sep 17 00:00:00 2001 From: Josha von Gizycki Date: Tue, 20 Oct 2026 11:34:19 +0200 Subject: [PATCH] preparations for SaveEntity --- src/Analyze/EntityAnalyzer.php | 2 +- src/Analyze/RepositoryAnalyzer.php | 6 +- src/Analyze/Result/RepositoryMethod.php | 1 - .../Result/RepositoryMethodParameter.php | 5 ++ src/Analyze/Result/Type/ClassType.php | 6 +- src/Repository/InvalidCall.php | 15 +++++ .../ResolutionStrategy/EntityToDataMapper.php | 53 ++++++++++++++++ .../ResolutionStrategy/FindById.php | 2 +- .../ResolutionStrategy/SaveEntity.php | 51 +++++++++++++++- .../EntityToDataMapperTest.php | 61 +++++++++++++++++++ .../ResolutionStrategy/FindByIdTest.php | 9 ++- .../ResolutionStrategy/SqliteBaseCase.php | 10 ++- 12 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 src/Repository/InvalidCall.php create mode 100644 src/Repository/ResolutionStrategy/EntityToDataMapper.php create mode 100644 tests/Repository/ResolutionStrategy/EntityToDataMapperTest.php diff --git a/src/Analyze/EntityAnalyzer.php b/src/Analyze/EntityAnalyzer.php index 19522b4..7fe8dda 100644 --- a/src/Analyze/EntityAnalyzer.php +++ b/src/Analyze/EntityAnalyzer.php @@ -171,7 +171,7 @@ readonly class EntityAnalyzer "bool" => PrimitiveType::BOOLEAN, "int" => PrimitiveType::INTEGER, "float" => PrimitiveType::FLOAT, - default => new ClassType($classType) + default => new ClassType((new EntityAnalyzer($classType))->analyze()) }; } diff --git a/src/Analyze/RepositoryAnalyzer.php b/src/Analyze/RepositoryAnalyzer.php index a0b2119..393ddfd 100644 --- a/src/Analyze/RepositoryAnalyzer.php +++ b/src/Analyze/RepositoryAnalyzer.php @@ -161,9 +161,7 @@ readonly class RepositoryAnalyzer foreach ($refMethod->getParameters() as $param) { $type = $param->getType(); - if ($type instanceof ReflectionUnionType - || $type instanceof ReflectionIntersectionType - || $type === null) { + if (!$type instanceof ReflectionNamedType) { throw new InvalidRepository( "Type of $refMethod->class::$refMethod->name::$param->name has an invalid type. " . 'Simple type expected. Unions, Intersections or no type are not simple.' @@ -172,7 +170,7 @@ readonly class RepositoryAnalyzer $params[] = new RepositoryMethodParameter( name: $param->getName(), - type: $type->__toString(), + type: $type->getName(), function: ParameterFunction::STATEMENT_PARAMETER ); } diff --git a/src/Analyze/Result/RepositoryMethod.php b/src/Analyze/Result/RepositoryMethod.php index abb6b3f..93c9c3b 100644 --- a/src/Analyze/Result/RepositoryMethod.php +++ b/src/Analyze/Result/RepositoryMethod.php @@ -6,7 +6,6 @@ namespace Kartierung\Analyze\Result; use Kartierung\Repository\ResolutionStrategy\ResolutionStrategy; use Kartierung\Sql\LazyQuery; -use phpDocumentor\Reflection\Types\Boolean; readonly class RepositoryMethod { diff --git a/src/Analyze/Result/RepositoryMethodParameter.php b/src/Analyze/Result/RepositoryMethodParameter.php index 4d0322b..b5417d7 100644 --- a/src/Analyze/Result/RepositoryMethodParameter.php +++ b/src/Analyze/Result/RepositoryMethodParameter.php @@ -6,6 +6,11 @@ namespace Kartierung\Analyze\Result; readonly class RepositoryMethodParameter { + /** + * @param string $name + * @param string|class-string $type + * @param ParameterFunction $function + */ public function __construct( public string $name, public string $type, diff --git a/src/Analyze/Result/Type/ClassType.php b/src/Analyze/Result/Type/ClassType.php index a84ebf3..28a7db1 100644 --- a/src/Analyze/Result/Type/ClassType.php +++ b/src/Analyze/Result/Type/ClassType.php @@ -4,12 +4,14 @@ declare(strict_types=1); namespace Kartierung\Analyze\Result\Type; +use Kartierung\Analyze\Result\EntityResult; + readonly class ClassType { /** - * @param class-string $fqcn + * @param EntityResult $entity */ public function __construct( - public string $fqcn + public EntityResult $entity ) {} } diff --git a/src/Repository/InvalidCall.php b/src/Repository/InvalidCall.php new file mode 100644 index 0000000..7a40831 --- /dev/null +++ b/src/Repository/InvalidCall.php @@ -0,0 +1,15 @@ + $entityResult + * @param E $entity + * @return array + */ + public function byColumnName(EntityResult $entityResult, object $entity): array + { + $result = []; + + foreach ($entityResult->fields as $field) { + $type = $field->type; + $value = $this->fieldFromEntity($field, $entity); + + if ($type instanceof ArrayType) { + /** @var array $value */ + $result[$field->columnName] = array_map( + fn(object $listValue) => $this->byColumnName($type->entity, $listValue), + $value + ); + } elseif ($type instanceof ClassType) { + /** @var object $value */ + $result[$field->columnName] = $this->byColumnName($type->entity, $value); + } else { + $result[$field->columnName] = $value; + } + } + + return $result; + } + + private function fieldFromEntity(EntityField $field, object $entity): mixed + { + return match ($field->readAccess) { + FieldAccess::PUBLIC => $entity->{$field->name}, + FieldAccess::GETTER => $entity->{'get' . ucfirst($field->name)}() + }; + } +} diff --git a/src/Repository/ResolutionStrategy/FindById.php b/src/Repository/ResolutionStrategy/FindById.php index 317966b..d6ee635 100644 --- a/src/Repository/ResolutionStrategy/FindById.php +++ b/src/Repository/ResolutionStrategy/FindById.php @@ -44,7 +44,7 @@ readonly class FindById implements ResolutionStrategy /** @var array|false $row */ $row = $stmt->fetch(mode: PDO::FETCH_ASSOC); - if($row) { + if ($row) { return $mapper->toEntity($row); } diff --git a/src/Repository/ResolutionStrategy/SaveEntity.php b/src/Repository/ResolutionStrategy/SaveEntity.php index 798500e..7ad7855 100644 --- a/src/Repository/ResolutionStrategy/SaveEntity.php +++ b/src/Repository/ResolutionStrategy/SaveEntity.php @@ -4,7 +4,11 @@ declare(strict_types=1); namespace Kartierung\Repository\ResolutionStrategy; +use Kartierung\Analyze\EntityAnalyzer; +use Kartierung\Analyze\Result\EntityResult; use Kartierung\Analyze\Result\RepositoryMethod; +use Kartierung\Analyze\Result\RepositoryMethodParameter; +use Kartierung\Repository\InvalidCall; use Kartierung\Sql\LazyQuery; use PDO; @@ -13,9 +17,54 @@ class SaveEntity implements ResolutionStrategy public function createQuery(RepositoryMethod $method, array $parameters): LazyQuery { return new LazyQuery( - function (PDO $pdo) { + function (PDO $pdo) use ($method, $parameters) { + /** @var RepositoryMethodParameter $firstDeclaredParam */ + $firstDeclaredParam = array_first($method->parameters); + /** @var object $firstParam */ + $firstParam = array_first($parameters); + if (!is_a($firstParam, $firstDeclaredParam->type)) { + throw new InvalidCall( + "Repository method $method->name is expected to be called with an entity " . + "of type $firstDeclaredParam->type. Something else was given." + ); + } + + /** @var object $firstParam */ + /** @var class-string $declaredType */ + $declaredType = $firstDeclaredParam->type; + + $entityResult = (new EntityAnalyzer($declaredType))->analyze(); + $entityData = (new EntityToDataMapper())->byColumnName($entityResult, $firstParam); + + if ($this->hasIdValue($entityResult, $entityData)) { + return $this->update(); + } + + return $this->insert(); } ); } + + private function update(): int + { + return 0; + } + + private function insert(): object + { + return new \stdClass(); + } + + /** + * @template E of object + * @param EntityResult $entityResult + * @param array $values + * @return bool + * @noinspection PhpPluralMixedCanBeReplacedWithArrayInspection + */ + private function hasIdValue(EntityResult $entityResult, array $values): bool + { + return ($values[$entityResult->idField->columnName] ?? null) !== null; + } } diff --git a/tests/Repository/ResolutionStrategy/EntityToDataMapperTest.php b/tests/Repository/ResolutionStrategy/EntityToDataMapperTest.php new file mode 100644 index 0000000..dae5e62 --- /dev/null +++ b/tests/Repository/ResolutionStrategy/EntityToDataMapperTest.php @@ -0,0 +1,61 @@ +byColumnName( + $entityResult, $entity + ); + + // Then + $this->assertEquals( + [ + 'intcol' => 1, + 'stringcol' => 2 + ], + $result + ); + } +} diff --git a/tests/Repository/ResolutionStrategy/FindByIdTest.php b/tests/Repository/ResolutionStrategy/FindByIdTest.php index 5033ee1..1b7c04d 100644 --- a/tests/Repository/ResolutionStrategy/FindByIdTest.php +++ b/tests/Repository/ResolutionStrategy/FindByIdTest.php @@ -31,6 +31,7 @@ class FindByIdTest extends SqliteBaseCase public function testSelectsFirstEntry(): void { // Given + $findBy = new FindById(); $method = new RepositoryMethod( query: '', returnType: SimpleEntity::class, @@ -38,7 +39,7 @@ class FindByIdTest extends SqliteBaseCase returnListType: '', name: '', parameters: [], - resolutionStrategy: new FindById() + resolutionStrategy: $findBy ); $query = $method->createQuery([1]); @@ -56,6 +57,7 @@ class FindByIdTest extends SqliteBaseCase public function testSelectsThirdEntry(): void { // Given + $findBy = new FindById(); $method = new RepositoryMethod( query: '', returnType: SimpleEntity::class, @@ -63,7 +65,7 @@ class FindByIdTest extends SqliteBaseCase returnListType: '', name: '', parameters: [], - resolutionStrategy: new FindById() + resolutionStrategy: $findBy ); $query = $method->createQuery([3]); @@ -81,6 +83,7 @@ class FindByIdTest extends SqliteBaseCase public function testSelectsNothingMatching(): void { // Given + $findBy = new FindById(); $method = new RepositoryMethod( query: '', returnType: SimpleEntity::class, @@ -88,7 +91,7 @@ class FindByIdTest extends SqliteBaseCase returnListType: '', name: '', parameters: [], - resolutionStrategy: new FindById() + resolutionStrategy: $findBy ); $query = $method->createQuery([4]); diff --git a/tests/Repository/ResolutionStrategy/SqliteBaseCase.php b/tests/Repository/ResolutionStrategy/SqliteBaseCase.php index e5f3854..b3de17e 100644 --- a/tests/Repository/ResolutionStrategy/SqliteBaseCase.php +++ b/tests/Repository/ResolutionStrategy/SqliteBaseCase.php @@ -11,13 +11,19 @@ use PHPUnit\Framework\TestCase; abstract class SqliteBaseCase extends TestCase { private PDO $pdo; + private $sqlitePath; protected function setUp(): void { parent::setUp(); - $tmpfile = tempnam(sys_get_temp_dir(), 'kartierung.sqlite'); - $this->pdo = new \PDO("sqlite:/$tmpfile"); + $this->sqlitePath = tempnam(sys_get_temp_dir(), 'kartierung.sqlite'); + $this->pdo = new \PDO("sqlite:/$this->sqlitePath"); + } + + protected function tearDown(): void + { + unlink($this->sqlitePath); } protected function getPdo(): PDO