Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Attribute/PrimaryKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types = 1);

namespace CoolBeans\Attribute;

#[\Attribute(\Attribute::TARGET_CLASS)]
final class PrimaryKey
{
public array $columns;

public function __construct(string ...$columns)
{
if (\count($columns) > 1) {
throw new \CoolBeans\Exception\PrimaryKeyMultipleColumnsNotImplemented('Multiple column PrimaryKey is not implemented yet.');
}

$this->columns = $columns;
}
}
104 changes: 79 additions & 25 deletions src/Command/SqlGeneratorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ public function __construct()
parent::__construct(self::$defaultName);
}

public function generate(string $source) : string
{
$beans = $this->getBeans($source);
$sorter = new \CoolBeans\Utils\TableSorter($beans);

$sortedBeans = $sorter->sort();

$ddl = '';
$lastBean = \array_key_last($sortedBeans);

foreach ($sortedBeans as $key => $bean) {
$ddl .= $this->generateBean($bean);

if ($lastBean !== $key) {
$ddl .= \PHP_EOL . \PHP_EOL;
}
}

return $ddl;
}

protected function configure() : void
{
$this->setName(self::$defaultName);
Expand All @@ -40,30 +61,16 @@ protected function execute(
return 0;
}

public function generate(string $source) : string
private static function hasPrimaryKeyAttribute(\ReflectionClass $bean) : bool
{
$beans = $this->getBeans($source);
$sorter = new \CoolBeans\Utils\TableSorter($beans);

$sortedBeans = $sorter->sort();

$ddl = '';
$lastBean = \array_key_last($sortedBeans);

foreach ($sortedBeans as $key => $bean) {
$ddl .= $this->generateBean($bean);

if ($lastBean !== $key) {
$ddl .= \PHP_EOL . \PHP_EOL;
}
}

return $ddl;
return \count($bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)) > 0
&& \count($bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns) > 0;
}

private function generateBean(string $className) : string
{
$bean = new \ReflectionClass($className);
$this->validateBean($bean);

$beanName = \Infinityloop\Utils\CaseConverter::toSnakeCase($bean->getShortName());
$toReturn = 'CREATE TABLE `' . $beanName . '`(' . \PHP_EOL;
Expand All @@ -83,11 +90,11 @@ private function generateBean(string $className) : string
'name' => $this->getPropertyName($property),
'dataType' => $this->getDataType($property),
'notNull' => $this->getNotNull($property),
'default' => $this->getDefault($property),
'default' => $this->getDefault($property, $bean),
'comment' => $this->getComment($property),
];

$foreignKey = $this->getForeignKey($property);
$foreignKey = $this->getForeignKey($property, $bean);
$uniqueConstraint = $this->getUnique($property, $beanName);

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

private function getDefault(\ReflectionProperty $property) : string
private function getDefault(\ReflectionProperty $property, \ReflectionClass $bean) : string
{
if ($property->getName() === 'id') {
$hasPrimaryKeyAttribute = self::hasPrimaryKeyAttribute($bean);
$attributeColumns = $hasPrimaryKeyAttribute
? $bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns
: [];

if (
($hasPrimaryKeyAttribute && $attributeColumns[0] === $property->getName())
|| (!$hasPrimaryKeyAttribute && $property->getName() === 'id')
) {
return ' AUTO_INCREMENT PRIMARY KEY';
}

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

private function getForeignKey(\ReflectionProperty $property) : ?string
private function validateBean(\ReflectionClass $bean) : void
{
$hasPrimaryKeyAttribute = self::hasPrimaryKeyAttribute($bean);
$attributeColumns = $hasPrimaryKeyAttribute
? $bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns
: [];
$hasId = false;

foreach ($bean->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
if ($property->getName() === 'id') {
$hasId = true;
}

foreach ($attributeColumns as $key => $column) {
if ($column === $property->getName()) {
$hasId = true;
unset($attributeColumns[$key]);
}
}
}

if ($hasPrimaryKeyAttribute && \count($attributeColumns) > 0) {
throw new \CoolBeans\Exception\PrimaryKeyColumnDoesntExist(
'PrimaryKey attribute column(s) ' . \implode(', ', $attributeColumns) . ' doesn\'t exist in Bean ' . $bean->getShortName() . '.',
);
}

if (!$hasPrimaryKeyAttribute && !$hasId) {
throw new \CoolBeans\Exception\MissingPrimaryKey('Bean ' . $bean->getShortName() . ' has no primary key.');
}
}

private function getForeignKey(\ReflectionProperty $property, \ReflectionClass $bean) : ?string
{
$type = $property->getType();
\assert($type instanceof \ReflectionNamedType);

if ($type->getName() !== \CoolBeans\PrimaryKey\IntPrimaryKey::class || $property->getName() === 'id') {
$hasPrimaryKeyAttribute = self::hasPrimaryKeyAttribute($bean);
$attributeColumns = $hasPrimaryKeyAttribute
? $bean->getAttributes(\CoolBeans\Attribute\PrimaryKey::class)[0]->newInstance()->columns
: [];

if ($property->getName() === 'id' || ($this->hasPrimaryKeyAttribute($bean) && \in_array($property->getName(), $attributeColumns, true))) {
return null;
}

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

$table = $foreignKey->table;
$column = $foreignKey->column;
} else {
} elseif (\str_contains($property->getName(), '_id')) {
$table = \str_replace('_id', '', $property->getName());
$column = 'id';
} else {
return null;
}

return self::INDENTATION . 'FOREIGN KEY (`' . $property->getName() . '`) REFERENCES `' . $table . '`(`' . $column . '`)'
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/BeanHasNoPrimaryKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types = 1);

namespace CoolBeans\Exception;

final class BeanHasNoPrimaryKey extends \Exception
{
}
9 changes: 9 additions & 0 deletions src/Exception/PrimaryKeyColumnDoesntExist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types = 1);

namespace CoolBeans\Exception;

final class PrimaryKeyColumnDoesntExist extends \Exception
{
}
9 changes: 9 additions & 0 deletions src/Exception/PrimaryKeyMultipleColumnsNotImplemented.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types = 1);

namespace CoolBeans\Exception;

final class PrimaryKeyMultipleColumnsNotImplemented extends \Exception
{
}
12 changes: 8 additions & 4 deletions src/Utils/TableSorter.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ private function getForeignKeyTables(\ReflectionClass $bean) : array
$type = $property->getType();
\assert($type instanceof \ReflectionNamedType);

if ($type->getName() !== \CoolBeans\PrimaryKey\IntPrimaryKey::class || !\str_ends_with($property->getName(), '_id')) {
$foreignKeyTarget = $this->getForeignKeyDependency($property);

if ($foreignKeyTarget === null) {
continue;
}

$foreignKeyTarget = $this->getForeignKeyDependency($property);

if ($foreignKeyTarget === \Infinityloop\Utils\CaseConverter::toSnakeCase($bean->getShortName())) {
continue; // self dependency
}
Expand All @@ -85,7 +85,7 @@ private function getForeignKeyTables(\ReflectionClass $bean) : array
return $toReturn;
}

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

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

if (!\str_ends_with($property->getName(), '_id')) {
return null;
}

return \substr($property->getName(), 0, -3);
}
}
6 changes: 2 additions & 4 deletions tests/Unit/BeanTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ public function testGetPrimaryKey() : void
public function testGetIterator() : void
{
$iteratorInstance = new class implements \Iterator {
public function rewind() : bool
public function rewind() : void
{
return false;
}

public function current() : bool
Expand All @@ -73,9 +72,8 @@ public function key() : bool
return false;
}

public function next() : bool
public function next() : void
{
return false;
}

public function valid() : bool
Expand Down
44 changes: 40 additions & 4 deletions tests/Unit/Command/SqlGeneratorCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public function testSimple() : void
`col8` DOUBLE(16, 2) NOT NULL,
`col9_id` INT(11) UNSIGNED NOT NULL,
`col10_id` INT(11) UNSIGNED NOT NULL,
`code` VARCHAR(255) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`id` INT(11) UNSIGNED NOT NULL,

INDEX `attribute_bean_col5_index` (`col5`),
INDEX `attribute_bean_col6_index` (`col6` ASC),
Expand Down Expand Up @@ -188,20 +190,54 @@ public function testUndefinedProperty() : void
]);
}

public function testColumnCount() : void
public function testMissingPrimaryKey() : void
{
$application = new \Symfony\Component\Console\Application();
$application->addCommands([new \CoolBeans\Command\SqlGeneratorCommand()]);

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

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

$commandTester->execute([
'command' => 'sqlGenerator',
'source' => __DIR__ . '/../InvalidBean/ColumnCount/',
'source' => __DIR__ . '/../InvalidBean/MissingPrimaryKey/',
]);
}

public function testPrimaryKeyAttributeMultipleColumns() : void
{
$application = new \Symfony\Component\Console\Application();
$application->addCommands([new \CoolBeans\Command\SqlGeneratorCommand()]);

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

$this->expectException(\CoolBeans\Exception\PrimaryKeyMultipleColumnsNotImplemented::class);
$this->expectExceptionMessage('Multiple column PrimaryKey is not implemented yet.');

$commandTester->execute([
'command' => 'sqlGenerator',
'source' => __DIR__ . '/../InvalidBean/PrimaryKeyAttributeMultipleColumns/',
]);
}

public function testPrimaryKeyAttributeMissingColumn() : void
{
$application = new \Symfony\Component\Console\Application();
$application->addCommands([new \CoolBeans\Command\SqlGeneratorCommand()]);

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

$this->expectException(\CoolBeans\Exception\PrimaryKeyColumnDoesntExist::class);
$this->expectExceptionMessage('PrimaryKey attribute column(s) unknown doesn\'t exist in Bean InvalidBean.');

$commandTester->execute([
'command' => 'sqlGenerator',
'source' => __DIR__ . '/../InvalidBean/PrimaryKeyAttributeMissingColumn/',
]);
}
}
13 changes: 13 additions & 0 deletions tests/Unit/InvalidBean/MissingPrimaryKey/InvalidBean.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types = 1);

namespace CoolBeans\Tests\Unit\InvalidBean\MissingPrimaryKey;

//@phpcs:disable SlevomatCodingStandard.Classes.ClassStructure.IncorrectGroupOrder
//@phpcs:disable SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty
final class InvalidBean extends \CoolBeans\Bean
{
public \CoolBeans\PrimaryKey\IntPrimaryKey $abc;
public int $code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types = 1);

namespace CoolBeans\Tests\Unit\InvalidBean\PrimaryKeyAttributeMissingColumn;

//@phpcs:disable SlevomatCodingStandard.Classes.ClassStructure.IncorrectGroupOrder
//@phpcs:disable SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty
#[\CoolBeans\Attribute\PrimaryKey('unknown')]
final class InvalidBean extends \CoolBeans\Bean
{
public \CoolBeans\PrimaryKey\IntPrimaryKey $id;
public \CoolBeans\PrimaryKey\IntPrimaryKey $code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types = 1);

namespace CoolBeans\Tests\Unit\InvalidBean\PrimaryKeyAttributeMultipleColumns;

//@phpcs:disable SlevomatCodingStandard.Classes.ClassStructure.IncorrectGroupOrder
//@phpcs:disable SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty
#[\CoolBeans\Attribute\PrimaryKey('id', 'code')]
final class InvalidBean extends \CoolBeans\Bean
{
public \CoolBeans\PrimaryKey\IntPrimaryKey $id;
public \CoolBeans\PrimaryKey\IntPrimaryKey $code;
}
3 changes: 3 additions & 0 deletions tests/Unit/TestBean/AttributeBean.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#[\CoolBeans\Attribute\ClassUniqueConstraint(['col4', 'col5', 'col6'])]
#[\CoolBeans\Attribute\ClassIndex(['col4', 'col5', 'col6'], [null, Order::DESC, Order::ASC])]
#[\CoolBeans\Attribute\Comment('Some random comment')]
#[\CoolBeans\Attribute\PrimaryKey('code')]
final class AttributeBean extends \CoolBeans\Bean
{
#[\CoolBeans\Attribute\DefaultValue(DefaultFunction::NOW)]
Expand Down Expand Up @@ -50,4 +51,6 @@ final class AttributeBean extends \CoolBeans\Bean
#[\CoolBeans\Attribute\ForeignKey('simple_bean_2')]
#[\CoolBeans\Attribute\ForeignKeyConstraint(ForeignKeyConstraintType::RESTRICT, ForeignKeyConstraintType::RESTRICT)]
public \CoolBeans\PrimaryKey\IntPrimaryKey $col10_id;
public string $code;
public \CoolBeans\PrimaryKey\IntPrimaryKey $id;
}