From b10dfdf4116db55ca4714717bec0eabefd6981a0 Mon Sep 17 00:00:00 2001 From: Josha von Gizycki Date: Fri, 9 Oct 2026 12:55:38 +0200 Subject: [PATCH] more precise type analysis: add distinction between primitive and class types --- deptrac.php | 33 +++++++------ src/Analyze/EntityAnalyzer.php | 47 ++++++++++++++++--- src/Analyze/Result/ClassType.php | 15 ++++++ src/Analyze/Result/EntityField.php | 8 +--- src/Analyze/Result/EntityResult.php | 5 +- src/Analyze/Result/PrimitiveType.php | 13 +++++ tests/Analyze/EntityAnalyzerTest.php | 7 +-- tests/Analyze/EntityWithoutTableAttribute.php | 2 +- tests/Sql/DataToEntityMapperTest.php | 7 +-- 9 files changed, 100 insertions(+), 37 deletions(-) create mode 100644 src/Analyze/Result/ClassType.php create mode 100644 src/Analyze/Result/PrimitiveType.php diff --git a/deptrac.php b/deptrac.php index 14daa2b..35bb0e2 100644 --- a/deptrac.php +++ b/deptrac.php @@ -14,21 +14,24 @@ return static function (DeptracConfig $config): void { ->paths('./src') ->excludeFiles('#.*test.*#') ->layers( - $analyzePublic = Layer::withName('AnalyzePublic')->collectors( - ClassLikeConfig::create('Kartierung\\Analyze\\Exception\\.+'), - ClassLikeConfig::create('Kartierung\\Analyze\\Result\\.+') - ), - $analyze = Layer::withName('Analyze')->collectors( - BoolConfig::create( - must: [ClassLikeConfig::create('Kartierung\\Analyze\\.+')], - mustNot: [ - LayerConfig::create($analyzePublic) - ] - ) - ), - $attribute = Layer::withName('Attribute')->collectors( - ClassLikeConfig::create('Kartierung\\Attribute\\.*'), - ), + $analyzePublic = Layer::withName('AnalyzePublic') + ->collectors( + ClassLikeConfig::create('Kartierung\\Analyze\\Exception\\.+'), + ClassLikeConfig::create('Kartierung\\Analyze\\Result\\.+') + ), + $analyze = Layer::withName('Analyze') + ->collectors( + BoolConfig::create( + must: [ClassLikeConfig::create('Kartierung\\Analyze\\.+')], + mustNot: [ + LayerConfig::create($analyzePublic) + ] + ) + ), + $attribute = Layer::withName('Attribute') + ->collectors( + ClassLikeConfig::create('Kartierung\\Attribute\\.*'), + ), ) ->rulesets( Ruleset::forLayer($analyze) diff --git a/src/Analyze/EntityAnalyzer.php b/src/Analyze/EntityAnalyzer.php index 8857bd0..b834381 100644 --- a/src/Analyze/EntityAnalyzer.php +++ b/src/Analyze/EntityAnalyzer.php @@ -5,14 +5,20 @@ declare(strict_types=1); namespace Kartierung\Analyze; use Kartierung\Analyze\Exception\InvalidEntity; +use Kartierung\Analyze\Exception\InvalidRepository; +use Kartierung\Analyze\Result\ClassType; use Kartierung\Analyze\Result\EntityField; use Kartierung\Analyze\Result\EntityResult; use Kartierung\Analyze\Result\FieldAccess; +use Kartierung\Analyze\Result\PrimitiveType; use Kartierung\Attribute\Column; use Kartierung\Attribute\Id; use Kartierung\Attribute\Table; use ReflectionClass; +use ReflectionIntersectionType; +use ReflectionNamedType; use ReflectionParameter; +use ReflectionUnionType; /** * @template E of object @@ -61,7 +67,7 @@ readonly class EntityAnalyzer /** * @param ReflectionClass $refClass - * @return list> + * @return list */ private function fields(ReflectionClass $refClass): array { @@ -79,8 +85,8 @@ readonly class EntityAnalyzer } /** - * @param list> $fields - * @return EntityField + * @param list $fields + * @return EntityField */ private function idField(array $fields): EntityField { @@ -97,7 +103,7 @@ readonly class EntityAnalyzer } /** - * @return EntityField + * @return EntityField */ private function fieldFromParameter(ReflectionParameter $param): EntityField { @@ -117,18 +123,47 @@ readonly class EntityAnalyzer return new EntityField( name: $param->name, - fqcn: $this->entity, + type: $this->parameterType($param), columnName: $name, isIdField: count($param->getAttributes(Id::class)) > 0, readAccess: $readAccess ); } + private function parameterType(ReflectionParameter $param): PrimitiveType|ClassType + { + 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 InvalidRepository( + "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 $classType */ + $classType = $type->getName(); + + return match ($type->getName()) { + "string" => PrimitiveType::STRING, + "bool" => PrimitiveType::BOOLEAN, + "int" => PrimitiveType::INTEGER, + "float" => PrimitiveType::FLOAT, + default => new ClassType($classType) + }; + } + /** * @param ReflectionClass $refClass * @return FieldAccess */ - public function readAccess(ReflectionClass $refClass, ReflectionParameter $param): FieldAccess + private function readAccess(ReflectionClass $refClass, ReflectionParameter $param): FieldAccess { $getter = 'get' . ucfirst($param->getName()); diff --git a/src/Analyze/Result/ClassType.php b/src/Analyze/Result/ClassType.php new file mode 100644 index 0000000..90f6a5a --- /dev/null +++ b/src/Analyze/Result/ClassType.php @@ -0,0 +1,15 @@ + $fqcn + */ + public function __construct( + public string $fqcn + ) {} +} diff --git a/src/Analyze/Result/EntityField.php b/src/Analyze/Result/EntityField.php index 3ef4af9..cfb6f0d 100644 --- a/src/Analyze/Result/EntityField.php +++ b/src/Analyze/Result/EntityField.php @@ -4,17 +4,11 @@ declare(strict_types=1); namespace Kartierung\Analyze\Result; -/** - * @template P of object - */ readonly class EntityField { - /** - * @param class-string

$fqcn - */ public function __construct( public string $name, - public string $fqcn, + public PrimitiveType|ClassType $type, public string $columnName, public bool $isIdField, public FieldAccess $readAccess, diff --git a/src/Analyze/Result/EntityResult.php b/src/Analyze/Result/EntityResult.php index df87ed0..4f59c36 100644 --- a/src/Analyze/Result/EntityResult.php +++ b/src/Analyze/Result/EntityResult.php @@ -11,8 +11,9 @@ readonly class EntityResult { /** * @param class-string $classFqcn - * @param EntityField $fields - * @param EntityField $idField + * @param string $tableName + * @param list $fields + * @param EntityField $idField */ public function __construct( public string $classFqcn, diff --git a/src/Analyze/Result/PrimitiveType.php b/src/Analyze/Result/PrimitiveType.php new file mode 100644 index 0000000..c815a70 --- /dev/null +++ b/src/Analyze/Result/PrimitiveType.php @@ -0,0 +1,13 @@ +assertContainsEquals( new EntityField( name: 'intField', - fqcn: $entity, + type: PrimitiveType::INTEGER, columnName: 'int-field', isIdField: false, readAccess: FieldAccess::GETTER @@ -48,7 +49,7 @@ class EntityAnalyzerTest extends TestCase $this->assertContainsEquals( new EntityField( name: 'idField', - fqcn: $entity, + type: PrimitiveType::INTEGER, columnName: 'renamed-id-field', isIdField: true, readAccess: FieldAccess::PUBLIC @@ -69,7 +70,7 @@ class EntityAnalyzerTest extends TestCase $this->assertEquals( new EntityField( name: 'idField', - fqcn: $entity, + type: PrimitiveType::INTEGER, columnName: 'renamed-id-field', isIdField: true, readAccess: FieldAccess::PUBLIC diff --git a/tests/Analyze/EntityWithoutTableAttribute.php b/tests/Analyze/EntityWithoutTableAttribute.php index 00dcad9..2ed690f 100644 --- a/tests/Analyze/EntityWithoutTableAttribute.php +++ b/tests/Analyze/EntityWithoutTableAttribute.php @@ -10,6 +10,6 @@ class EntityWithoutTableAttribute { public function __construct( #[Id] - public $id + public int $id ) {} } diff --git a/tests/Sql/DataToEntityMapperTest.php b/tests/Sql/DataToEntityMapperTest.php index ca42db0..95d19fc 100644 --- a/tests/Sql/DataToEntityMapperTest.php +++ b/tests/Sql/DataToEntityMapperTest.php @@ -7,6 +7,7 @@ namespace Kartierung\Sql; use Kartierung\Analyze\Result\EntityField; use Kartierung\Analyze\Result\EntityResult; use Kartierung\Analyze\Result\FieldAccess; +use Kartierung\Analyze\Result\PrimitiveType; use PHPUnit\Framework\TestCase; class DataToEntityMapperTest extends TestCase @@ -21,14 +22,14 @@ class DataToEntityMapperTest extends TestCase fields: [ new EntityField( name: 'publicVal', - fqcn: 'int', + type: PrimitiveType::INTEGER, columnName: 'public_val', isIdField: false, readAccess: FieldAccess::PUBLIC ), new EntityField( name: 'setterVal', - fqcn: 'int', + type: PrimitiveType::INTEGER, columnName: 'setter_val', isIdField: false, readAccess: FieldAccess::GETTER @@ -36,7 +37,7 @@ class DataToEntityMapperTest extends TestCase ], idField: new EntityField( name: '', - fqcn: '', + type: PrimitiveType::INTEGER, columnName: '', isIdField: false, readAccess: FieldAccess::GETTER