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

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

@ -6,17 +6,6 @@ namespace Kartierung\Analyze;
enum FieldAccess
{
case GETSET;
case GETTER;
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;
#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_PARAMETER)]
readonly class Column
{
public function __construct(

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

@ -5,8 +5,10 @@ declare(strict_types=1);
namespace Kartierung\Analyze;
use Kartierung\EntityWithoutId;
use Kartierung\EntityWithoutTableAttribute;
use Kartierung\EntityWithTwoIds;
use Kartierung\FullEntity;
use Kartierung\PropertyPropagatedEntity;
use PHPUnit\Framework\TestCase;
class EntityAnalyzerTest extends TestCase
@ -33,25 +35,13 @@ class EntityAnalyzerTest extends TestCase
$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
readAccess: FieldAccess::GETTER
),
$result->fields
);
@ -61,7 +51,6 @@ class EntityAnalyzerTest extends TestCase
fqcn: $entity,
columnName: 'renamed-id-field',
isIdField: true,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::PUBLIC
),
$result->fields
@ -83,7 +72,6 @@ class EntityAnalyzerTest extends TestCase
fqcn: $entity,
columnName: 'renamed-id-field',
isIdField: true,
writeAccess: FieldAccess::GETSET,
readAccess: FieldAccess::PUBLIC
),
$result->idField
@ -113,4 +101,16 @@ class EntityAnalyzerTest extends TestCase
// When
(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")]
class FullEntity
{
public string $stringField;
public function __construct(
#[Column('int-field')]
public int $intField;
public int $intField,
#[Id]
#[Column('renamed-id-field')]
public int $idField;
public int $idField
) {}
public function setIdField(int $idField): void
{
@ -27,10 +28,4 @@ class FullEntity
{
return $this->intField;
}
public function withStringField(string $stringField): self
{
$this->stringField = $stringField;
return $this;
}
}

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

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