switch to constructor based value injection

master
Josha von Gizycki 3 weeks ago
parent 58974e6a8e
commit c439307d81

@ -9,6 +9,7 @@ use Kartierung\Attribute\Id;
use Kartierung\Attribute\Table; use Kartierung\Attribute\Table;
use ReflectionClass; use ReflectionClass;
use ReflectionNamedType; use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty; use ReflectionProperty;
/** /**
@ -49,7 +50,6 @@ readonly class EntityAnalyzer
$attrs = $refClass->getAttributes(Table::class); $attrs = $refClass->getAttributes(Table::class);
if (count($attrs) !== 1) { if (count($attrs) !== 1) {
//throw new InvalidEntity('Attribute #[Table] not found on entity ' . $refClass->name);
return $refClass->getShortName(); return $refClass->getShortName();
} }
@ -63,9 +63,17 @@ readonly class EntityAnalyzer
*/ */
private function fields(ReflectionClass $refClass): array private function fields(ReflectionClass $refClass): array
{ {
$props = $refClass->getProperties(); $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."
);
}
return array_map(fn(ReflectionProperty $prop) => $this->fieldFromProperty($prop), $props); $params = $ctor->getParameters();
return array_map(fn(ReflectionParameter $param) => $this->fieldFromParameter($param), $params);
} }
/** /**
@ -77,93 +85,61 @@ readonly class EntityAnalyzer
$idFields = array_filter($fields, static fn(EntityField $f) => $f->isIdField); $idFields = array_filter($fields, static fn(EntityField $f) => $f->isIdField);
$nrIdFields = count($idFields); $nrIdFields = count($idFields);
if ($nrIdFields === 0 || $nrIdFields > 1) { if ($nrIdFields !== 1) {
throw new InvalidEntity("Invalid number of id fields found: $nrIdFields, expected: 1"); throw new InvalidEntity(
"Invalid number of id fields on entity $this->entity found: $nrIdFields, expected: 1"
);
} }
return $idFields[array_key_first($idFields)]; return $idFields[array_key_first($idFields)];
} }
/** /**
* @param ReflectionProperty $prop
* @return EntityField<E> * @return EntityField<E>
*/ */
private function fieldFromProperty(ReflectionProperty $prop): EntityField private function fieldFromParameter(ReflectionParameter $param): EntityField
{ {
$attrs = $prop->getAttributes(Column::class); $attrs = $param->getAttributes(Column::class);
if (count($attrs) === 1) { if (count($attrs) === 1) {
/** @var string $name */ /** @var Column $column */
$name = $attrs[0]->getArguments()[0]; $column = $attrs[0]->newInstance();
$name = $column->name ?: $param->getName();
} else { } else {
$name = $prop->getName(); $name = $param->getName();
} }
$refClass = $prop->getDeclaringClass(); $refClass = $param->getDeclaringClass();
$writeAccess = $this->writeAccess($refClass, $prop); $readAccess = $this->readAccess($refClass, $param);
$readAccess = $this->readAccess($refClass, $prop);
return new EntityField( return new EntityField(
name: $prop->name, name: $param->name,
fqcn: $this->entity, fqcn: $this->entity,
columnName: $name, columnName: $name,
isIdField: count($prop->getAttributes(Id::class)) > 0, isIdField: count($param->getAttributes(Id::class)) > 0,
writeAccess: $writeAccess,
readAccess: $readAccess readAccess: $readAccess
); );
} }
/** /**
* @param ReflectionClass<E> $refClass * @param ReflectionClass<E> $refClass
* @param ReflectionProperty $prop
* @return FieldAccess * @return FieldAccess
*/ */
public function writeAccess(ReflectionClass $refClass, ReflectionProperty $prop): FieldAccess public function readAccess(ReflectionClass $refClass, ReflectionParameter $param): FieldAccess
{ {
$wither = FieldAccess::witherName($prop->getName()); $getter = 'get' . ucfirst($param->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;
}
/** $hasPublicProp = $refClass->hasProperty($param->getName())
* @param ReflectionClass<E> $refClass && $refClass->getProperty($param->getName())->isPublic();
* @param ReflectionProperty $prop
* @return FieldAccess
*/
public function readAccess(ReflectionClass $refClass, ReflectionProperty $prop): FieldAccess
{
$getter = 'get' . ucfirst($prop->getName());
if ($refClass->hasMethod($getter)) { if ($refClass->hasMethod($getter)) {
$readAccess = FieldAccess::GETSET; $readAccess = FieldAccess::GETTER;
} elseif ($prop->isPublic()) { } elseif ($hasPublicProp) {
$readAccess = FieldAccess::PUBLIC; $readAccess = FieldAccess::PUBLIC;
} else { } else {
throw new InvalidEntity( throw new InvalidEntity(
"Field $refClass->name::$prop->name has no valid reading method. " "Constructor parameter of entity $this->entity '$param->name' has no valid reading method. "
. "Implement $getter or make the field public." . "Implement $getter() or make the field public."
); );
} }

@ -17,7 +17,6 @@ readonly class EntityField
public string $fqcn, public string $fqcn,
public string $columnName, public string $columnName,
public bool $isIdField, public bool $isIdField,
public FieldAccess $writeAccess,
public FieldAccess $readAccess, public FieldAccess $readAccess,
) {} ) {}
} }

@ -6,17 +6,6 @@ namespace Kartierung\Analyze;
enum FieldAccess enum FieldAccess
{ {
case GETSET; case GETTER;
case PUBLIC; 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);
}
} }

@ -6,7 +6,7 @@ namespace Kartierung\Attribute;
use Attribute; use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)] #[Attribute(Attribute::TARGET_PARAMETER)]
readonly class Column readonly class Column
{ {
public function __construct( public function __construct(

@ -26,21 +26,15 @@ readonly class DataToEntityMapper
*/ */
public function toEntity(array $row): object public function toEntity(array $row): object
{ {
/** @var E $entity */ $params = [];
$entity = new $this->entityResult->classFqcn();
foreach ($this->entityResult->fields as $field) { foreach ($this->entityResult->fields as $field) {
/** @var object $dbValue */ /** @var object $dbValue */
$dbValue = $row[$field->columnName]; $dbValue = $row[$field->columnName];
$params[] = $dbValue;
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; /** @var E */
return new $this->entityResult->classFqcn(...$params);
} }
/** /**

@ -5,8 +5,10 @@ declare(strict_types=1);
namespace Kartierung\Analyze; namespace Kartierung\Analyze;
use Kartierung\EntityWithoutId; use Kartierung\EntityWithoutId;
use Kartierung\EntityWithoutTableAttribute;
use Kartierung\EntityWithTwoIds; use Kartierung\EntityWithTwoIds;
use Kartierung\FullEntity; use Kartierung\FullEntity;
use Kartierung\PropertyPropagatedEntity;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class EntityAnalyzerTest extends TestCase class EntityAnalyzerTest extends TestCase
@ -33,25 +35,13 @@ class EntityAnalyzerTest extends TestCase
$result = (new EntityAnalyzer($entity))->analyze(); $result = (new EntityAnalyzer($entity))->analyze();
// Then // Then
$this->assertContainsEquals(
new EntityField(
name: 'stringField',
fqcn: $entity,
columnName: 'stringField',
isIdField: false,
writeAccess: FieldAccess::WITHER,
readAccess: FieldAccess::PUBLIC
),
$result->fields
);
$this->assertContainsEquals( $this->assertContainsEquals(
new EntityField( new EntityField(
name: 'intField', name: 'intField',
fqcn: $entity, fqcn: $entity,
columnName: 'int-field', columnName: 'int-field',
isIdField: false, isIdField: false,
writeAccess: FieldAccess::PUBLIC, readAccess: FieldAccess::GETTER
readAccess: FieldAccess::GETSET
), ),
$result->fields $result->fields
); );
@ -61,7 +51,6 @@ class EntityAnalyzerTest extends TestCase
fqcn: $entity, fqcn: $entity,
columnName: 'renamed-id-field', columnName: 'renamed-id-field',
isIdField: true, isIdField: true,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::PUBLIC readAccess: FieldAccess::PUBLIC
), ),
$result->fields $result->fields
@ -83,7 +72,6 @@ class EntityAnalyzerTest extends TestCase
fqcn: $entity, fqcn: $entity,
columnName: 'renamed-id-field', columnName: 'renamed-id-field',
isIdField: true, isIdField: true,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::PUBLIC readAccess: FieldAccess::PUBLIC
), ),
$result->idField $result->idField
@ -113,4 +101,16 @@ class EntityAnalyzerTest extends TestCase
// When // When
(new EntityAnalyzer($entity))->analyze(); (new EntityAnalyzer($entity))->analyze();
} }
public function testTableAttributeIsOptional(): void
{
// Given
$entity = EntityWithoutTableAttribute::class;
// When
$result = (new EntityAnalyzer($entity))->analyze();
// Then
$this->assertEquals("EntityWithoutTableAttribute", $result->tableName);
}
} }

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Kartierung;
use Kartierung\Attribute\Id;
class EntityWithoutTableAttribute
{
public function __construct(
#[Id]
public $id
) {}
}

@ -11,12 +11,13 @@ use Kartierung\Attribute\Table;
#[Table("simple-table")] #[Table("simple-table")]
class FullEntity class FullEntity
{ {
public string $stringField; public function __construct(
#[Column('int-field')] #[Column('int-field')]
public int $intField; public int $intField,
#[Id] #[Id]
#[Column('renamed-id-field')] #[Column('renamed-id-field')]
public int $idField; public int $idField
) {}
public function setIdField(int $idField): void public function setIdField(int $idField): void
{ {
@ -27,10 +28,4 @@ class FullEntity
{ {
return $this->intField; return $this->intField;
} }
public function withStringField(string $stringField): self
{
$this->stringField = $stringField;
return $this;
}
} }

@ -8,10 +8,11 @@ use Kartierung\Attribute\Id;
class SimpleEntity class SimpleEntity
{ {
public string $stringcol; public function __construct(
#[Id]
#[Id] public int $intcol,
public int $intcol; public string $stringcol
) {}
public function withStringCol(string $value): self public function withStringCol(string $value): self
{ {

@ -16,23 +16,14 @@ class DateToEntityMapperTest extends TestCase
// Given // Given
$mapper = new DataToEntityMapper( $mapper = new DataToEntityMapper(
entityResult: new EntityResult( entityResult: new EntityResult(
classFqcn: EntityWithThreeAccessMethods::class, classFqcn: EntityWithAllReadMechanics::class,
tableName: '', tableName: '',
fields: [ fields: [
new EntityField(
name: 'witherVal',
fqcn: 'int',
columnName: 'wither_val',
isIdField: false,
writeAccess: FieldAccess::WITHER,
readAccess: FieldAccess::WITHER
),
new EntityField( new EntityField(
name: 'publicVal', name: 'publicVal',
fqcn: 'int', fqcn: 'int',
columnName: 'public_val', columnName: 'public_val',
isIdField: false, isIdField: false,
writeAccess: FieldAccess::PUBLIC,
readAccess: FieldAccess::PUBLIC readAccess: FieldAccess::PUBLIC
), ),
new EntityField( new EntityField(
@ -40,8 +31,7 @@ class DateToEntityMapperTest extends TestCase
fqcn: 'int', fqcn: 'int',
columnName: 'setter_val', columnName: 'setter_val',
isIdField: false, isIdField: false,
writeAccess: FieldAccess::GETSET, readAccess: FieldAccess::GETTER
readAccess: FieldAccess::GETSET
) )
], ],
idField: new EntityField( idField: new EntityField(
@ -49,14 +39,12 @@ class DateToEntityMapperTest extends TestCase
fqcn: '', fqcn: '',
columnName: '', columnName: '',
isIdField: false, isIdField: false,
writeAccess: FieldAccess::GETSET, readAccess: FieldAccess::GETTER
readAccess: FieldAccess::GETSET
) )
) )
); );
$row = [ $row = [
'wither_val' => 1,
'public_val' => 2, 'public_val' => 2,
'setter_val' => 3 'setter_val' => 3
]; ];
@ -65,8 +53,7 @@ class DateToEntityMapperTest extends TestCase
$entity = $mapper->toEntity($row); $entity = $mapper->toEntity($row);
// Then // Then
$this->assertInstanceOf(EntityWithThreeAccessMethods::class, $entity); $this->assertInstanceOf(EntityWithAllReadMechanics::class, $entity);
$this->assertEquals(1, $entity->getWitherVal());
$this->assertEquals(2, $entity->publicVal); $this->assertEquals(2, $entity->publicVal);
$this->assertEquals(3, $entity->getSetterVal()); $this->assertEquals(3, $entity->getSetterVal());
} }

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Kartierung\Sql;
class EntityWithAllReadMechanics
{
public function __construct(
public int $publicVal,
private int $setterVal
) {}
public function getSetterVal(): int
{
return $this->setterVal;
}
}

@ -1,35 +0,0 @@
<?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;
}
}
Loading…
Cancel
Save