Skip to content

Commit

Permalink
feature #183 FlagBagType support (michnovka)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 2.x-dev branch.

Discussion
----------

FlagBagType support

This is initial commit for `FlagBagType` support.

Lets discuss in here

Commits
-------

d66316e FlagBagType support
  • Loading branch information
ogizanagi committed Apr 19, 2022
2 parents b950578 + d66316e commit 7600213
Show file tree
Hide file tree
Showing 8 changed files with 479 additions and 8 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,35 @@ class CardType extends AbstractType
// ...
}
```
#### FlagBag Form Type

If you want to use `FlagBag` in Symfony Forms, use the [FlagBagType](src/Bridge/Symfony/Form/Type/FlagBagType.php). This
type also extends Symfony [EnumType](https://symfony.com/doc/current/reference/forms/types/enum.html), but it transforms
form values to and from `FlagBag` instances.

```php
namespace App\Form\Type;
use App\Enum\Permissions;
use Symfony\Component\Form\AbstractType;
use Elao\Enum\Bridge\Symfony\Form\Type\FlagBagType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
class AuthenticationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('permission', FlagBagType::class, [
'class' => Permissions::class,
])
;
}
// ...
}
```

### Symfony HttpKernel

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum\Bridge\Symfony\Form\DataTransformer;

use Elao\Enum\Exception\InvalidArgumentException as ElaoInvalidArgumentException;
use Elao\Enum\Exception\LogicException;
use Elao\Enum\FlagBag;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\InvalidArgumentException;

abstract class AbstractFlagBagTransformer implements DataTransformerInterface
{
/** @var class-string<\BackedEnum> */
protected string $enumType;

/** @var class-string<\BackedEnum> */
public function __construct(string $enumType)
{
if (!is_a($enumType, \BackedEnum::class, true)) {
throw new InvalidArgumentException(sprintf(
'"%s" is not an instance of "%s"',
$enumType,
\BackedEnum::class
));
}

try {
FlagBag::getBitmask($enumType);
} catch (LogicException $e) {
throw new InvalidArgumentException(sprintf(
'"%s" is not a valid bitmask enum',
$enumType
), 0, $e);
} catch (ElaoInvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}

$this->enumType = $enumType;
}

/**
* @param int|\BackedEnum[] $value
*/
protected function createEnum(int|array $value): FlagBag
{
if (\is_int($value)) {
return new FlagBag($this->enumType, $value);
} else {
return FlagBag::from($this->enumType, ...$value);
}
}

protected function isAcceptableValueForEnum(int $value): bool
{
return FlagBag::accepts($this->enumType, $value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum\Bridge\Symfony\Form\DataTransformer;

use Elao\Enum\FlagBag;
use Symfony\Component\Form\Exception\TransformationFailedException;

class FlagBagToCollectionTransformer extends AbstractFlagBagTransformer
{
/**
* Transforms a FlagBag objects to an array of BackedEnum
*
* @param FlagBag $value A FlagBag instance
*
* @throws TransformationFailedException When the transformation fails
*
* @return \BackedEnum[]|null An array of FlagBag instances with single bit flag
*/
public function transform($value): ?array
{
if ($value === null) {
return null;
}

if (!$value instanceof FlagBag) {
throw new TransformationFailedException(sprintf(
'Expected instance of "%s". Got "%s".',
FlagBag::class,
get_debug_type($value)
));
}

if ($value->getType() !== $this->enumType) {
throw new TransformationFailedException(sprintf(
'Expected FlagBag instance of "%s" values. Got FlagBag instance of "%s" values.',
$this->enumType,
$value->getType()
));
}

return $value->getFlags();
}

/**
* Transforms an array of BackedEnum to single FlagBag instance.
*
* @param \BackedEnum[] $values An array of flag enums
*
* @throws TransformationFailedException When the transformation fails
*
* @return FlagBag|null A single FlagBag instance or null
*/
public function reverseTransform($values): ?FlagBag
{
if (null === $values) {
return null;
}

if (!\is_array($values)) {
throw new TransformationFailedException(sprintf(
'Expected array. Got "%s".',
get_debug_type($values)
));
}

if (0 === \count($values)) {
return $this->createEnum(FlagBag::NONE);
}

foreach ($values as $value) {
if (!$value instanceof $this->enumType) {
throw new TransformationFailedException(sprintf(
'Expected array of "%s". Got a "%s" inside.',
$this->enumType,
get_debug_type($value)
));
}
}

return $this->createEnum($values);
}
}
55 changes: 55 additions & 0 deletions src/Bridge/Symfony/Form/Type/FlagBagType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum\Bridge\Symfony\Form\Type;

use Elao\Enum\Bridge\Symfony\Form\DataTransformer\FlagBagToCollectionTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* @final
*/
class FlagBagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['multiple']) {
throw new InvalidConfigurationException(sprintf(
'The "multiple" option of the "%s" form type cannot be set to false.',
static::class
));
}

$builder->addModelTransformer(new FlagBagToCollectionTransformer($options['class']));
}

/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('multiple', true)
;
}

/**
* {@inheritdoc}
*/
public function getParent(): ?string
{
return EnumType::class;
}
}
26 changes: 18 additions & 8 deletions src/FlagBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,7 @@ class FlagBag
*/
public function __construct(string $enumType, int ...$bits)
{
if (!is_a($enumType, \BackedEnum::class, true)) {
throw new InvalidArgumentException(sprintf('"%s" is not a backed enum', $enumType));
}

if ('int' !== (string) (new \ReflectionEnum($enumType))->getBackingType()) {
throw new InvalidArgumentException(sprintf('"%s" is not an int backed enum', $enumType));
}
self::checkIntBackedEnumType($enumType);

$this->type = $enumType;

Expand All @@ -57,6 +51,20 @@ public function __construct(string $enumType, int ...$bits)
$this->bits = static::decodeBits($bits);
}

/**
* @param class-string $enumType
*/
private static function checkIntBackedEnumType(string $enumType): void
{
if (!is_a($enumType, \BackedEnum::class, true)) {
throw new InvalidArgumentException(sprintf('"%s" is not a backed enum', $enumType));
}

if ('int' !== (string) (new \ReflectionEnum($enumType))->getBackingType()) {
throw new InvalidArgumentException(sprintf('"%s" is not an int backed enum', $enumType));
}
}

/**
* @param class-string<\BackedEnum> $enumType
*/
Expand Down Expand Up @@ -138,9 +146,11 @@ private static function encodeBits(array $bits): int
*
* @throws LogicException If the possibles values are not valid bit flags
*/
private static function getBitmask(string $enumType): int
public static function getBitmask(string $enumType): int
{
if (!isset(self::$masks[$enumType])) {
self::checkIntBackedEnumType($enumType);

/** @var \BackedEnum[] $cases */
$cases = $enumType::cases();
$mask = 0;
Expand Down
29 changes: 29 additions & 0 deletions tests/Fixtures/Enum/PermissionsReadable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum\Tests\Fixtures\Enum;

use Elao\Enum\Attribute\EnumCase;
use Elao\Enum\ReadableEnumInterface;
use Elao\Enum\ReadableEnumTrait;

enum PermissionsReadable: int implements ReadableEnumInterface
{
use ReadableEnumTrait;

#[EnumCase('Execute')]
case Execute = 1 << 0;
#[EnumCase('Write')]
case Write = 1 << 1;
#[EnumCase('Read')]
case Read = 1 << 2;
}
Loading

0 comments on commit 7600213

Please sign in to comment.