Skip to content

Commit 3811651

Browse files
committed
Implement an EnumType for MySQL/MariaDB
1 parent e0f3674 commit 3811651

File tree

12 files changed

+291
-12
lines changed

12 files changed

+291
-12
lines changed

docs/en/reference/types.rst

+10
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ type natively, this type is mapped to the ``string`` type internally.
196196
Values retrieved from the database are always converted to PHP's ``string`` type
197197
or ``null`` if no data is present.
198198

199+
enum
200+
++++
201+
202+
Maps and converts a string which is one of a set of predefined values. This
203+
type is specifically designed for MySQL and MariaDB, where it is mapped to
204+
the native ``ENUM`` type. For other database vendors, this type is mapped to
205+
a string field (``VARCHAR``) with the maximum length being the length of the
206+
longest value in the set. Values retrieved from the database are always
207+
converted to PHP's ``string`` type or ``null`` if no data is present.
208+
199209
Binary string types
200210
^^^^^^^^^^^^^^^^^^^
201211

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Exception\InvalidColumnType;
6+
7+
use Doctrine\DBAL\Exception\InvalidColumnType;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
10+
use function get_debug_type;
11+
use function sprintf;
12+
13+
final class ColumnValuesRequired extends InvalidColumnType
14+
{
15+
/**
16+
* @param AbstractPlatform $platform The target platform
17+
* @param string $type The SQL column type
18+
*/
19+
public static function new(AbstractPlatform $platform, string $type): self
20+
{
21+
return new self(
22+
sprintf(
23+
'%s requires the values of a %s column to be specified',
24+
get_debug_type($platform),
25+
$type,
26+
),
27+
);
28+
}
29+
}

src/Platforms/AbstractMySQLPlatform.php

+19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Doctrine\DBAL\Connection;
88
use Doctrine\DBAL\Exception;
9+
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnValuesRequired;
910
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
1011
use Doctrine\DBAL\Platforms\Keywords\MySQLKeywords;
1112
use Doctrine\DBAL\Schema\AbstractAsset;
@@ -18,12 +19,14 @@
1819
use Doctrine\DBAL\TransactionIsolationLevel;
1920
use Doctrine\DBAL\Types\Types;
2021

22+
use function array_map;
2123
use function array_merge;
2224
use function array_unique;
2325
use function array_values;
2426
use function count;
2527
use function implode;
2628
use function in_array;
29+
use function is_array;
2730
use function is_numeric;
2831
use function sprintf;
2932
use function str_replace;
@@ -645,6 +648,21 @@ public function getDecimalTypeDeclarationSQL(array $column): string
645648
return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column);
646649
}
647650

651+
/**
652+
* {@inheritDoc}
653+
*/
654+
public function getEnumDeclarationSQL(array $column): string
655+
{
656+
if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) {
657+
throw ColumnValuesRequired::new($this, 'ENUM');
658+
}
659+
660+
return sprintf('ENUM(%s)', implode(', ', array_map(
661+
$this->quoteStringLiteral(...),
662+
$column['values'],
663+
)));
664+
}
665+
648666
/**
649667
* Get unsigned declaration for a column.
650668
*
@@ -718,6 +736,7 @@ protected function initializeDoctrineTypeMappings(): void
718736
'datetime' => Types::DATETIME_MUTABLE,
719737
'decimal' => Types::DECIMAL,
720738
'double' => Types::FLOAT,
739+
'enum' => Types::ENUM,
721740
'float' => Types::SMALLFLOAT,
722741
'int' => Types::INTEGER,
723742
'integer' => Types::INTEGER,

src/Platforms/AbstractPlatform.php

+22
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnLengthRequired;
1313
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnPrecisionRequired;
1414
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnScaleRequired;
15+
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnValuesRequired;
1516
use Doctrine\DBAL\LockMode;
1617
use Doctrine\DBAL\Platforms\Exception\NoColumnsSpecifiedForTable;
1718
use Doctrine\DBAL\Platforms\Exception\NotSupported;
@@ -51,6 +52,8 @@
5152
use function is_float;
5253
use function is_int;
5354
use function is_string;
55+
use function max;
56+
use function mb_strlen;
5457
use function preg_quote;
5558
use function preg_replace;
5659
use function sprintf;
@@ -190,6 +193,25 @@ public function getBinaryTypeDeclarationSQL(array $column): string
190193
}
191194
}
192195

196+
/**
197+
* Returns the SQL snippet to declare an ENUM column.
198+
*
199+
* Enum is a non-standard type that is especially popular in MySQL and MariaDB. By default, this method map to
200+
* a simple VARCHAR field which allows us to deploy it on any platform, e.g. SQLite.
201+
*
202+
* @param array<string, mixed> $column
203+
*
204+
* @throws ColumnValuesRequired If the column definition does not contain any values.
205+
*/
206+
public function getEnumDeclarationSQL(array $column): string
207+
{
208+
if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) {
209+
throw ColumnValuesRequired::new($this, 'ENUM');
210+
}
211+
212+
return $this->getStringTypeDeclarationSQL(['length' => max(...array_map(mb_strlen(...), $column['values']))]);
213+
}
214+
193215
/**
194216
* Returns the SQL snippet to declare a GUID/UUID column.
195217
*

src/Schema/Column.php

+33-11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class Column extends AbstractAsset
3333

3434
protected bool $_autoincrement = false;
3535

36+
/** @var list<string> */
37+
protected array $_values = [];
38+
3639
/** @var array<string, mixed> */
3740
protected array $_platformOptions = [];
3841

@@ -231,22 +234,41 @@ public function getComment(): string
231234
return $this->_comment;
232235
}
233236

237+
/**
238+
* @param list<string> $values
239+
*
240+
* @return $this
241+
*/
242+
public function setValues(array $values): static
243+
{
244+
$this->_values = $values;
245+
246+
return $this;
247+
}
248+
249+
/** @return list<string> */
250+
public function getValues(): array
251+
{
252+
return $this->_values;
253+
}
254+
234255
/** @return array<string, mixed> */
235256
public function toArray(): array
236257
{
237258
return array_merge([
238-
'name' => $this->_name,
239-
'type' => $this->_type,
240-
'default' => $this->_default,
241-
'notnull' => $this->_notnull,
242-
'length' => $this->_length,
243-
'precision' => $this->_precision,
244-
'scale' => $this->_scale,
245-
'fixed' => $this->_fixed,
246-
'unsigned' => $this->_unsigned,
247-
'autoincrement' => $this->_autoincrement,
259+
'name' => $this->_name,
260+
'type' => $this->_type,
261+
'default' => $this->_default,
262+
'notnull' => $this->_notnull,
263+
'length' => $this->_length,
264+
'precision' => $this->_precision,
265+
'scale' => $this->_scale,
266+
'fixed' => $this->_fixed,
267+
'unsigned' => $this->_unsigned,
268+
'autoincrement' => $this->_autoincrement,
248269
'columnDefinition' => $this->_columnDefinition,
249-
'comment' => $this->_comment,
270+
'comment' => $this->_comment,
271+
'values' => $this->_values,
250272
], $this->_platformOptions);
251273
}
252274
}

src/Schema/MySQLSchemaManager.php

+21
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
use Doctrine\DBAL\Types\Type;
1818

1919
use function array_change_key_case;
20+
use function array_map;
2021
use function assert;
2122
use function explode;
2223
use function implode;
2324
use function is_string;
2425
use function preg_match;
26+
use function preg_match_all;
2527
use function str_contains;
2628
use function strtok;
2729
use function strtolower;
@@ -134,6 +136,8 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
134136

135137
$type = $this->platform->getDoctrineTypeMapping($dbType);
136138

139+
$values = [];
140+
137141
switch ($dbType) {
138142
case 'char':
139143
case 'binary':
@@ -192,6 +196,10 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
192196
case 'year':
193197
$length = null;
194198
break;
199+
200+
case 'enum':
201+
$values = $this->parseEnumExpression($tableColumn['type']);
202+
break;
195203
}
196204

197205
if ($this->platform instanceof MariaDBPlatform) {
@@ -209,6 +217,7 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
209217
'scale' => $scale,
210218
'precision' => $precision,
211219
'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'),
220+
'values' => $values,
212221
];
213222

214223
if (isset($tableColumn['comment'])) {
@@ -228,6 +237,18 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
228237
return $column;
229238
}
230239

240+
/** @return list<string> */
241+
private function parseEnumExpression(string $expression): array
242+
{
243+
$result = preg_match_all("/'([^']*(?:''[^']*)*)'/", $expression, $matches);
244+
assert($result !== false);
245+
246+
return array_map(
247+
static fn (string $match): string => strtr($match, ["''" => "'"]),
248+
$matches[1],
249+
);
250+
}
251+
231252
/**
232253
* Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
233254
*

src/Types/EnumType.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Types;
6+
7+
use Doctrine\DBAL\Platforms\AbstractPlatform;
8+
9+
final class EnumType extends Type
10+
{
11+
/**
12+
* {@inheritDoc}
13+
*/
14+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
15+
{
16+
return $platform->getEnumDeclarationSQL($column);
17+
}
18+
}

src/Types/Type.php

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ abstract class Type
3434
Types::DATETIMETZ_MUTABLE => DateTimeTzType::class,
3535
Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class,
3636
Types::DECIMAL => DecimalType::class,
37+
Types::ENUM => EnumType::class,
3738
Types::FLOAT => FloatType::class,
3839
Types::GUID => GuidType::class,
3940
Types::INTEGER => IntegerType::class,

src/Types/Types.php

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class Types
2323
public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable';
2424
public const DECIMAL = 'decimal';
2525
public const FLOAT = 'float';
26+
public const ENUM = 'enum';
2627
public const GUID = 'guid';
2728
public const INTEGER = 'integer';
2829
public const JSON = 'json';

tests/Functional/Schema/MySQLSchemaManagerTest.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,10 @@ public function testColumnIntrospection(): void
561561
$doctrineTypes = array_keys(Type::getTypesMap());
562562

563563
foreach ($doctrineTypes as $type) {
564-
$table->addColumn('col_' . $type, $type, ['length' => 8, 'precision' => 8, 'scale' => 2]);
564+
$table->addColumn('col_' . $type, $type, match ($type) {
565+
Types::ENUM => ['values' => ['foo', 'bar']],
566+
default => ['length' => 8, 'precision' => 8, 'scale' => 2],
567+
});
565568
}
566569

567570
$this->dropAndCreateTable($table);

0 commit comments

Comments
 (0)