initial commit

master
Josha von Gizycki 2 weeks ago
commit 1cec4a45f6

4
.gitignore vendored

@ -0,0 +1,4 @@
.idea
var
vendor
tests/.phpunit.cache

@ -0,0 +1,16 @@
.PHONY: cc
cc: stan test
.PHONY: stan
stan:
vendor/bin/phpstan analyse src --level=10
.PHONY: test
test:
vendor/bin/phpunit
.PHONY: classes
classes: var/classes.png
var/classes.png: $(shell find src)
vendor/bin/php-class-diagram src | plantuml -p > var/classes.png

@ -0,0 +1,22 @@
{
"name": "kartierung/kartierung",
"version": "0.1",
"require": {
"php": "^8.2",
"ext-pdo": "*"
},
"autoload": {
"psr-4": {
"Kartierung\\": [
"src/",
"tests/"
]
}
},
"require-dev": {
"phpunit/phpunit": "^12.4",
"phpstan/phpstan": "^2.1",
"smeghead/php-class-diagram": "^1.6",
"ext-pdo_sqlite": "*"
}
}

1922
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
cacheDirectory="tests/.phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="false"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
displayDetailsOnPhpunitDeprecations="true"
failOnPhpunitDeprecation="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>

@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
use Kartierung\Attribute\Column;
use Kartierung\Attribute\Id;
use Kartierung\Attribute\Table;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionProperty;
/**
* @template E of object
*/
readonly class EntityAnalyzer
{
/**
* @param class-string<E> $entity
*/
public function __construct(
private string $entity
) {}
/**
* @return EntityResult<E>
*/
public function analyze(): EntityResult
{
$refClass = new ReflectionClass($this->entity);
$fields = $this->fields($refClass);
return new EntityResult(
classFqcn: $refClass->getName(),
tableName: $this->tableName($refClass),
fields: $fields,
idField: $this->idField($fields)
);
}
/**
* @param ReflectionClass<E> $refClass
* @return string
*/
private function tableName(ReflectionClass $refClass): string
{
$attrs = $refClass->getAttributes(Table::class);
if (count($attrs) !== 1) {
//throw new InvalidEntity('Attribute #[Table] not found on entity ' . $refClass->name);
return $refClass->getShortName();
}
/** @var string */
return $attrs[0]->getArguments()[0];
}
/**
* @param ReflectionClass<E> $refClass
* @return list<EntityField<E>>
*/
private function fields(ReflectionClass $refClass): array
{
$props = $refClass->getProperties();
return array_map(fn(ReflectionProperty $prop) => $this->fieldFromProperty($prop), $props);
}
/**
* @param list<EntityField<E>> $fields
* @return EntityField<E>
*/
private function idField(array $fields): EntityField
{
$idFields = array_filter($fields, static fn(EntityField $f) => $f->isIdField);
$nrIdFields = count($idFields);
if ($nrIdFields === 0 || $nrIdFields > 1) {
throw new InvalidEntity("Invalid number of id fields found: $nrIdFields, expected: 1");
}
return $idFields[array_key_first($idFields)];
}
/**
* @param ReflectionProperty $prop
* @return EntityField<E>
*/
private function fieldFromProperty(ReflectionProperty $prop): EntityField
{
$attrs = $prop->getAttributes(Column::class);
if (count($attrs) === 1) {
/** @var string $name */
$name = $attrs[0]->getArguments()[0];
} else {
$name = $prop->getName();
}
$refClass = $prop->getDeclaringClass();
$writeAccess = $this->writeAccess($refClass, $prop);
$readAccess = $this->readAccess($refClass, $prop);
return new EntityField(
name: $prop->name,
fqcn: $this->entity,
columnName: $name,
isIdField: count($prop->getAttributes(Id::class)) > 0,
writeAccess: $writeAccess,
readAccess: $readAccess
);
}
/**
* @param ReflectionClass<E> $refClass
* @param ReflectionProperty $prop
* @return FieldAccess
*/
public function writeAccess(ReflectionClass $refClass, ReflectionProperty $prop): FieldAccess
{
$wither = FieldAccess::witherName($prop->getName());
$setter = FieldAccess::setterName($prop->getName());
if ($refClass->hasMethod($setter)) {
$writeAccess = FieldAccess::GETSET;
} elseif ($refClass->hasMethod($wither)) {
$returnType = $refClass->getMethod($wither)->getReturnType();
if (!$returnType instanceof ReflectionNamedType || $returnType->getName() !== 'self') {
throw new InvalidEntity(
"Field $refClass->name::$prop->name defines wither writing method with invalid return type, " .
"expecting `self`."
);
}
$writeAccess = FieldAccess::WITHER;
} elseif ($prop->isPublic() && !$prop->isReadOnly()) {
$writeAccess = FieldAccess::PUBLIC;
} else {
throw new InvalidEntity(
"Field $refClass->name::$prop->name has no valid writing method. "
. "Implement $setter, $wither or make the field public."
);
}
return $writeAccess;
}
/**
* @param ReflectionClass<E> $refClass
* @param ReflectionProperty $prop
* @return FieldAccess
*/
public function readAccess(ReflectionClass $refClass, ReflectionProperty $prop): FieldAccess
{
$getter = 'get' . ucfirst($prop->getName());
if ($refClass->hasMethod($getter)) {
$readAccess = FieldAccess::GETSET;
} elseif ($prop->isPublic()) {
$readAccess = FieldAccess::PUBLIC;
} else {
throw new InvalidEntity(
"Field $refClass->name::$prop->name has no valid reading method. "
. "Implement $getter or make the field public."
);
}
return $readAccess;
}
}

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
/**
* @template P of object
*/
readonly class EntityField
{
/**
* @param class-string<P> $fqcn
*/
public function __construct(
public string $name,
public string $fqcn,
public string $columnName,
public bool $isIdField,
public FieldAccess $writeAccess,
public FieldAccess $readAccess,
) {}
}

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
/**
* @template E of object
*/
readonly class EntityResult
{
/**
* @param class-string<E> $classFqcn
* @param list<EntityField<object>> $fields
* @param EntityField<E> $idField
*/
public function __construct(
public string $classFqcn,
public string $tableName,
public array $fields,
public EntityField $idField
) {}
}

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
enum FieldAccess
{
case GETSET;
case PUBLIC;
case WITHER;
public static function setterName(string $fieldName): string
{
return 'set' . ucfirst($fieldName);
}
public static function witherName(string $fieldName): string
{
return 'with' . ucfirst($fieldName);
}
}

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
use RuntimeException;
use Throwable;
class InvalidEntity extends RuntimeException
{
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
use RuntimeException;
use Throwable;
class InvalidRepository extends RuntimeException
{
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
enum ParameterFunction
{
case STATEMENT_PARAMETER;
}

@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
use Kartierung\Analyze\Validate\ResolutionStrategyFactory;
use Kartierung\Attribute\ListResultOf;
use Kartierung\Attribute\Query;
use ReflectionClass;
use ReflectionIntersectionType;
use ReflectionMethod;
use ReflectionUnionType;
/**
* @template R of object
*/
readonly class RepositoryAnalyzer
{
public function __construct(
private ResolutionStrategyFactory $methodValidator
) {}
/**
* @param class-string<R> $repositoryClass
* @noinspection PhpDocMissingThrowsInspection
*/
public function analyze(
string $repositoryClass
): RepositoryResult
{
/** @noinspection PhpUnhandledExceptionInspection */
$refClass = new ReflectionClass($repositoryClass);
return new RepositoryResult(
methods: $this->methods($refClass)
);
}
/**
* @param ReflectionClass<R> $refClass
* @return array<string, RepositoryMethod>
*/
private function methods(ReflectionClass $refClass): array
{
$refMethods = $refClass->getMethods();
$methods = [];
foreach ($refMethods as $refMethod) {
$query = $this->queryString($refMethod);
$returnType = $this->returnType($refMethod);
$listType = $this->listType($refMethod);
$params = $this->parameters($refMethod);
$name = $refMethod->name;
$resolutionStrategy = $this->methodValidator->forRepositoryMethod(
$refClass->name,
$name,
$query,
$returnType,
$listType,
$params
);
$methods[$refMethod->name] = new RepositoryMethod(
query: $query,
returnType: $returnType,
returnListType: $listType,
name: $name,
parameters: $params,
resolutionStrategy: $resolutionStrategy
);
}
return $methods;
}
/**
* @param ReflectionMethod $refMethod
* @return string|null
*/
private function queryString(ReflectionMethod $refMethod): ?string
{
$queryAttr = $refMethod->getAttributes(Query::class);
$query = null;
if (count($queryAttr) === 1) {
/** @var Query $queryInst */
$queryInst = $queryAttr[0]->newInstance();
$query = $queryInst->query;
}
return $query;
}
/**
* @param ReflectionMethod $refMethod
* @return class-string<object>|string
*/
private function returnType(ReflectionMethod $refMethod): string
{
$returnType = $refMethod->getReturnType();
if ($returnType instanceof ReflectionUnionType
|| $returnType instanceof ReflectionIntersectionType
|| $returnType === null) {
throw new InvalidRepository(
"Return type of $refMethod->class::$refMethod->name has an invalid return type. "
. 'Simple type expected. Unions, Intersections or no type are not simple.'
);
}
/** @var class-string<object>|string */
return $returnType->__toString();
}
/**
* @param ReflectionMethod $refMethod
* @return class-string|null
*/
private function listType(ReflectionMethod $refMethod): ?string
{
$listResultOfAttr = $refMethod->getAttributes(ListResultOf::class);
$listType = null;
if (count($listResultOfAttr) === 1) {
/** @var ListResultOf $inst */
$inst = $listResultOfAttr[0]->newInstance();
$listType = $inst->entitiyFqcn;
if (!class_exists($listType)) {
throw new InvalidRepository(
"ListResultOf parameter of $refMethod->class::$refMethod->name does not map to a class. "
. 'Fqcn of entity class expected.'
);
}
}
return $listType;
}
/**
* @param ReflectionMethod $refMethod
* @return list<RepositoryMethodParameter>
*/
private function parameters(ReflectionMethod $refMethod): array
{
$params = [];
foreach ($refMethod->getParameters() as $param) {
$type = $param->getType();
if ($type instanceof ReflectionUnionType
|| $type instanceof ReflectionIntersectionType
|| $type === null) {
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.'
);
}
$params[] = new RepositoryMethodParameter(
name: $param->getName(),
type: $type->__toString(),
function: ParameterFunction::STATEMENT_PARAMETER
);
}
return $params;
}
}

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
use Kartierung\Repository\ResolutionStrategy\ResolutionStrategy;
readonly class RepositoryMethod
{
/**
* @param class-string|string $returnType
* @param list<RepositoryMethodParameter> $parameters
*/
public function __construct(
public ?string $query,
public string $returnType,
public ?string $returnListType,
public string $name,
public array $parameters,
public ResolutionStrategy $resolutionStrategy
) {}
public function returnsNullable(): bool
{
return str_starts_with($this->returnType, '?');
}
}

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
readonly class RepositoryMethodParameter
{
public function __construct(
public string $name,
public string $type,
public ParameterFunction $function
) {}
}

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
readonly class RepositoryResult
{
/**
* @param array<string, RepositoryMethod> $methods
*/
public function __construct(
public array $methods
) {}
}

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze\Validate;
use Kartierung\Analyze\InvalidRepository;
use Kartierung\Repository\ResolutionStrategy\DeleteEntity;
use Kartierung\Repository\ResolutionStrategy\FindAll;
use Kartierung\Repository\ResolutionStrategy\FindById;
use Kartierung\Repository\ResolutionStrategy\ResolutionStrategy;
use Kartierung\Repository\ResolutionStrategy\SaveEntity;
use Kartierung\Repository\ResolutionStrategy\UserDefinedQuery;
class ResolutionStrategyFactory
{
/**
* @param list<mixed> $params
* @param class-string|null $listType
*/
public function forRepositoryMethod(
string $repName,
string $methodName,
?string $query,
string $returnType,
?string $listType,
array $params
): ResolutionStrategy
{
$oneParam = count($params) === 1;
$returnsNullable = str_starts_with($returnType, '?');
return match (true) {
$query !== null => new UserDefinedQuery(),
$methodName === 'save' && $oneParam => new SaveEntity(),
$methodName === 'save' => throw new InvalidRepository(
"'save' functions expect exactly one parameter. Invalid function found in $repName"
),
$methodName === 'delete' && $oneParam && $returnType === 'int' => new DeleteEntity(),
$methodName === 'delete' => throw new InvalidRepository(
"'delete' functions expect exactly one parameter and integer return type. " .
"Invalid function found in $repName"
),
$methodName === 'findById' && $oneParam && $returnsNullable => new FindById(),
$methodName === 'findById' => throw new InvalidRepository(
"fnyById functions expect exactly one parameter and nullable return type." .
"Invalid function found in $repName"
),
$methodName === 'findAll' && $returnType === 'array' && $listType !== null => new FindAll(),
$methodName === 'findAll' => throw new InvalidRepository(
"FindAll"
),
default => throw new InvalidRepository(
"No suitable strategy for repository function $repName::$methodName can be found."
)
};
}
}

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Kartierung\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
readonly class Column
{
public function __construct(
public string $name = ''
) {}
}

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Kartierung\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
class Id
{
}

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Kartierung\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
readonly class ListResultOf
{
/**
* @param class-string $entitiyFqcn
*/
public function __construct(
public string $entitiyFqcn
) {}
}

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Kartierung\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
readonly class Query
{
public function __construct(
public string $query
) {}
}

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Kartierung\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
readonly class Table
{
public function __construct(
public string $name = ''
) {}
}

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository;
use Kartierung\Analyze\InvalidRepository;
use Kartierung\Attribute\Query;
use ReflectionClass;
class Repository
{
/**
* @template R of object
* @param class-string<R> $clazz
* @return R
* @noinspection PhpDocMissingThrowsInspection
*/
public function ofType(string $clazz): object
{
/** @noinspection PhpUnhandledExceptionInspection */
$refClass = new ReflectionClass($clazz);
/**
* @var R of object
* @phpstan-ignore varTag.nativeType
*/
return new class($refClass) {
/**
* @param ReflectionClass<R> $refClass
*/
public function __construct(
private readonly ReflectionClass $refClass
) {}
/**
* @param string $name
* @param list<mixed> $arguments
* @return mixed
*/
public function __call(string $name, array $arguments): mixed
{
$method = $this->refClass->getMethod($name);
$queryAttr = $method->getAttributes(Query::class);
if (count($queryAttr) !== 1) {
throw new InvalidRepository(
"Called method $name has an invalid number of Query attributes. Expected: 1"
);
}
return '';
}
};
}
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\RepositoryMethod;
use Kartierung\Sql\LazyQuery;
use PDO;
class DeleteEntity implements ResolutionStrategy
{
public function execute(RepositoryMethod $method, array $parameters): LazyQuery
{
return new LazyQuery(
function (PDO $pdo) {
}
);
}
}

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\EntityAnalyzer;
use Kartierung\Analyze\EntityField;
use Kartierung\Analyze\RepositoryMethod;
use Kartierung\Sql\LazyQuery;
use Kartierung\Sql\DataToEntityMapper;
use PDO;
use PDOStatement;
readonly class FindAll implements ResolutionStrategy
{
public function execute(RepositoryMethod $method, array $parameters): LazyQuery
{
return new LazyQuery(function (PDO $pdo) use ($method): array {
/** @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<string, mixed> $row */
$entity = $mapper->toEntity($row);
$statementResult[] = $entity;
}
return $statementResult;
});
}
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\RepositoryMethod;
use Kartierung\Sql\LazyQuery;
use PDO;
class FindById implements ResolutionStrategy
{
public function execute(RepositoryMethod $method, array $parameters): LazyQuery
{
return new LazyQuery(
function (PDO $pdo) {
}
);
}
}

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\RepositoryMethod;
use Kartierung\Sql\LazyQuery;
interface ResolutionStrategy
{
/**
* @param RepositoryMethod $method
* @param array<string, mixed> $parameters
* @return LazyQuery
*/
public function execute(RepositoryMethod $method, array $parameters): LazyQuery;
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\RepositoryMethod;
use Kartierung\Sql\LazyQuery;
use PDO;
class SaveEntity implements ResolutionStrategy
{
public function execute(RepositoryMethod $method, array $parameters): LazyQuery
{
return new LazyQuery(
function (PDO $pdo) {
}
);
}
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\RepositoryMethod;
use Kartierung\Sql\LazyQuery;
use PDO;
class UserDefinedQuery implements ResolutionStrategy
{
public function execute(RepositoryMethod $method, array $parameters): LazyQuery
{
return new LazyQuery(
function (PDO $pdo) {
}
);
}
}

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Kartierung\Sql;
use Kartierung\Analyze\EntityField;
use Kartierung\Analyze\EntityResult;
use Kartierung\Analyze\FieldAccess;
/**
* @template E of object
*/
readonly class DataToEntityMapper
{
/**
* @param EntityResult<E> $entityResult
*/
public function __construct(
private EntityResult $entityResult
) {}
/**
* @param array<string, mixed> $row
* @return E
*/
public function toEntity(array $row): object
{
/** @var E $entity */
$entity = new $this->entityResult->classFqcn();
foreach ($this->entityResult->fields as $field) {
/** @var object $dbValue */
$dbValue = $row[$field->columnName];
match ($field->writeAccess) {
FieldAccess::GETSET => $this->getsetWrite($entity, $field, $dbValue),
FieldAccess::PUBLIC => $this->publicWrite($entity, $field, $dbValue),
FieldAccess::WITHER => $entity = $this->witherWrite($entity, $field, $dbValue),
};
}
return $entity;
}
/**
* @param E $entity
* @param EntityField<object> $field
* @param object $value
*/
private function getsetWrite(object $entity, EntityField $field, mixed $value): void
{
$setter = FieldAccess::setterName($field->name);
$entity->$setter($value);
}
/**
* @param E $entity
* @param EntityField<object> $field
* @param object $value
*/
private function publicWrite(object $entity, EntityField $field, mixed $value): void
{
$entity->{$field->name} = $value;
}
/**
* @param E $entity
* @param EntityField<object> $field
* @param object $value
* @return E
*/
private function witherWrite(object $entity, EntityField $field, mixed $value): object
{
$wither = FieldAccess::witherName($field->name);
/** @var E */
return $entity->$wither($value);
}
}

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Kartierung\Sql;
use Closure;
use PDO;
readonly class LazyQuery
{
/**
* @param (Closure(PDO): mixed) $execute
*/
public function __construct(
public Closure $execute
) {}
}

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Kartierung\Analyze;
use Kartierung\EntityWithoutId;
use Kartierung\EntityWithTwoIds;
use Kartierung\FullEntity;
use PHPUnit\Framework\TestCase;
class EntityAnalyzerTest extends TestCase
{
public function testTableNameIsAnalyzed(): void
{
// Given
$entity = FullEntity::class;
// When
$result = (new EntityAnalyzer(FullEntity::class))->analyze();
// Then
$this->assertEquals('simple-table', $result->tableName);
$this->assertEquals(FullEntity::class, $result->classFqcn);
}
public function testFieldsAreAnalyzed(): void
{
// Given
$entity = FullEntity::class;
// When
$result = (new EntityAnalyzer($entity))->analyze();
// Then
$this->assertContainsEquals(
new EntityField(
name: 'stringField',
fqcn: $entity,
columnName: 'stringField',
isIdField: false,
writeAccess: FieldAccess::WITHER,
readAccess: FieldAccess::PUBLIC
),
$result->fields
);
$this->assertContainsEquals(
new EntityField(
name: 'intField',
fqcn: $entity,
columnName: 'int-field',
isIdField: false,
writeAccess: FieldAccess::PUBLIC,
readAccess: FieldAccess::GETSET
),
$result->fields
);
$this->assertContainsEquals(
new EntityField(
name: 'idField',
fqcn: $entity,
columnName: 'renamed-id-field',
isIdField: true,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::PUBLIC
),
$result->fields
);
}
public function testIdFieldIsFound(): void
{
// Given
$entity = FullEntity::class;
// When
$result = (new EntityAnalyzer($entity))->analyze();
// Then
$this->assertEquals(
new EntityField(
name: 'idField',
fqcn: $entity,
columnName: 'renamed-id-field',
isIdField: true,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::PUBLIC
),
$result->idField
);
}
public function testNoIdIsInvalid(): void
{
// Given
$entity = EntityWithoutId::class;
// Then
$this->expectException(InvalidEntity::class);
// When
(new EntityAnalyzer($entity))->analyze();
}
public function testMultipleIdsIsInvalid(): void
{
// Given
$entity = EntityWithTwoIds::class;
// Then
$this->expectException(InvalidEntity::class);
// When
(new EntityAnalyzer($entity))->analyze();
}
}

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Analyze;
use Kartierung\Analyze\InvalidRepository;
use Kartierung\Analyze\RepositoryAnalyzer;
use Kartierung\Analyze\RepositoryMethodParameter;
use Kartierung\Analyze\Validate\ResolutionStrategyFactory;
use Kartierung\FullEntity;
use Kartierung\InvalidListResultOfRepository;
use Kartierung\Repository;
use PHPUnit\Framework\TestCase;
class RepositoryAnalyzerTest extends TestCase
{
public function testQueryAttributeIsRead(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// When
$result = $analyzer->analyze(Repository::class);
// Then
$this->assertEquals('SELECT something', $result->methods['fetchWithQuery']->query);
}
public function testEntityReturnTypeIsAnalyzed(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// When
$result = $analyzer->analyze(Repository::class);
// Then
$this->assertEquals(FullEntity::class, $result->methods['fetchWithQuery']->returnType);
}
public function testArrayReturnTypeIsRead(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// When
$result = $analyzer->analyze(Repository::class);
// Then
$this->assertEquals('array', $result->methods['fetchArray']->returnType);
}
public function testVoidReturnTypeIsRead(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// When
$result = $analyzer->analyze(Repository::class);
// Then
$this->assertEquals('void', $result->methods['insert']->returnType);
}
public function testListResultOfIsRead(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// When
$result = $analyzer->analyze(Repository::class);
// Then
$this->assertEquals(FullEntity::class, $result->methods['fetchArray']->returnListType);
}
public function testInvalidListResultOfIsReported(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// Then
$this->expectException(InvalidRepository::class);
// When
$analyzer->analyze(InvalidListResultOfRepository::class);
}
public function testUnionReturnTypeIsReported(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// Then
$this->expectException(InvalidRepository::class);
// When
$analyzer->analyze(InvalidRepository::class);
}
public function testParametersAreAnalyzed(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// When
$result = $analyzer->analyze(Repository::class);
// Then
$method = $result->methods['doSomethingWithParameters'];
/** @var array<RepositoryMethodParameter> $parameters */
$parameters = $method->parameters;
$this->assertCount(2, $parameters);
$this->assertEquals('stringParam', $parameters[0]->name);
$this->assertEquals('string', $parameters[0]->type);
$this->assertEquals('intParam', $parameters[1]->name);
$this->assertEquals('int', $parameters[1]->type);
}
public function testNullableReturnType(): void
{
// Given
$analyzer = new RepositoryAnalyzer(new ResolutionStrategyFactory());
// When
$result = $analyzer->analyze(Repository::class);
// Then
$method = $result->methods['findById'];
$this->assertEquals('?Kartierung\FullEntity', $method->returnType);
$this->assertTrue($method->returnsNullable());
}
}

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Analyze\Validate;
use Kartierung\Analyze\Validate\ResolutionStrategyFactory;
use Kartierung\FullEntity;
use Kartierung\Repository\ResolutionStrategy\DeleteEntity;
use Kartierung\Repository\ResolutionStrategy\FindAll;
use Kartierung\Repository\ResolutionStrategy\FindById;
use Kartierung\Repository\ResolutionStrategy\SaveEntity;
use Kartierung\Repository\ResolutionStrategy\UserDefinedQuery;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class RepositoryValidatorTest extends TestCase
{
public static function validFunctionsProvider(): array
{
return [
['R', 'save', null, '', null, [1], SaveEntity::class],
['R', 'delete', null, 'int', null, [1], DeleteEntity::class],
['R', 'findById', null, '?int', null, [1], FindById::class],
['R', 'findAll', null, 'array', FullEntity::class, [], FindAll::class],
['R', 'something', 'SELECT', 'int', null, [], UserDefinedQuery::class]
];
}
#[DataProvider('validFunctionsProvider')]
public function testValidFunctions(
string $repName,
string $name,
?string $query,
string $returnType,
?string $listType,
array $params,
string $strategyClass
): void
{
// When
$function = (new ResolutionStrategyFactory())->forRepositoryMethod(
$repName,
$name,
$query,
$returnType,
$listType,
$params
);
// Then
$this->assertInstanceOf($strategyClass, $function);
}
}

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Kartierung;
use Kartierung\Attribute\Column;
use Kartierung\Attribute\Id;
use Kartierung\Attribute\Table;
class EntityWithTwoIds
{
#[Id]
public string $idOne;
#[Id]
public string $idTwo;
}

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Kartierung;
use Kartierung\Attribute\Column;
use Kartierung\Attribute\Id;
use Kartierung\Attribute\Table;
class EntityWithoutId
{
public string $stringField;
}

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Kartierung;
use Kartierung\Attribute\Column;
use Kartierung\Attribute\Id;
use Kartierung\Attribute\Table;
#[Table("simple-table")]
class FullEntity
{
public string $stringField;
#[Column('int-field')]
public int $intField;
#[Id]
#[Column('renamed-id-field')]
public int $idField;
public function setIdField(int $idField): void
{
$this->idField = $idField;
}
public function getIntField(): int
{
return $this->intField;
}
public function withStringField(string $stringField): self
{
$this->stringField = $stringField;
return $this;
}
}

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Kartierung;
use Kartierung\Attribute\ListResultOf;
interface InvalidListResultOfRepository
{
#[ListResultOf('void')]
public function invalidListResultOf(): array;
}

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Kartierung;
use Kartierung\Attribute\ListResultOf;
use Kartierung\Attribute\Query;
interface Repository
{
#[Query("SELECT something")]
public function fetchWithQuery(): FullEntity;
#[Query("SELECT something")]
#[ListResultOf(FullEntity::class)]
public function fetchArray(): array;
#[Query("SELECT something")]
public function insert(): void;
#[Query("SELECT something")]
public function doSomethingWithParameters(
string $stringParam,
int $intParam
): void;
public function findById(int $id): ?FullEntity;
}

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Analyze\RepositoryMethod;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
class FindAllTest extends TestCase
{
private \PDO $pdo;
protected function setUp(): void
{
$tmpfile = tempnam(sys_get_temp_dir(), 'kartierung.sqlite');
$this->pdo = new \PDO("sqlite:/$tmpfile");
$this->pdo->exec(
"
CREATE TABLE SimpleEntity (
stringcol VARCHAR(250),
intcol INT
);
INSERT INTO SimpleEntity(stringcol, intcol)
VALUES ('dings', 1),
('bumms', 2);
"
);
}
#[Group('integration')]
public function testSelectsData(): void
{
// Given
$query = (new FindAll())->execute(
method: new RepositoryMethod(
query: '',
returnType: '',
returnListType: SimpleEntity::class,
name: '',
parameters: [],
resolutionStrategy: new FindAll()
),
parameters: []
);
// When
$result = $query->execute->__invoke($this->pdo);
// Then
$this->assertNotEmpty($result);
}
}

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Kartierung\Repository\ResolutionStrategy;
use Kartierung\Attribute\Id;
class SimpleEntity
{
public string $stringcol;
#[Id]
public int $intcol;
public function withStringCol(string $value): self
{
$new = new self();
$new->stringcol = $value;
return $new;
}
}

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Kartierung\Sql;
use Kartierung\Analyze\EntityField;
use Kartierung\Analyze\EntityResult;
use Kartierung\Analyze\FieldAccess;
use PHPUnit\Framework\TestCase;
class DateToEntityMapperTest extends TestCase
{
public function testMapsSetter(): void
{
// Given
$mapper = new DataToEntityMapper(
entityResult: new EntityResult(
classFqcn: EntityWithThreeAccessMethods::class,
tableName: '',
fields: [
new EntityField(
name: 'witherVal',
fqcn: 'int',
columnName: 'witherVal',
isIdField: false,
writeAccess: FieldAccess::WITHER,
readAccess: FieldAccess::WITHER
),
new EntityField(
name: 'publicVal',
fqcn: 'int',
columnName: 'publicVal',
isIdField: false,
writeAccess: FieldAccess::PUBLIC,
readAccess: FieldAccess::PUBLIC
),
new EntityField(
name: 'setterVal',
fqcn: 'int',
columnName: 'setterVal',
isIdField: false,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::GETSET
)
],
idField: new EntityField(
name: '',
fqcn: '',
columnName: '',
isIdField: false,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::GETSET
)
)
);
$row = [
'witherVal' => 1,
'publicVal' => 2,
'setterVal' => 3
];
// When
$entity = $mapper->toEntity($row);
// Then
$this->assertInstanceOf(EntityWithThreeAccessMethods::class, $entity);
$this->assertEquals(1, $entity->getWitherVal());
$this->assertEquals(2, $entity->publicVal);
$this->assertEquals(3, $entity->getSetterVal());
}
}

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Kartierung\Sql;
class EntityWithThreeAccessMethods
{
public int $publicVal;
private int $setterVal;
private int $witherVal;
public function getSetterVal(): int
{
return $this->setterVal;
}
public function setSetterVal(int $value): void
{
$this->setterVal = $value;
}
public function withWitherVal(int $value): self
{
$this->witherVal = $value;
return $this;
}
public function getWitherVal(): int
{
return $this->witherVal;
}
}

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Kartierung;
interface UnionReturnTypeRepository
{
public function returnsUnion(): int|string;
}
Loading…
Cancel
Save