Skip to content

Commit 1feb1d7

Browse files
committed
Add support for generated fields
1 parent 8d462c9 commit 1feb1d7

File tree

12 files changed

+313
-6
lines changed

12 files changed

+313
-6
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"prefer-stable": true,
88
"require": {
99
"php": ">=8.0",
10-
"cycle/orm": "^2.2.0",
11-
"cycle/schema-builder": "^2.6",
10+
"cycle/orm": "^2.7",
11+
"cycle/schema-builder": "^2.8",
1212
"doctrine/annotations": "^1.14.3 || ^2.0.1",
1313
"spiral/attributes": "^2.8|^3.0",
1414
"spiral/tokenizer": "^2.8|^3.0",

src/Annotation/GeneratedValue.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Annotated\Annotation;
6+
7+
use Cycle\ORM\Schema\GeneratedField;
8+
use Spiral\Attributes\NamedArgumentConstructor;
9+
10+
/**
11+
* @Annotation
12+
* @NamedArgumentConstructor
13+
* @Target({"PROPERTY"})
14+
*/
15+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
16+
#[NamedArgumentConstructor]
17+
class GeneratedValue
18+
{
19+
public function __construct(
20+
protected bool $beforeInsert = false,
21+
protected bool $onInsert = false,
22+
protected bool $beforeUpdate = false,
23+
) {
24+
}
25+
26+
public function getFlags(): ?int
27+
{
28+
if (!$this->beforeInsert && !$this->onInsert && !$this->beforeUpdate) {
29+
return null;
30+
}
31+
32+
return
33+
($this->beforeInsert ? GeneratedField::BEFORE_INSERT : 0) |
34+
($this->onInsert ? GeneratedField::ON_INSERT : 0) |
35+
($this->beforeUpdate ? GeneratedField::BEFORE_UPDATE : 0);
36+
}
37+
}

src/Configurator.php

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
use Cycle\Annotated\Annotation\Embeddable;
99
use Cycle\Annotated\Annotation\Entity;
1010
use Cycle\Annotated\Annotation\ForeignKey;
11+
use Cycle\Annotated\Annotation\GeneratedValue;
1112
use Cycle\Annotated\Annotation\Relation as RelationAnnotation;
1213
use Cycle\Annotated\Exception\AnnotationException;
1314
use Cycle\Annotated\Exception\AnnotationRequiredArgumentsException;
1415
use Cycle\Annotated\Exception\AnnotationWrongTypeArgumentException;
1516
use Cycle\Annotated\Utils\EntityUtils;
17+
use Cycle\ORM\Schema\GeneratedField;
1618
use Cycle\Schema\Definition\Entity as EntitySchema;
1719
use Cycle\Schema\Definition\ForeignKey as ForeignKeySchema;
1820
use Cycle\Schema\Definition\Field;
@@ -22,7 +24,6 @@
2224
use Doctrine\Common\Annotations\Reader as DoctrineReader;
2325
use Doctrine\Inflector\Inflector;
2426
use Doctrine\Inflector\Rules\English\InflectorFactory;
25-
use Exception;
2627
use Spiral\Attributes\ReaderInterface;
2728

2829
final class Configurator
@@ -91,7 +92,7 @@ public function initFields(EntitySchema $entity, \ReflectionClass $class, string
9192
foreach ($class->getProperties() as $property) {
9293
try {
9394
$column = $this->reader->firstPropertyMetadata($property, Column::class);
94-
} catch (Exception $e) {
95+
} catch (\Exception $e) {
9596
throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
9697
} catch (\ArgumentCountError $e) {
9798
throw AnnotationRequiredArgumentsException::createFor($property, Column::class, $e);
@@ -221,6 +222,9 @@ public function initField(string $name, Column $column, \ReflectionClass $class,
221222
$field->setType($column->getType());
222223
$field->setColumn($columnPrefix . ($column->getColumn() ?? $this->inflector->tableize($name)));
223224
$field->setPrimary($column->isPrimary());
225+
if ($this->isOnInsertGeneratedField($field)) {
226+
$field->setGenerated(GeneratedField::ON_INSERT);
227+
}
224228

225229
$field->setTypecast($this->resolveTypecast($column->getTypecast(), $class));
226230

@@ -286,6 +290,20 @@ public function initForeignKeys(Entity $ann, EntitySchema $entity, \ReflectionCl
286290
}
287291
}
288292

293+
public function initGeneratedFields(EntitySchema $entity, \ReflectionClass $class): void
294+
{
295+
foreach ($class->getProperties() as $property) {
296+
try {
297+
$generated = $this->reader->firstPropertyMetadata($property, GeneratedValue::class);
298+
if ($generated !== null) {
299+
$entity->getFields()->get($property->getName())->setGenerated($generated->getFlags());
300+
}
301+
} catch (\Throwable $e) {
302+
throw new AnnotationException($e->getMessage(), (int) $e->getCode(), $e);
303+
}
304+
}
305+
}
306+
289307
/**
290308
* Resolve class or role name relative to the current class.
291309
*/
@@ -346,7 +364,7 @@ private function getClassMetadata(\ReflectionClass $class, string $name): iterab
346364
{
347365
try {
348366
return $this->reader->getClassMetadata($class, $name);
349-
} catch (Exception $e) {
367+
} catch (\Exception $e) {
350368
throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
351369
}
352370
}
@@ -364,8 +382,16 @@ private function getPropertyMetadata(\ReflectionProperty $property, string $name
364382
{
365383
try {
366384
return $this->reader->getPropertyMetadata($property, $name);
367-
} catch (Exception $e) {
385+
} catch (\Exception $e) {
368386
throw new AnnotationException($e->getMessage(), $e->getCode(), $e);
369387
}
370388
}
389+
390+
private function isOnInsertGeneratedField(Field $field): bool
391+
{
392+
return match ($field->getType()) {
393+
'serial', 'bigserial', 'smallserial' => true,
394+
default => $field->isPrimary()
395+
};
396+
}
371397
}

src/Entities.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ public function run(Registry $registry): Registry
8484
continue;
8585
}
8686

87+
// generated fields
88+
$this->generator->initGeneratedFields($e, $class);
89+
8790
// register entity (OR find parent)
8891
$registry->register($e);
8992
$registry->linkTable($e, $e->getDatabase(), $e->getTableName());
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Annotated\Tests\Fixtures\Fixtures25\PostgreSQL;
6+
7+
use Cycle\Annotated\Annotation\Column;
8+
use Cycle\Annotated\Annotation\Entity;
9+
10+
/**
11+
* @Entity(role="withGeneratedSerial", table="with_generated_serial")
12+
*/
13+
#[Entity(role: 'withGeneratedSerial', table: 'with_generated_serial')]
14+
class WithGeneratedSerial
15+
{
16+
/**
17+
* @Column(type="primary")
18+
*/
19+
#[Column(type: 'primary')]
20+
public int $id;
21+
22+
/**
23+
* @Column(type="smallserial", name="small_serial")
24+
*/
25+
#[Column(type: 'smallserial', name: 'small_serial')]
26+
public int $smallSerial;
27+
28+
/**
29+
* @Column(type="serial")
30+
*/
31+
#[Column(type: 'serial')]
32+
public int $serial;
33+
34+
/**
35+
* @Column(type="bigserial", name="big_serial")
36+
*/
37+
#[Column(type: 'bigserial', name: 'big_serial')]
38+
public int $bigSerial;
39+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Annotated\Tests\Fixtures\Fixtures25;
6+
7+
use Cycle\Annotated\Annotation\Column;
8+
use Cycle\Annotated\Annotation\Entity;
9+
use Cycle\Annotated\Annotation\GeneratedValue;
10+
11+
/**
12+
* @Entity(role="withGeneratedFields", table="with_generated_fields")
13+
*/
14+
#[Entity(role: 'withGeneratedFields', table: 'with_generated_fields')]
15+
class WithGeneratedFields
16+
{
17+
/**
18+
* @Column(type="primary")
19+
*/
20+
#[Column(type: 'primary')]
21+
public int $id;
22+
23+
/**
24+
* @Column(type="datetime", name="created_at")
25+
* @GeneratedValue(beforeInsert=true)
26+
*/
27+
#[
28+
Column(type: 'datetime', name: 'created_at'),
29+
GeneratedValue(beforeInsert: true)
30+
]
31+
public \DateTimeImmutable $createdAt;
32+
33+
/**
34+
* @Column(type="datetime", name="created_at_generated_by_database")
35+
* @GeneratedValue(onInsert=true)
36+
*/
37+
#[
38+
Column(type: 'datetime', name: 'created_at_generated_by_database'),
39+
GeneratedValue(onInsert: true)
40+
]
41+
public \DateTimeImmutable $createdAtGeneratedByDatabase;
42+
43+
/**
44+
* @Column(type="datetime", name="created_at")
45+
* @GeneratedValue(beforeInsert=true, beforeUpdate=true)
46+
*/
47+
#[
48+
Column(type: 'datetime', name: 'updated_at'),
49+
GeneratedValue(beforeInsert: true, beforeUpdate: true)
50+
]
51+
public \DateTimeImmutable $updatedAt;
52+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Annotated\Tests\Functional\Driver\Common;
6+
7+
use Cycle\Annotated\Entities;
8+
use Cycle\ORM\Schema\GeneratedField;
9+
use Cycle\ORM\SchemaInterface;
10+
use Cycle\Schema\Compiler;
11+
use Cycle\Schema\Registry;
12+
use Spiral\Attributes\ReaderInterface;
13+
use Spiral\Tokenizer\Config\TokenizerConfig;
14+
use Spiral\Tokenizer\Tokenizer;
15+
16+
abstract class GeneratedFieldsTest extends BaseTest
17+
{
18+
/**
19+
* @dataProvider allReadersProvider
20+
*/
21+
public function testGeneratedFields(ReaderInterface $reader): void
22+
{
23+
$tokenizer = new Tokenizer(new TokenizerConfig([
24+
'directories' => [__DIR__ . '/../../../Fixtures/Fixtures25'],
25+
'exclude' => ['PostgreSQL'],
26+
]));
27+
28+
$r = new Registry($this->dbal);
29+
$schema = (new Compiler())->compile($r, [
30+
new Entities($tokenizer->classLocator(), $reader),
31+
]);
32+
33+
$this->assertSame(
34+
[
35+
'id' => GeneratedField::ON_INSERT,
36+
'createdAt' => GeneratedField::BEFORE_INSERT,
37+
'createdAtGeneratedByDatabase' => GeneratedField::ON_INSERT,
38+
'updatedAt' => GeneratedField::BEFORE_INSERT | GeneratedField::BEFORE_UPDATE,
39+
],
40+
$schema['withGeneratedFields'][SchemaInterface::GENERATED_FIELDS]
41+
);
42+
}
43+
}

tests/Annotated/Functional/Driver/Common/Inheritance/SingleTableTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Cycle\ORM\Relation;
2020
use Cycle\ORM\Schema;
2121
use Cycle\ORM\SchemaInterface;
22+
use Cycle\ORM\Schema\GeneratedField;
2223
use Cycle\ORM\Select\Repository;
2324
use Cycle\ORM\Select\Source;
2425
use Cycle\ORM\Transaction;
@@ -312,6 +313,9 @@ public function testSingleTableInheritanceWithDifferentColumnDeclaration(
312313
],
313314
SchemaInterface::SCHEMA => [],
314315
SchemaInterface::TYPECAST_HANDLER => null,
316+
SchemaInterface::GENERATED_FIELDS => [
317+
'id' => GeneratedField::ON_INSERT,
318+
],
315319
],
316320
$schema['comment']
317321
);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Annotated\Tests\Functional\Driver\MySQL;
6+
7+
// phpcs:ignore
8+
use Cycle\Annotated\Tests\Functional\Driver\Common\GeneratedFieldsTest as CommonClass;
9+
10+
/**
11+
* @group driver
12+
* @group driver-mysql
13+
*/
14+
final class GeneratedFieldsTest extends CommonClass
15+
{
16+
public const DRIVER = 'mysql';
17+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Annotated\Tests\Functional\Driver\Postgres;
6+
7+
// phpcs:ignore
8+
use Cycle\Annotated\Entities;
9+
use Cycle\Annotated\Tests\Functional\Driver\Common\GeneratedFieldsTest as CommonClass;
10+
use Cycle\ORM\Schema\GeneratedField;
11+
use Cycle\ORM\SchemaInterface;
12+
use Cycle\Schema\Compiler;
13+
use Cycle\Schema\Registry;
14+
use Spiral\Attributes\ReaderInterface;
15+
use Spiral\Tokenizer\Config\TokenizerConfig;
16+
use Spiral\Tokenizer\Tokenizer;
17+
18+
/**
19+
* @group driver
20+
* @group driver-postgres
21+
*/
22+
final class GeneratedFieldsTest extends CommonClass
23+
{
24+
public const DRIVER = 'postgres';
25+
26+
/**
27+
* @dataProvider allReadersProvider
28+
*/
29+
public function testSerialGeneratedFields(ReaderInterface $reader): void
30+
{
31+
$tokenizer = new Tokenizer(new TokenizerConfig([
32+
'directories' => [__DIR__ . '/../../../Fixtures/Fixtures25/PostgreSQL'],
33+
'exclude' => [],
34+
]));
35+
36+
$r = new Registry($this->dbal);
37+
38+
$schema = (new Compiler())->compile($r, [
39+
new Entities($tokenizer->classLocator(), $reader),
40+
]);
41+
42+
$this->assertSame(
43+
[
44+
'id' => GeneratedField::ON_INSERT,
45+
'smallSerial' => GeneratedField::ON_INSERT,
46+
'serial' => GeneratedField::ON_INSERT,
47+
'bigSerial' => GeneratedField::ON_INSERT,
48+
],
49+
$schema['withGeneratedSerial'][SchemaInterface::GENERATED_FIELDS]
50+
);
51+
}
52+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Annotated\Tests\Functional\Driver\SQLServer;
6+
7+
// phpcs:ignore
8+
use Cycle\Annotated\Tests\Functional\Driver\Common\GeneratedFieldsTest as CommonClass;
9+
10+
/**
11+
* @group driver
12+
* @group driver-sqlserver
13+
*/
14+
final class GeneratedFieldsTest extends CommonClass
15+
{
16+
public const DRIVER = 'sqlserver';
17+
}

0 commit comments

Comments
 (0)