diff --git a/src/Annotation/Column.php b/src/Annotation/Column.php index 7e534c20..348c6383 100644 --- a/src/Annotation/Column.php +++ b/src/Annotation/Column.php @@ -21,6 +21,7 @@ final class Column { private bool $hasDefault = false; + private bool $hasNullable = false; /** * @param non-empty-string $type Column type. {@see \Cycle\Database\Schema\AbstractColumn::$mapping} @@ -41,11 +42,11 @@ public function __construct( 'string', 'text', 'tinyText', 'longText', 'double', 'float', 'decimal', 'datetime', 'date', 'time', 'timestamp', 'binary', 'tinyBinary', 'longBinary', 'json', ])] - private string $type, + private ?string $type = null, private ?string $name = null, private ?string $property = null, private bool $primary = false, - private bool $nullable = false, + private ?bool $nullable = null, private mixed $default = null, private mixed $typecast = null, private bool $castDefault = false, @@ -53,6 +54,10 @@ public function __construct( if ($default !== null) { $this->hasDefault = true; } + + if ($this->nullable !== null) { + $this->hasNullable = true; + } } public function getType(): ?string @@ -72,7 +77,7 @@ public function getProperty(): ?string public function isNullable(): bool { - return $this->nullable; + return $this->nullable === true; } public function isPrimary(): bool @@ -85,6 +90,11 @@ public function hasDefault(): bool return $this->hasDefault; } + public function hasNullable(): bool + { + return $this->hasNullable; + } + public function getDefault(): mixed { return $this->default; diff --git a/src/Configurator.php b/src/Configurator.php index c90fc76e..cb4a7a10 100644 --- a/src/Configurator.php +++ b/src/Configurator.php @@ -22,6 +22,7 @@ use Doctrine\Inflector\Rules\English\InflectorFactory; use Exception; use Spiral\Attributes\ReaderInterface; +use function is_subclass_of; final class Configurator { @@ -101,7 +102,7 @@ public function initFields(EntitySchema $entity, \ReflectionClass $class, string continue; } - $field = $this->initField($property->getName(), $column, $class, $columnPrefix); + $field = $this->initField($property, $column, $class, $columnPrefix); $field->setEntityClass($property->getDeclaringClass()->getName()); $entity->getFields()->set($property->getName(), $field); } @@ -208,35 +209,64 @@ public function initColumns(EntitySchema $entity, array $columns, \ReflectionCla ); } - if ($column->getType() === null) { - throw new AnnotationException( - "Column type definition is required on `{$entity->getClass()}`.`{$columnName}`" - ); - } - $field = $this->initField($columnName, $column, $class, ''); $field->setEntityClass($entity->getClass()); $entity->getFields()->set($propertyName, $field); } } - public function initField(string $name, Column $column, \ReflectionClass $class, string $columnPrefix): Field + public function initField(string|\ReflectionProperty $nameOrProperty, Column $column, \ReflectionClass $class, string $columnPrefix): Field { + $type = $column->getType(); + $isNullable = $column->hasNullable() ? $column->isNullable() : null; + $hasDefault = $column->hasDefault(); + $default = $column->getDefault(); + + if ($nameOrProperty instanceof \ReflectionProperty) { + $name = ($property = $nameOrProperty)->getName(); + $propertyType = $property->getType(); + + if ($property->hasDefaultValue() && !$hasDefault) { + $hasDefault = true; + $default = $property->getDefaultValue(); + } + + if ($propertyType instanceof \ReflectionType) { + $isNullable ??= $propertyType->allowsNull(); + + if ($propertyType instanceof \ReflectionNamedType) { + if ($propertyType->isBuiltin()) { + $type ??= $propertyType->getName(); + } elseif (is_subclass_of($propertyType->getName(), \DateTimeInterface::class)) { + $type = 'datetime'; + } + } + } + } else { + $name = $nameOrProperty; + } + + if ($type === null) { + throw new AnnotationException( + "Column type definition is required on `{$class->getName()}`.`{$name}`" + ); + } + $field = new Field(); - $field->setType($column->getType()); + $field->setType($type); $field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name))); $field->setPrimary($column->isPrimary()); $field->setTypecast($this->resolveTypecast($column->getTypecast(), $class)); - if ($column->isNullable()) { + if ($isNullable) { $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_NULLABLE, true); $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, null); } - if ($column->hasDefault()) { - $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $column->getDefault()); + if ($hasDefault) { + $field->getOptions()->set(\Cycle\Schema\Table\Column::OPT_DEFAULT, $default); } if ($column->castDefault()) { diff --git a/tests/Annotated/Fixtures/Fixtures1/SomeEntity.php b/tests/Annotated/Fixtures/Fixtures1/SomeEntity.php new file mode 100644 index 00000000..3c51a580 --- /dev/null +++ b/tests/Annotated/Fixtures/Fixtures1/SomeEntity.php @@ -0,0 +1,33 @@ +assertFalse($r->getEntity('eComplete')->getFields()->has('ignored')); } + + /** + * @dataProvider allReadersProvider + */ + public function testSimpleReferredSchema(ReaderInterface $reader): void + { + $r = new Registry($this->dbal); + (new Entities($this->locator, $reader))->run($r); + + $fields = $r->getEntity(SomeEntity::class)->getFields(); + + $this->assertSame('int', $fields->get('idificator')->getType()); + $this->assertFalse($fields->get('idificator')->getOptions()->has(Column::OPT_NULLABLE)); + $this->assertFalse($fields->get('idificator')->getOptions()->has(Column::OPT_DEFAULT)); + + $this->assertSame('string', $fields->get('nullableString')->getType()); + $this->assertSame(true, $fields->get('nullableString')->getOptions()->get(Column::OPT_NULLABLE)); + $this->assertSame(null, $fields->get('nullableString')->getOptions()->get(Column::OPT_DEFAULT)); + + $this->assertSame('string', $fields->get('nullableStringWithDefault')->getType()); + $this->assertSame(true, $fields->get('nullableStringWithDefault')->getOptions()->get(Column::OPT_NULLABLE)); + $this->assertSame('123', $fields->get('nullableStringWithDefault')->getOptions()->get(Column::OPT_DEFAULT)); + + $this->assertSame('datetime', $fields->get('dateTime')->getType()); + } + + public function testSimpleReferredSchemaWithColumnInEntity(): void + { + $reader = new AnnotationReader(); + $r = new Registry($this->dbal); + (new Entities($this->locator, $reader))->run($r); + + $fields = $r->getEntity(WithColumnInEntity::class)->getFields(); + + $this->assertFalse($fields->get('columnDeclaredInEntity')->getOptions()->has(Column::OPT_DEFAULT)); + } } diff --git a/tests/Annotated/Functional/Driver/Common/InvalidTest.php b/tests/Annotated/Functional/Driver/Common/InvalidTest.php index 8a83f476..134b5fda 100644 --- a/tests/Annotated/Functional/Driver/Common/InvalidTest.php +++ b/tests/Annotated/Functional/Driver/Common/InvalidTest.php @@ -61,7 +61,7 @@ public function testNotDefinedColumnTypeShouldThrowAnException(ReaderInterface $ { $this->expectException(AnnotationException::class); $this->expectErrorMessage( - 'Some of required arguments [`type`] is missed on `Cycle\Annotated\Tests\Fixtures\Fixtures4\User.id.`' + 'Column type definition is required on `Cycle\Annotated\Tests\Fixtures\Fixtures4\User`.`id`' ); $tokenizer = new Tokenizer(new TokenizerConfig([