move query execution logic to a reusable place, implement insertion of entities

master
Josha von Gizycki 2 days ago
parent c0dd69eac0
commit 21a93966d7

@ -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 private function fieldFromParameter(ReflectionParameter $param): EntityField
@ -186,17 +196,10 @@ readonly class EntityAnalyzer
$hasPublicProp = $refClass->hasProperty($param->getName()) $hasPublicProp = $refClass->hasProperty($param->getName())
&& $refClass->getProperty($param->getName())->isPublic(); && $refClass->getProperty($param->getName())->isPublic();
if ($refClass->hasMethod($getter)) { return match (true) {
$readAccess = FieldAccess::GETTER; $refClass->hasMethod($getter) => FieldAccess::GETTER,
} elseif ($hasPublicProp) { $hasPublicProp => FieldAccess::PUBLIC,
$readAccess = FieldAccess::PUBLIC; default => FieldAccess::NONE
} 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;
} }
} }

@ -8,4 +8,5 @@ enum FieldAccess
{ {
case GETTER; case GETTER;
case PUBLIC; case PUBLIC;
case NONE;
} }

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy\Execution;
use Kartierung\Analyze\Result\EntityField;
use Kartierung\Analyze\Result\EntityResult;
use Kartierung\Repository\InvalidCall;
use PDO;
readonly class InsertEntity
{
public function __construct(
private PDO $pdo
) {}
/**
* @param EntityResult<object> $entityResult
* @param array<string, mixed> $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();
}
}

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy\Execution;
use Kartierung\Analyze\Result\EntityField;
use Kartierung\Analyze\Result\EntityResult;
use Kartierung\Sql\DataToEntityMapper;
use PDO;
use PDOStatement;
readonly class SelectAllEntities
{
public function __construct(
private PDO $pdo
) {}
/**
* @template E of object
* @param EntityResult<E> $entityResult
* @return list<E>
*/
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<string, mixed> $row */
$entity = $mapper->toEntity($row);
$statementResult[] = $entity;
}
return $statementResult;
}
}

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy\Execution;
use Kartierung\Analyze\Result\EntityField;
use Kartierung\Analyze\Result\EntityResult;
use Kartierung\Sql\DataToEntityMapper;
use PDO;
use PDOStatement;
readonly class SelectEntityById
{
public function __construct(
private PDO $pdo
) {}
/**
* @template E of object
* @param EntityResult<E> $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<string, mixed>|false $row */
$row = $stmt->fetch(mode: PDO::FETCH_ASSOC);
if ($row) {
$mapper = new DataToEntityMapper($entityResult);
return $mapper->toEntity($row);
}
return null;
}
}

@ -5,12 +5,10 @@ declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy; namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\EntityAnalyzer; use Kartierung\Analyze\EntityAnalyzer;
use Kartierung\Analyze\Result\EntityField;
use Kartierung\Analyze\Result\RepositoryMethod; use Kartierung\Analyze\Result\RepositoryMethod;
use Kartierung\Sql\DataToEntityMapper; use Kartierung\Repository\ResolutionStrategy\Execution\SelectAllEntities;
use Kartierung\Sql\LazyQuery; use Kartierung\Sql\LazyQuery;
use PDO; use PDO;
use PDOStatement;
readonly class FindAll implements ResolutionStrategy readonly class FindAll implements ResolutionStrategy
{ {
@ -20,34 +18,8 @@ readonly class FindAll implements ResolutionStrategy
/** @var class-string $entityFqcn */ /** @var class-string $entityFqcn */
$entityFqcn = $method->returnListType; $entityFqcn = $method->returnListType;
$entityResult = (new EntityAnalyzer($entityFqcn))->analyze(); $entityResult = (new EntityAnalyzer($entityFqcn))->analyze();
$mapper = new DataToEntityMapper($entityResult);
$fields = implode( return (new SelectAllEntities($pdo))->selectAll($entityResult);
', ',
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<string, mixed> $row */
$entity = $mapper->toEntity($row);
$statementResult[] = $entity;
}
return $statementResult;
}); });
} }
} }

@ -7,6 +7,8 @@ namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\EntityAnalyzer; use Kartierung\Analyze\EntityAnalyzer;
use Kartierung\Analyze\Result\EntityField; use Kartierung\Analyze\Result\EntityField;
use Kartierung\Analyze\Result\RepositoryMethod; use Kartierung\Analyze\Result\RepositoryMethod;
use Kartierung\Repository\InvalidCall;
use Kartierung\Repository\ResolutionStrategy\Execution\SelectEntityById;
use Kartierung\Sql\DataToEntityMapper; use Kartierung\Sql\DataToEntityMapper;
use Kartierung\Sql\LazyQuery; use Kartierung\Sql\LazyQuery;
use PDO; use PDO;
@ -18,37 +20,18 @@ readonly class FindById implements ResolutionStrategy
{ {
return new LazyQuery( return new LazyQuery(
static function (PDO $pdo) use ($method, $parameters) { 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<object> $type */ /** @var class-string<object> $type */
$type = $method->returnType; $type = $method->returnType;
$entityResult = (new EntityAnalyzer($type))->analyze(); $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<string, mixed>|false $row */
$row = $stmt->fetch(mode: PDO::FETCH_ASSOC);
if ($row) {
return $mapper->toEntity($row);
}
return null; return (new SelectEntityById($pdo))->select($entityResult, $id);
} }
); );
} }

@ -9,6 +9,7 @@ use Kartierung\Analyze\Result\EntityResult;
use Kartierung\Analyze\Result\RepositoryMethod; use Kartierung\Analyze\Result\RepositoryMethod;
use Kartierung\Analyze\Result\RepositoryMethodParameter; use Kartierung\Analyze\Result\RepositoryMethodParameter;
use Kartierung\Repository\InvalidCall; use Kartierung\Repository\InvalidCall;
use Kartierung\Repository\ResolutionStrategy\Execution\InsertEntity;
use Kartierung\Sql\LazyQuery; use Kartierung\Sql\LazyQuery;
use PDO; use PDO;
@ -17,7 +18,7 @@ class SaveEntity implements ResolutionStrategy
public function createQuery(RepositoryMethod $method, array $parameters): LazyQuery public function createQuery(RepositoryMethod $method, array $parameters): LazyQuery
{ {
return new LazyQuery( return new LazyQuery(
function (PDO $pdo) use ($method, $parameters) { function (PDO $pdo) use ($method, $parameters): object|int {
/** @var RepositoryMethodParameter $firstDeclaredParam */ /** @var RepositoryMethodParameter $firstDeclaredParam */
$firstDeclaredParam = array_first($method->parameters); $firstDeclaredParam = array_first($method->parameters);
/** @var object $firstParam */ /** @var object $firstParam */
@ -41,7 +42,7 @@ class SaveEntity implements ResolutionStrategy
return $this->update(); return $this->update();
} }
return $this->insert(); return $this->insert($pdo, $entityResult, $entityData);
} }
); );
} }
@ -51,9 +52,15 @@ class SaveEntity implements ResolutionStrategy
return 0; return 0;
} }
private function insert(): object /**
* @param PDO $pdo
* @param EntityResult<object> $entityResult
* @param array<string, mixed> $data
* @return object
*/
private function insert(PDO $pdo, EntityResult $entityResult, array $data): object
{ {
return new \stdClass(); return (new InsertEntity($pdo))->insert($entityResult, $data);
} }
/** /**

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\Result\ParameterFunction;
use Kartierung\Analyze\Result\RepositoryMethod;
use Kartierung\Analyze\Result\RepositoryMethodParameter;
use PHPUnit\Framework\Attributes\Group;
class SaveEntityTest extends SqliteBaseCase
{
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}

@ -10,7 +10,7 @@ class SimpleEntity
{ {
public function __construct( public function __construct(
#[Id] #[Id]
public int $intcol, public ?int $intcol,
public string $stringcol public string $stringcol
) {} ) {}
} }

Loading…
Cancel
Save