initial commit
commit
1cec4a45f6
@ -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": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
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…
Reference in New Issue