Skip to content

Commit 94c3983

Browse files
authored
Merge pull request #38 from vossik/primarykey
PrimaryKey attribute
2 parents c7e798d + 9d60ad9 commit 94c3983

12 files changed

Lines changed: 220 additions & 37 deletions

File tree

src/Attribute/PrimaryKey.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CoolBeans\Attribute;
6+
7+
#[\Attribute(\Attribute::TARGET_CLASS)]
8+
final class PrimaryKey
9+
{
10+
public array $columns;
11+
12+
public function __construct(string ...$columns)
13+
{
14+
if (\count($columns) > 1) {
15+
throw new \CoolBeans\Exception\PrimaryKeyMultipleColumnsNotImplemented('Multiple column PrimaryKey is not implemented yet.');
16+
}
17+
18+
$this->columns = $columns;
19+
}
20+
}

src/Command/SqlGeneratorCommand.php

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ public function __construct()
1414
parent::__construct(self::$defaultName);
1515
}
1616

17+
public function generate(string $source) : string
18+
{
19+
$beans = $this->getBeans($source);
20+
$sorter = new \CoolBeans\Utils\TableSorter($beans);
21+
22+
$sortedBeans = $sorter->sort();
23+
24+
$ddl = '';
25+
$lastBean = \array_key_last($sortedBeans);
26+
27+
foreach ($sortedBeans as $key => $bean) {
28+
$ddl .= $this->generateBean($bean);
29+
30+
if ($lastBean !== $key) {
31+
$ddl .= \PHP_EOL . \PHP_EOL;
32+
}
33+
}
34+
35+
return $ddl;
36+
}
37+
1738
protected function configure() : void
1839
{
1940
$this->setName(self::$defaultName);
@@ -40,30 +61,16 @@ protected function execute(
4061
return 0;
4162
}
4263

43-
public function generate(string $source) : string
64+
private static function hasPrimaryKeyAttribute(\ReflectionClass $bean) : bool
4465
{
45-
$beans = $this->getBeans($source);
46-
$sorter = new \CoolBeans\Utils\TableSorter($beans);
47-
48-
$sortedBeans = $sorter->sort();
49-
50-
$ddl = '';
51-
$lastBean = \array_key_last($sortedBeans);
52-
53-
foreach ($sortedBeans as $key => $bean) {
54-
$ddl .= $this->generateBean($bean);
55-
56-
if ($lastBean !== $key) {
57-
$ddl .= \PHP_EOL . \PHP_EOL;
58-
}
59-
}
60-
61-
return $ddl;
66+
return \count($bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)) > 0
67+
&& \count($bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns) > 0;
6268
}
6369

6470
private function generateBean(string $className) : string
6571
{
6672
$bean = new \ReflectionClass($className);
73+
$this->validateBean($bean);
6774

6875
$beanName = \Infinityloop\Utils\CaseConverter::toSnakeCase($bean->getShortName());
6976
$toReturn = 'CREATE TABLE `' . $beanName . '`(' . \PHP_EOL;
@@ -83,11 +90,11 @@ private function generateBean(string $className) : string
8390
'name' => $this->getPropertyName($property),
8491
'dataType' => $this->getDataType($property),
8592
'notNull' => $this->getNotNull($property),
86-
'default' => $this->getDefault($property),
93+
'default' => $this->getDefault($property, $bean),
8794
'comment' => $this->getComment($property),
8895
];
8996

90-
$foreignKey = $this->getForeignKey($property);
97+
$foreignKey = $this->getForeignKey($property, $bean);
9198
$uniqueConstraint = $this->getUnique($property, $beanName);
9299

93100
if (\is_string($uniqueConstraint)) {
@@ -192,9 +199,17 @@ private function getTableCollation(\ReflectionClass $bean) : string
192199
return 'COLLATE = `' . $collationAttribute[0]->newInstance()->collation . '`';
193200
}
194201

195-
private function getDefault(\ReflectionProperty $property) : string
202+
private function getDefault(\ReflectionProperty $property, \ReflectionClass $bean) : string
196203
{
197-
if ($property->getName() === 'id') {
204+
$hasPrimaryKeyAttribute = self::hasPrimaryKeyAttribute($bean);
205+
$attributeColumns = $hasPrimaryKeyAttribute
206+
? $bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns
207+
: [];
208+
209+
if (
210+
($hasPrimaryKeyAttribute && $attributeColumns[0] === $property->getName())
211+
|| (!$hasPrimaryKeyAttribute && $property->getName() === 'id')
212+
) {
198213
return ' AUTO_INCREMENT PRIMARY KEY';
199214
}
200215

@@ -450,12 +465,49 @@ private function printSection(array $data) : string
450465
return ',' . \PHP_EOL . \PHP_EOL . \implode(',' . \PHP_EOL, $data);
451466
}
452467

453-
private function getForeignKey(\ReflectionProperty $property) : ?string
468+
private function validateBean(\ReflectionClass $bean) : void
469+
{
470+
$hasPrimaryKeyAttribute = self::hasPrimaryKeyAttribute($bean);
471+
$attributeColumns = $hasPrimaryKeyAttribute
472+
? $bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns
473+
: [];
474+
$hasId = false;
475+
476+
foreach ($bean->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
477+
if ($property->getName() === 'id') {
478+
$hasId = true;
479+
}
480+
481+
foreach ($attributeColumns as $key => $column) {
482+
if ($column === $property->getName()) {
483+
$hasId = true;
484+
unset($attributeColumns[$key]);
485+
}
486+
}
487+
}
488+
489+
if ($hasPrimaryKeyAttribute && \count($attributeColumns) > 0) {
490+
throw new \CoolBeans\Exception\PrimaryKeyColumnDoesntExist(
491+
'PrimaryKey attribute column(s) ' . \implode(', ', $attributeColumns) . ' doesn\'t exist in Bean ' . $bean->getShortName() . '.',
492+
);
493+
}
494+
495+
if (!$hasPrimaryKeyAttribute && !$hasId) {
496+
throw new \CoolBeans\Exception\MissingPrimaryKey('Bean ' . $bean->getShortName() . ' has no primary key.');
497+
}
498+
}
499+
500+
private function getForeignKey(\ReflectionProperty $property, \ReflectionClass $bean) : ?string
454501
{
455502
$type = $property->getType();
456503
\assert($type instanceof \ReflectionNamedType);
457504

458-
if ($type->getName() !== \CoolBeans\PrimaryKey\IntPrimaryKey::class || $property->getName() === 'id') {
505+
$hasPrimaryKeyAttribute = self::hasPrimaryKeyAttribute($bean);
506+
$attributeColumns = $hasPrimaryKeyAttribute
507+
? $bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns
508+
: [];
509+
510+
if ($property->getName() === 'id' || ($this->hasPrimaryKeyAttribute($bean) && \in_array($property->getName(), $attributeColumns, true))) {
459511
return null;
460512
}
461513

@@ -484,9 +536,11 @@ private function getForeignKey(\ReflectionProperty $property) : ?string
484536

485537
$table = $foreignKey->table;
486538
$column = $foreignKey->column;
487-
} else {
539+
} elseif (\str_contains($property->getName(), '_id')) {
488540
$table = \str_replace('_id', '', $property->getName());
489541
$column = 'id';
542+
} else {
543+
return null;
490544
}
491545

492546
return self::INDENTATION . 'FOREIGN KEY (`' . $property->getName() . '`) REFERENCES `' . $table . '`(`' . $column . '`)'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CoolBeans\Exception;
6+
7+
final class BeanHasNoPrimaryKey extends \Exception
8+
{
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CoolBeans\Exception;
6+
7+
final class PrimaryKeyColumnDoesntExist extends \Exception
8+
{
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CoolBeans\Exception;
6+
7+
final class PrimaryKeyMultipleColumnsNotImplemented extends \Exception
8+
{
9+
}

src/Utils/TableSorter.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@ private function getForeignKeyTables(\ReflectionClass $bean) : array
6969
$type = $property->getType();
7070
\assert($type instanceof \ReflectionNamedType);
7171

72-
if ($type->getName() !== \CoolBeans\PrimaryKey\IntPrimaryKey::class || !\str_ends_with($property->getName(), '_id')) {
72+
$foreignKeyTarget = $this->getForeignKeyDependency($property);
73+
74+
if ($foreignKeyTarget === null) {
7375
continue;
7476
}
7577

76-
$foreignKeyTarget = $this->getForeignKeyDependency($property);
77-
7878
if ($foreignKeyTarget === \Infinityloop\Utils\CaseConverter::toSnakeCase($bean->getShortName())) {
7979
continue; // self dependency
8080
}
@@ -85,7 +85,7 @@ private function getForeignKeyTables(\ReflectionClass $bean) : array
8585
return $toReturn;
8686
}
8787

88-
private function getForeignKeyDependency(\ReflectionProperty $property) : string
88+
private function getForeignKeyDependency(\ReflectionProperty $property) : ?string
8989
{
9090
$foreignKeyAttribute = $property->getAttributes(\CoolBeans\Attribute\ForeignKey::class);
9191

@@ -95,6 +95,10 @@ private function getForeignKeyDependency(\ReflectionProperty $property) : string
9595
return $foreignKey->table;
9696
}
9797

98+
if (!\str_ends_with($property->getName(), '_id')) {
99+
return null;
100+
}
101+
98102
return \substr($property->getName(), 0, -3);
99103
}
100104
}

tests/Unit/BeanTest.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,8 @@ public function testGetPrimaryKey() : void
5858
public function testGetIterator() : void
5959
{
6060
$iteratorInstance = new class implements \Iterator {
61-
public function rewind() : bool
61+
public function rewind() : void
6262
{
63-
return false;
6463
}
6564

6665
public function current() : bool
@@ -73,9 +72,8 @@ public function key() : bool
7372
return false;
7473
}
7574

76-
public function next() : bool
75+
public function next() : void
7776
{
78-
return false;
7977
}
8078

8179
public function valid() : bool

tests/Unit/Command/SqlGeneratorCommandTest.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public function testSimple() : void
8181
`col8` DOUBLE(16, 2) NOT NULL,
8282
`col9_id` INT(11) UNSIGNED NOT NULL,
8383
`col10_id` INT(11) UNSIGNED NOT NULL,
84+
`code` VARCHAR(255) NOT NULL AUTO_INCREMENT PRIMARY KEY,
85+
`id` INT(11) UNSIGNED NOT NULL,
8486
8587
INDEX `attribute_bean_col5_index` (`col5`),
8688
INDEX `attribute_bean_col6_index` (`col6` ASC),
@@ -188,20 +190,54 @@ public function testUndefinedProperty() : void
188190
]);
189191
}
190192

191-
public function testColumnCount() : void
193+
public function testMissingPrimaryKey() : void
192194
{
193195
$application = new \Symfony\Component\Console\Application();
194196
$application->addCommands([new \CoolBeans\Command\SqlGeneratorCommand()]);
195197

196198
$command = $application->find('sqlGenerator');
197199
$commandTester = new \Symfony\Component\Console\Tester\CommandTester($command);
198200

199-
$this->expectException(\CoolBeans\Exception\InvalidClassUniqueConstraintColumnCount::class);
200-
$this->expectExceptionMessage('ClassUniqueConstraint expects at least two column names.');
201+
$this->expectException(\CoolBeans\Exception\MissingPrimaryKey::class);
202+
$this->expectExceptionMessage('Bean InvalidBean has no primary key.');
201203

202204
$commandTester->execute([
203205
'command' => 'sqlGenerator',
204-
'source' => __DIR__ . '/../InvalidBean/ColumnCount/',
206+
'source' => __DIR__ . '/../InvalidBean/MissingPrimaryKey/',
207+
]);
208+
}
209+
210+
public function testPrimaryKeyAttributeMultipleColumns() : void
211+
{
212+
$application = new \Symfony\Component\Console\Application();
213+
$application->addCommands([new \CoolBeans\Command\SqlGeneratorCommand()]);
214+
215+
$command = $application->find('sqlGenerator');
216+
$commandTester = new \Symfony\Component\Console\Tester\CommandTester($command);
217+
218+
$this->expectException(\CoolBeans\Exception\PrimaryKeyMultipleColumnsNotImplemented::class);
219+
$this->expectExceptionMessage('Multiple column PrimaryKey is not implemented yet.');
220+
221+
$commandTester->execute([
222+
'command' => 'sqlGenerator',
223+
'source' => __DIR__ . '/../InvalidBean/PrimaryKeyAttributeMultipleColumns/',
224+
]);
225+
}
226+
227+
public function testPrimaryKeyAttributeMissingColumn() : void
228+
{
229+
$application = new \Symfony\Component\Console\Application();
230+
$application->addCommands([new \CoolBeans\Command\SqlGeneratorCommand()]);
231+
232+
$command = $application->find('sqlGenerator');
233+
$commandTester = new \Symfony\Component\Console\Tester\CommandTester($command);
234+
235+
$this->expectException(\CoolBeans\Exception\PrimaryKeyColumnDoesntExist::class);
236+
$this->expectExceptionMessage('PrimaryKey attribute column(s) unknown doesn\'t exist in Bean InvalidBean.');
237+
238+
$commandTester->execute([
239+
'command' => 'sqlGenerator',
240+
'source' => __DIR__ . '/../InvalidBean/PrimaryKeyAttributeMissingColumn/',
205241
]);
206242
}
207243
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CoolBeans\Tests\Unit\InvalidBean\MissingPrimaryKey;
6+
7+
//@phpcs:disable SlevomatCodingStandard.Classes.ClassStructure.IncorrectGroupOrder
8+
//@phpcs:disable SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty
9+
final class InvalidBean extends \CoolBeans\Bean
10+
{
11+
public \CoolBeans\PrimaryKey\IntPrimaryKey $abc;
12+
public int $code;
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CoolBeans\Tests\Unit\InvalidBean\PrimaryKeyAttributeMissingColumn;
6+
7+
//@phpcs:disable SlevomatCodingStandard.Classes.ClassStructure.IncorrectGroupOrder
8+
//@phpcs:disable SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty
9+
#[\CoolBeans\Attribute\PrimaryKey('unknown')]
10+
final class InvalidBean extends \CoolBeans\Bean
11+
{
12+
public \CoolBeans\PrimaryKey\IntPrimaryKey $id;
13+
public \CoolBeans\PrimaryKey\IntPrimaryKey $code;
14+
}

0 commit comments

Comments
 (0)