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

<?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;
}
}