diff --git a/src/Analyze/EntityAnalyzer.php b/src/Analyze/EntityAnalyzer.php index 7fe8dda..838fd99 100644 --- a/src/Analyze/EntityAnalyzer.php +++ b/src/Analyze/EntityAnalyzer.php @@ -101,7 +101,17 @@ readonly class EntityAnalyzer ); } - return $idFields[array_key_first($idFields)]; + /** @var EntityField $field */ + $field = $idFields[array_key_first($idFields)]; + + if ($field->readAccess === FieldAccess::NONE) { + throw new InvalidEntity( + "ID field $field->name of entity $this->entity has no viable read access. " + . "Implement a getter or make the property public." + ); + } + + return $field; } private function fieldFromParameter(ReflectionParameter $param): EntityField @@ -186,17 +196,10 @@ readonly class EntityAnalyzer $hasPublicProp = $refClass->hasProperty($param->getName()) && $refClass->getProperty($param->getName())->isPublic(); - if ($refClass->hasMethod($getter)) { - $readAccess = FieldAccess::GETTER; - } elseif ($hasPublicProp) { - $readAccess = FieldAccess::PUBLIC; - } else { - throw new InvalidEntity( - "Constructor parameter of entity $this->entity '$param->name' has no valid reading method. " - . "Implement $getter() or make the field public." - ); - } - - return $readAccess; + return match (true) { + $refClass->hasMethod($getter) => FieldAccess::GETTER, + $hasPublicProp => FieldAccess::PUBLIC, + default => FieldAccess::NONE + }; } } diff --git a/src/Analyze/Result/FieldAccess.php b/src/Analyze/Result/FieldAccess.php index 53719d2..dfb706f 100644 --- a/src/Analyze/Result/FieldAccess.php +++ b/src/Analyze/Result/FieldAccess.php @@ -8,4 +8,5 @@ enum FieldAccess { case GETTER; case PUBLIC; + case NONE; } diff --git a/src/Repository/ResolutionStrategy/Execution/InsertEntity.php b/src/Repository/ResolutionStrategy/Execution/InsertEntity.php new file mode 100644 index 0000000..8957c65 --- /dev/null +++ b/src/Repository/ResolutionStrategy/Execution/InsertEntity.php @@ -0,0 +1,65 @@ + $entityResult + * @param array $data + * @return object + */ + public function insert(EntityResult $entityResult, array $data): object + { + $newId = $this->insertWithNewId($entityResult, $data); + + if ($newId === false) { + throw new InvalidCall( + "Expected new ID to be generated by database. " + . "New entities must be provided with a new ID by the database schema." + ); + } + + $newEntity = (new SelectEntityById($this->pdo))->select($entityResult, $newId); + if (!$newEntity) { + throw new InvalidCall( + "No Entity with id $newId could be found in table $entityResult->tableName after insert." + ); + } + + return $newEntity; + } + + private function insertWithNewId(EntityResult $entityResult, array $data): string|false + { + $cols = implode(", ", array_map( + static fn(EntityField $field) => $field->columnName, + $entityResult->fields + )); + $vals = implode(", ", array_map( + static fn($_) => "?", + $entityResult->fields + )); + $params = array_map( + static fn(EntityField $field) => $data[$field->columnName] ?? null, + $entityResult->fields + ); + + $sql = "INSERT INTO $entityResult->tableName ($cols) VALUES ($vals)"; + $prepStmt = $this->pdo->prepare($sql); + $prepStmt->execute($params); + + return $this->pdo->lastInsertId(); + } +} diff --git a/src/Repository/ResolutionStrategy/Execution/SelectAllEntities.php b/src/Repository/ResolutionStrategy/Execution/SelectAllEntities.php new file mode 100644 index 0000000..439a787 --- /dev/null +++ b/src/Repository/ResolutionStrategy/Execution/SelectAllEntities.php @@ -0,0 +1,54 @@ + $entityResult + * @return list + */ + public function selectAll(EntityResult $entityResult): array + { + $fields = implode( + ', ', + array_map( + static fn(EntityField $e) => $e->columnName, $entityResult->fields + ) + ); + $sql = " + SELECT $fields + FROM {$entityResult->tableName} + "; + + /** @var PDOStatement $stmt */ + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + $statementResult = []; + + $mapper = new DataToEntityMapper($entityResult); + while ($row = $stmt->fetch( + mode: PDO::FETCH_ASSOC + )) { + /** @var array $row */ + $entity = $mapper->toEntity($row); + $statementResult[] = $entity; + } + + return $statementResult; + } +} diff --git a/src/Repository/ResolutionStrategy/Execution/SelectEntityById.php b/src/Repository/ResolutionStrategy/Execution/SelectEntityById.php new file mode 100644 index 0000000..baf4164 --- /dev/null +++ b/src/Repository/ResolutionStrategy/Execution/SelectEntityById.php @@ -0,0 +1,55 @@ + $entityResult + * @param mixed $id + * @return E|null + */ + public function select(EntityResult $entityResult, mixed $id): ?object + { + $fields = implode( + ', ', + array_map( + static fn(EntityField $e) => $e->columnName, $entityResult->fields + ) + ); + + $idField = $entityResult->idField->name; + + $sql = " + SELECT $fields + FROM {$entityResult->tableName} + WHERE $idField = ? + "; + + /** @var PDOStatement $stmt */ + $stmt = $this->pdo->prepare($sql); + $stmt->execute([$id]); + + /** @var array|false $row */ + $row = $stmt->fetch(mode: PDO::FETCH_ASSOC); + if ($row) { + $mapper = new DataToEntityMapper($entityResult); + return $mapper->toEntity($row); + } + + return null; + } +} diff --git a/src/Repository/ResolutionStrategy/FindAll.php b/src/Repository/ResolutionStrategy/FindAll.php index 71382c3..83d5e89 100644 --- a/src/Repository/ResolutionStrategy/FindAll.php +++ b/src/Repository/ResolutionStrategy/FindAll.php @@ -5,12 +5,10 @@ declare(strict_types=1); namespace Kartierung\Repository\ResolutionStrategy; use Kartierung\Analyze\EntityAnalyzer; -use Kartierung\Analyze\Result\EntityField; use Kartierung\Analyze\Result\RepositoryMethod; -use Kartierung\Sql\DataToEntityMapper; +use Kartierung\Repository\ResolutionStrategy\Execution\SelectAllEntities; use Kartierung\Sql\LazyQuery; use PDO; -use PDOStatement; readonly class FindAll implements ResolutionStrategy { @@ -20,34 +18,8 @@ readonly class FindAll implements ResolutionStrategy /** @var class-string $entityFqcn */ $entityFqcn = $method->returnListType; $entityResult = (new EntityAnalyzer($entityFqcn))->analyze(); - $mapper = new DataToEntityMapper($entityResult); - $fields = implode( - ', ', - array_map( - static fn(EntityField $e) => $e->columnName, $entityResult->fields - ) - ); - $sql = " - SELECT $fields - FROM {$entityResult->tableName} - "; - - /** @var PDOStatement $stmt */ - $stmt = $pdo->prepare($sql); - $stmt->execute(); - - $statementResult = []; - - while ($row = $stmt->fetch( - mode: PDO::FETCH_ASSOC - )) { - /** @var array $row */ - $entity = $mapper->toEntity($row); - $statementResult[] = $entity; - } - - return $statementResult; + return (new SelectAllEntities($pdo))->selectAll($entityResult); }); } } diff --git a/src/Repository/ResolutionStrategy/FindById.php b/src/Repository/ResolutionStrategy/FindById.php index d6ee635..e107419 100644 --- a/src/Repository/ResolutionStrategy/FindById.php +++ b/src/Repository/ResolutionStrategy/FindById.php @@ -7,6 +7,8 @@ namespace Kartierung\Repository\ResolutionStrategy; use Kartierung\Analyze\EntityAnalyzer; use Kartierung\Analyze\Result\EntityField; use Kartierung\Analyze\Result\RepositoryMethod; +use Kartierung\Repository\InvalidCall; +use Kartierung\Repository\ResolutionStrategy\Execution\SelectEntityById; use Kartierung\Sql\DataToEntityMapper; use Kartierung\Sql\LazyQuery; use PDO; @@ -18,37 +20,18 @@ readonly class FindById implements ResolutionStrategy { return new LazyQuery( static function (PDO $pdo) use ($method, $parameters) { + $id = array_first($parameters); + if($id === null || count($parameters) !== 1) { + throw new InvalidCall( + "Expected exactly on parameter as id of an entity to find. $id was given." + ); + } + /** @var class-string $type */ $type = $method->returnType; $entityResult = (new EntityAnalyzer($type))->analyze(); - $mapper = new DataToEntityMapper($entityResult); - - $fields = implode( - ', ', - array_map( - static fn(EntityField $e) => $e->columnName, $entityResult->fields - ) - ); - - $idField = $entityResult->idField->name; - - $sql = " - SELECT $fields - FROM {$entityResult->tableName} - WHERE $idField = ? - "; - - /** @var PDOStatement $stmt */ - $stmt = $pdo->prepare($sql); - $stmt->execute([array_first($parameters)]); - - /** @var array|false $row */ - $row = $stmt->fetch(mode: PDO::FETCH_ASSOC); - if ($row) { - return $mapper->toEntity($row); - } - return null; + return (new SelectEntityById($pdo))->select($entityResult, $id); } ); } diff --git a/src/Repository/ResolutionStrategy/SaveEntity.php b/src/Repository/ResolutionStrategy/SaveEntity.php index 7ad7855..fbd7993 100644 --- a/src/Repository/ResolutionStrategy/SaveEntity.php +++ b/src/Repository/ResolutionStrategy/SaveEntity.php @@ -9,6 +9,7 @@ use Kartierung\Analyze\Result\EntityResult; use Kartierung\Analyze\Result\RepositoryMethod; use Kartierung\Analyze\Result\RepositoryMethodParameter; use Kartierung\Repository\InvalidCall; +use Kartierung\Repository\ResolutionStrategy\Execution\InsertEntity; use Kartierung\Sql\LazyQuery; use PDO; @@ -17,7 +18,7 @@ class SaveEntity implements ResolutionStrategy public function createQuery(RepositoryMethod $method, array $parameters): LazyQuery { return new LazyQuery( - function (PDO $pdo) use ($method, $parameters) { + function (PDO $pdo) use ($method, $parameters): object|int { /** @var RepositoryMethodParameter $firstDeclaredParam */ $firstDeclaredParam = array_first($method->parameters); /** @var object $firstParam */ @@ -41,7 +42,7 @@ class SaveEntity implements ResolutionStrategy return $this->update(); } - return $this->insert(); + return $this->insert($pdo, $entityResult, $entityData); } ); } @@ -51,9 +52,15 @@ class SaveEntity implements ResolutionStrategy return 0; } - private function insert(): object + /** + * @param PDO $pdo + * @param EntityResult $entityResult + * @param array $data + * @return object + */ + private function insert(PDO $pdo, EntityResult $entityResult, array $data): object { - return new \stdClass(); + return (new InsertEntity($pdo))->insert($entityResult, $data); } /** diff --git a/tests/Repository/ResolutionStrategy/SaveEntityTest.php b/tests/Repository/ResolutionStrategy/SaveEntityTest.php new file mode 100644 index 0000000..d6bb06e --- /dev/null +++ b/tests/Repository/ResolutionStrategy/SaveEntityTest.php @@ -0,0 +1,62 @@ +getPdo()->exec( + " + CREATE TABLE SimpleEntity ( + stringcol VARCHAR(250), + intcol INTEGER PRIMARY KEY AUTOINCREMENT + ); + INSERT INTO SimpleEntity(stringcol, intcol) + VALUES ('dings', 1), + ('bumms', 2); + " + ); + } + + #[Group('integration')] + public function testSelectsData(): void + { + // Given + $strategy = new SaveEntity(); + $method = new RepositoryMethod( + query: '', + returnType: '', + returnsNullable: true, + returnListType: '', + name: '', + parameters: [ + new RepositoryMethodParameter( + name: '', + type: SimpleEntity::class, + function: ParameterFunction::STATEMENT_PARAMETER + ) + ], + resolutionStrategy: $strategy + ); + $entity = new SimpleEntity(null, 'foobar'); + $query = $method->createQuery([$entity]); + + // When + $result = $query->execute($this->getPdo()); + + // Then + $this->assertInstanceOf(SimpleEntity::class, $result); + $this->assertNotNull($result->intcol); + } + +} diff --git a/tests/Repository/ResolutionStrategy/SimpleEntity.php b/tests/Repository/ResolutionStrategy/SimpleEntity.php index a2e28c4..50943f6 100644 --- a/tests/Repository/ResolutionStrategy/SimpleEntity.php +++ b/tests/Repository/ResolutionStrategy/SimpleEntity.php @@ -10,7 +10,7 @@ class SimpleEntity { public function __construct( #[Id] - public int $intcol, + public ?int $intcol, public string $stringcol ) {} }