Skip to content

Commit

Permalink
Complete baking of enums. (#972)
Browse files Browse the repository at this point in the history
* Complete baking of enums.

* Update src/Command/EnumCommand.php

Co-authored-by: othercorey <[email protected]>

* Fix up index/view for enums.

* Fix up index/view for enums.

* Switch key/value, use CamelCase

* Stricter validation

* Update src/Command/ModelCommand.php

Co-authored-by: Mark Story <[email protected]>

* Cleanup

* Cleanup

* Add more tests.

---------

Co-authored-by: othercorey <[email protected]>
Co-authored-by: Mark Story <[email protected]>
  • Loading branch information
3 people authored Jan 2, 2024
1 parent b5f106f commit c7cb269
Show file tree
Hide file tree
Showing 15 changed files with 468 additions and 10 deletions.
86 changes: 83 additions & 3 deletions src/Command/EnumCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
*/
namespace Bake\Command;

use Bake\Utility\Model\EnumParser;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Utility\Inflector;
use InvalidArgumentException;

/**
* Enum code generator.
Expand Down Expand Up @@ -64,8 +68,20 @@ public function template(): string
*/
public function templateData(Arguments $arguments): array
{
$cases = EnumParser::parseCases($arguments->getArgument('cases'), (bool)$arguments->getOption('int'));
$isOfTypeInt = $this->isOfTypeInt($cases);
$backingType = $isOfTypeInt ? 'int' : 'string';
if ($arguments->getOption('int')) {
if ($cases && !$isOfTypeInt) {
throw new InvalidArgumentException('Cases do not match requested `int` backing type.');
}

$backingType = 'int';
}

$data = parent::templateData($arguments);
$data['backingType'] = $arguments->getOption('int') ? 'int' : 'string';
$data['backingType'] = $backingType;
$data['cases'] = $this->formatCases($cases);

return $data;
}
Expand All @@ -82,12 +98,76 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar

$parser->setDescription(
'Bake backed enums for use in models.'
)->addOption('int', [
'help' => 'Using backed enums with int instead of string as return type',
)->addArgument('name', [
'help' => 'Name of the enum to bake. You can use Plugin.name to bake plugin enums.',
'required' => true,
])->addArgument('cases', [
'help' => 'List of either `one,two` for string or `foo:0,bar:1` for int type.',
])->addOption('int', [
'help' => 'Using backed enums with int instead of string as return type.',
'boolean' => true,
'short' => 'i',
]);

return $parser;
}

/**
* @param array<string, int|string> $definition
* @return bool
*/
protected function isOfTypeInt(array $definition): bool
{
if (!$definition) {
return false;
}

foreach ($definition as $value) {
if (!is_int($value)) {
return false;
}
}

return true;
}

/**
* @param array<string, int|string> $cases
* @return array<string>
*/
protected function formatCases(array $cases): array
{
$formatted = [];
foreach ($cases as $case => $value) {
$case = Inflector::camelize(Inflector::underscore($case));
if (is_string($value)) {
$value = '\'' . $value . '\'';
}
$formatted[] = 'case ' . $case . ' = ' . $value . ';';
}

return $formatted;
}

/**
* Generate a class stub
*
* @param string $name The class name
* @param \Cake\Console\Arguments $args The console arguments
* @param \Cake\Console\ConsoleIo $io The console io
* @return void
*/
protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
parent::bake($name, $args, $io);

$path = $this->getPath($args);
$filename = $path . $name . '.php';

// Work around composer caching that classes/files do not exist.
// Check for the file as it might not exist in tests.
if (file_exists($filename)) {
require_once $filename;
}
}
}
108 changes: 105 additions & 3 deletions src/Command/ModelCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace Bake\Command;

use Bake\CodeGen\FileBuilder;
use Bake\Utility\Model\EnumParser;
use Bake\Utility\TableScanner;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
Expand All @@ -28,9 +29,12 @@
use Cake\Database\Schema\CachedCollection;
use Cake\Database\Schema\TableSchema;
use Cake\Database\Schema\TableSchemaInterface;
use Cake\Database\Type\EnumType;
use Cake\Database\TypeFactory;
use Cake\Datasource\ConnectionManager;
use Cake\ORM\Table;
use Cake\Utility\Inflector;
use ReflectionEnum;
use function Cake\Core\pluginSplit;

/**
Expand Down Expand Up @@ -111,6 +115,8 @@ public function bake(string $name, Arguments $args, ConsoleIo $io): void
$tableObject = $this->getTableObject($name, $table);
$this->validateNames($tableObject->getSchema(), $io);
$data = $this->getTableContext($tableObject, $table, $name, $args, $io);

$this->bakeEnums($tableObject, $data, $args, $io);
$this->bakeTable($tableObject, $data, $args, $io);
$this->bakeEntity($tableObject, $data, $args, $io);
$this->bakeFixture($tableObject->getAlias(), $tableObject->getTable(), $args, $io);
Expand Down Expand Up @@ -168,6 +174,7 @@ public function getTableContext(
$behaviors = $this->getBehaviors($tableObject);
$connection = $this->connection;
$hidden = $this->getHiddenFields($tableObject, $args);
$enumSchema = $this->getEnumDefinitions($tableObject->getSchema());

return compact(
'associations',
Expand All @@ -181,7 +188,8 @@ public function getTableContext(
'rulesChecker',
'behaviors',
'connection',
'hidden'
'hidden',
'enumSchema',
);
}

Expand Down Expand Up @@ -1118,7 +1126,7 @@ public function getCounterCache(Table $model): array
* Bake an entity class.
*
* @param \Cake\ORM\Table $model Model name or object
* @param array $data An array to use to generate the Table
* @param array<string, mixed> $data An array to use to generate the Table
* @param \Cake\Console\Arguments $args CLI Arguments
* @param \Cake\Console\ConsoleIo $io CLI io
* @return void
Expand Down Expand Up @@ -1170,7 +1178,7 @@ public function bakeEntity(Table $model, array $data, Arguments $args, ConsoleIo
* Bake a table class.
*
* @param \Cake\ORM\Table $model Model name or object
* @param array $data An array to use to generate the Table
* @param array<string, mixed> $data An array to use to generate the Table
* @param \Cake\Console\Arguments $args CLI Arguments
* @param \Cake\Console\ConsoleIo $io CLI Arguments
* @return void
Expand Down Expand Up @@ -1435,6 +1443,12 @@ protected function possibleEnumFields(TableSchemaInterface $schema): array

foreach ($schema->columns() as $column) {
$columnSchema = $schema->getColumn($column);
if (str_starts_with($columnSchema['type'], 'enum-')) {
$fields[] = $column;

continue;
}

if (!in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true)) {
continue;
}
Expand All @@ -1444,4 +1458,92 @@ protected function possibleEnumFields(TableSchemaInterface $schema): array

return $fields;
}

/**
* @param \Cake\Database\Schema\TableSchemaInterface $schema
* @return array<string, mixed>
*/
protected function getEnumDefinitions(TableSchemaInterface $schema): array
{
$enums = [];

foreach ($schema->columns() as $column) {
$columnSchema = $schema->getColumn($column);
if (
!in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true)
&& !str_starts_with($columnSchema['type'], 'enum-')
) {
continue;
}

if (empty($columnSchema['comment']) || strpos($columnSchema['comment'], '[enum]') === false) {
continue;
}

$enumsDefinitionString = trim(mb_substr($columnSchema['comment'], strpos($columnSchema['comment'], '[enum]') + 6));
$isInt = in_array($columnSchema['type'], ['integer', 'tinyinteger', 'smallinteger'], true);
if (str_starts_with($columnSchema['type'], 'enum-')) {
$dbType = TypeFactory::build($columnSchema['type']);
if ($dbType instanceof EnumType) {
$class = $dbType->getEnumClassName();
$reflectionEnum = new ReflectionEnum($class);
$backingType = (string)$reflectionEnum->getBackingType();
if ($backingType === 'int') {
$isInt = true;
}
}
}
$enumsDefinition = EnumParser::parseCases($enumsDefinitionString, $isInt);
if (!$enumsDefinition) {
continue;
}

$enums[$column] = [
'type' => $isInt ? 'int' : 'string',
'cases' => $enumsDefinition,
];
}

return $enums;
}

/**
* @param \Cake\ORM\Table $model
* @param array<string, mixed> $data
* @param \Cake\Console\Arguments $args
* @param \Cake\Console\ConsoleIo $io
* @return void
*/
protected function bakeEnums(Table $model, array $data, Arguments $args, ConsoleIo $io): void
{
$enums = $data['enumSchema'];
if (!$enums) {
return;
}

$entity = $this->_entityName($model->getAlias());

foreach ($enums as $column => $data) {
$enumCommand = new EnumCommand();

$name = $entity . Inflector::camelize($column);
if ($this->plugin) {
$name = $this->plugin . '.' . $name;
}

$enumCases = $data['cases'];

$cases = [];
foreach ($enumCases as $k => $v) {
$cases[] = $k . ':' . $v;
}

$args = new Arguments(
[$name, implode(',', $cases)],
['int' => $data['type'] === 'int'] + $args->getOptions(),
['name', 'cases']
);
$enumCommand->execute($args, $io);
}
}
}
45 changes: 45 additions & 0 deletions src/Utility/Model/EnumParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);

namespace Bake\Utility\Model;

use InvalidArgumentException;

enum EnumParser
{
/**
* @param string|null $casesString
* @param bool $int
* @return array<string, int|string>
*/
public static function parseCases(?string $casesString, bool $int): array
{
if ($casesString === null || $casesString === '') {
return [];
}

$enumCases = explode(',', $casesString);

$definition = [];
foreach ($enumCases as $k => $enumCase) {
$case = $value = trim($enumCase);
if (str_contains($case, ':')) {
$value = trim(mb_substr($case, strpos($case, ':') + 1));
$case = mb_substr($case, 0, strpos($case, ':'));
} elseif ($int) {
$value = $k;
}

if (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $case)) {
throw new InvalidArgumentException(sprintf('`%s` is not a valid enum case', $case));
}
if (is_string($value) && str_contains($value, '\'')) {
throw new InvalidArgumentException(sprintf('`%s` value cannot contain `\'` character', $case));
}

$definition[$case] = $int ? (int)$value : $value;
}

return $definition;
}
}
4 changes: 4 additions & 0 deletions src/View/Helper/BakeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array
if (isset($associationFields[$field])) {
return 'string';
}
if ($type && str_starts_with($type, 'enum-')) {
return 'enum';
}
$numberTypes = ['decimal', 'biginteger', 'integer', 'float', 'smallinteger', 'tinyinteger'];
if (in_array($type, $numberTypes, true)) {
return 'number';
Expand All @@ -258,6 +261,7 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array
'number' => [],
'string' => [],
'boolean' => [],
'enum' => [],
'date' => [],
'text' => [],
];
Expand Down
4 changes: 4 additions & 0 deletions templates/bake/Model/enum.twig
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
{{ DocBlock.classDescription(name, 'Enum', [])|raw }}
enum {{ name }}: {{ backingType }} implements EnumLabelInterface
{
{% if cases %}
{{ Bake.concat('\n ', cases) }}

{% endif %}
/**
* @return string
*/
Expand Down
4 changes: 3 additions & 1 deletion templates/bake/Template/index.twig
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
{% endif %}
{% if isKey is not same as(true) %}
{% set columnData = Bake.columnData(field, schema) %}
{% if columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %}
{% if columnData.type starts with 'enum-' %}
<td><?= ${{ singularVar }}->{{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->label()) ?></td>
{% elseif columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %}
<td><?= h(${{ singularVar }}->{{ field }}) ?></td>
{% elseif columnData.null %}
<td><?= ${{ singularVar }}->{{ field }} === null ? '' : $this->Number->format(${{ singularVar }}->{{ field }}) ?></td>
Expand Down
13 changes: 13 additions & 0 deletions templates/bake/Template/view.twig
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@
</tr>
{% endfor %}
{% endif %}
{% if groupedFields.enum %}
{% for field in groupedFields.enum %}
<tr>
<th><?= __('{{ field|humanize }}') ?></th>
{% set columnData = Bake.columnData(field, schema) %}
{% if columnData.null %}
<td><?= ${{ singularVar }}->{{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->label()) ?></td>
{% else %}
<td><?= h(${{ singularVar }}->{{ field }}->label()) ?></td>
{% endif %}
</tr>
{% endfor %}
{% endif %}
{% if groupedFields.date %}
{% for field in groupedFields.date %}
<tr>
Expand Down
Loading

0 comments on commit c7cb269

Please sign in to comment.