You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
6.0 KiB
PHP
201 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Kartierung\Analyze;
|
|
|
|
use Kartierung\Analyze\Exception\InvalidEntity;
|
|
use Kartierung\Analyze\Result\EntityField;
|
|
use Kartierung\Analyze\Result\EntityResult;
|
|
use Kartierung\Analyze\Result\FieldAccess;
|
|
use Kartierung\Analyze\Result\Type\ArrayType;
|
|
use Kartierung\Analyze\Result\Type\ClassType;
|
|
use Kartierung\Analyze\Result\Type\PrimitiveType;
|
|
use Kartierung\Attribute\Column;
|
|
use Kartierung\Attribute\Id;
|
|
use Kartierung\Attribute\ListOf;
|
|
use Kartierung\Attribute\Table;
|
|
use ReflectionClass;
|
|
use ReflectionNamedType;
|
|
use ReflectionParameter;
|
|
|
|
/**
|
|
* @template E of object
|
|
*/
|
|
readonly class EntityAnalyzer
|
|
{
|
|
/**
|
|
* @param class-string<E> $entity
|
|
*/
|
|
public function __construct(
|
|
private string $entity
|
|
) {}
|
|
|
|
/**
|
|
* @return EntityResult<E>
|
|
* @noinspection PhpDocMissingThrowsInspection
|
|
*/
|
|
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) {
|
|
return $refClass->getShortName();
|
|
}
|
|
|
|
/** @var string */
|
|
return $attrs[0]->getArguments()[0];
|
|
}
|
|
|
|
/**
|
|
* @param ReflectionClass<E> $refClass
|
|
* @return list<EntityField>
|
|
*/
|
|
private function fields(ReflectionClass $refClass): array
|
|
{
|
|
$ctor = $refClass->getConstructor();
|
|
if (!$ctor) {
|
|
throw new InvalidEntity(
|
|
"Entity $this->entity does not declare a constructor. Implement one accepting all of the "
|
|
. "entitie's values."
|
|
);
|
|
}
|
|
|
|
$params = $ctor->getParameters();
|
|
|
|
return array_map(fn(ReflectionParameter $param) => $this->fieldFromParameter($param), $params);
|
|
}
|
|
|
|
/**
|
|
* @param list<EntityField> $fields
|
|
* @return EntityField
|
|
*/
|
|
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 on entity $this->entity found: $nrIdFields, expected: 1"
|
|
);
|
|
}
|
|
|
|
return $idFields[array_key_first($idFields)];
|
|
}
|
|
|
|
private function fieldFromParameter(ReflectionParameter $param): EntityField
|
|
{
|
|
$attrs = $param->getAttributes(Column::class);
|
|
|
|
if (count($attrs) === 1) {
|
|
/** @var Column $column */
|
|
$column = $attrs[0]->newInstance();
|
|
$name = $column->name ?: $param->getName();
|
|
} else {
|
|
$name = $param->getName();
|
|
}
|
|
|
|
/** @var ReflectionClass<E> $refClass */
|
|
$refClass = $param->getDeclaringClass();
|
|
$readAccess = $this->readAccess($refClass, $param);
|
|
|
|
return new EntityField(
|
|
name: $param->name,
|
|
type: $this->parameterType($param),
|
|
columnName: $name,
|
|
isIdField: count($param->getAttributes(Id::class)) > 0,
|
|
readAccess: $readAccess
|
|
);
|
|
}
|
|
|
|
private function parameterType(ReflectionParameter $param): PrimitiveType|ClassType|ArrayType
|
|
{
|
|
if (!$param->hasType()) {
|
|
throw new InvalidEntity(
|
|
"Entity parameter $this->entity::$param->name has no type declaration. Add one."
|
|
);
|
|
}
|
|
|
|
$type = $param->getType();
|
|
if (!$type instanceof ReflectionNamedType) {
|
|
throw new InvalidEntity(
|
|
"Entity parameter $this->entity::$param->name has an invalid return type. "
|
|
. "Simple type expected. Unions, Intersections or no type are not simple."
|
|
);
|
|
}
|
|
|
|
// workaround for phpstan
|
|
/** @var class-string<object> $classType */
|
|
$classType = $type->getName();
|
|
|
|
if ($classType === "array") {
|
|
$listAttr = $param->getAttributes(ListOf::class);
|
|
if (empty($listAttr) || count($listAttr) > 1) {
|
|
throw new InvalidEntity(
|
|
"Entity parameter $this->entity::$param->name declares its typr as array. "
|
|
. "Exactly one #[ListOf(...)] attribute expected."
|
|
);
|
|
}
|
|
|
|
/** @var ListOf $listAttrInstance */
|
|
$listAttrInstance = $listAttr[0]->newInstance();
|
|
|
|
return new ArrayType(
|
|
(new EntityAnalyzer($listAttrInstance->entitiyFqcn))->analyze()
|
|
);
|
|
}
|
|
|
|
return match ($type->getName()) {
|
|
"string" => PrimitiveType::STRING,
|
|
"bool" => PrimitiveType::BOOLEAN,
|
|
"int" => PrimitiveType::INTEGER,
|
|
"float" => PrimitiveType::FLOAT,
|
|
default => new ClassType($classType)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param ReflectionClass<E> $refClass
|
|
* @noinspection PhpDocMissingThrowsInspection
|
|
*/
|
|
private function readAccess(ReflectionClass $refClass, ReflectionParameter $param): FieldAccess
|
|
{
|
|
$getter = 'get' . ucfirst($param->getName());
|
|
|
|
$hasPublicProp = $refClass->hasProperty($param->getName())
|
|
&& $refClass->getProperty($param->getName())->isPublic();
|
|
|
|
if ($refClass->hasMethod($getter)) {
|
|
$readAccess = FieldAccess::GETTER;
|
|
} elseif ($hasPublicProp) {
|
|
$readAccess = FieldAccess::PUBLIC;
|
|
} else {
|
|
throw new InvalidEntity(
|
|
"Constructor parameter of entity $this->entity '$param->name' has no valid reading method. "
|
|
. "Implement $getter() or make the field public."
|
|
);
|
|
}
|
|
|
|
return $readAccess;
|
|
}
|
|
}
|