Skip to content
Closed
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
5 changes: 5 additions & 0 deletions src/Dto/FieldDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -505,13 +505,18 @@ public function setCustomOption(string $optionName, mixed $optionValue): void
$this->customOptions->set($optionName, $optionValue);
}

/**
* @deprecated since 4.27 and to be removed in 5.0, use $entityDto->getClassMetadata() instead
*/
public function getDoctrineMetadata(): KeyValueStore
{
return $this->doctrineMetadata;
}

/**
* @param array<string, mixed> $metadata
*
* @deprecated since 4.27 and to be removed in 5.0 without replacement
*/
public function setDoctrineMetadata(array $metadata): void
{
Expand Down
5 changes: 4 additions & 1 deletion src/Field/Configurator/ChoiceConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c

// if no choices are passed to the field, check if it's related to an Enum;
// in that case, get all the possible values of the Enum (Doctrine supports only BackedEnum as enumType)
$enumTypeClass = $field->getDoctrineMetadata()->get('enumType');
$enumTypeClass = null;
if (isset($entityDto->getClassMetadata()->fieldMappings[$field->getProperty()])) {
$enumTypeClass = $entityDto->getClassMetadata()->getFieldMapping($field->getProperty())['enumType'] ?? null;
}
if (0 === \count($choices) && null !== $enumTypeClass && enum_exists($enumTypeClass)) {
$choices = $enumTypeClass::cases();
$allChoicesAreEnums = true;
Expand Down
12 changes: 8 additions & 4 deletions src/Field/Configurator/CollectionConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,19 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
$field->setCustomOption(CollectionField::OPTION_ENTRY_IS_COMPLEX, $isComplexEntry);
}

$field->setFormattedValue($this->formatCollection($field, $context));
$field->setFormattedValue($this->formatCollection($field, $entityDto, $context));

$this->configureEntryType($field, $entityDto, $context);
}

private function formatCollection(FieldDto $field, AdminContext $context): int|string
private function formatCollection(FieldDto $field, EntityDto $entityDto, AdminContext $context): int|string
{
$doctrineMetadata = $field->getDoctrineMetadata();
if ('array' !== $doctrineMetadata->get('type') && !$field->getValue() instanceof PersistentCollection) {
$doctrineType = null;
if (isset($entityDto->getClassMetadata()->fieldMappings[$field->getProperty()])) {
$doctrineType = $entityDto->getClassMetadata()->getFieldMapping($field->getProperty())['type'];
}

if ('array' !== $doctrineType && !$field->getValue() instanceof PersistentCollection) {
return $this->countNumElements($field->getValue());
}

Expand Down
7 changes: 6 additions & 1 deletion src/Filter/ArrayFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto,
$parameterName = $filterDataDto->getParameterName();
$value = $filterDataDto->getValue();

$useQuotes = Types::SIMPLE_ARRAY === $fieldDto->getDoctrineMetadata()->get('type');
$doctrineType = null;
if (isset($entityDto->getClassMetadata()->fieldMappings[$fieldDto->getProperty()])) {
$doctrineType = $entityDto->getClassMetadata()->getFieldMapping($fieldDto->getProperty())['type'];
}

$useQuotes = Types::SIMPLE_ARRAY === $doctrineType;

if (null === $value || [] === $value) {
$queryBuilder->andWhere(sprintf('%s.%s %s', $alias, $property, $comparison));
Expand Down
4 changes: 2 additions & 2 deletions tests/Field/Configurator/AssociationConfiguratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,15 @@ public static function failsOnOptionRenderAsEmbeddedCrudFormIfPropertyIsCollecti
*/
public function testFailsOnOptionRenderAsEmbeddedCrudFormIfNoCrudControllerCanBeFound(FieldInterface $field): void
{
$field->getAsDto()->setDoctrineMetadata((array) $this->projectDto->getClassMetadata()->getAssociationMapping($field->getAsDto()->getProperty()));
$field->getAsDto()->setDoctrineMetadata($associationMapping = (array) $this->projectDto->getClassMetadata()->getAssociationMapping($field->getAsDto()->getProperty()));
$field->setCustomOption(AssociationField::OPTION_RENDER_AS_EMBEDDED_FORM, true);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage(sprintf(
'The "%s" association field of "%s" wants to render its contents using an EasyAdmin CRUD form. However, no CRUD form was found related to this field. You can either create a CRUD controller for the entity "%s" or pass the CRUD controller to use as the first argument of the "renderAsEmbeddedForm()" method.',
$field->getAsDto()->getProperty(),
ProjectCrudController::class,
$field->getAsDto()->getDoctrineMetadata()->get('targetEntity'),
$associationMapping['targetEntity'],
));

$this->configure($field, controllerFqcn: ProjectCrudController::class);
Expand Down
147 changes: 79 additions & 68 deletions tests/Field/Configurator/ChoiceConfiguratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,115 +2,126 @@

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Field\Configurator;

use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\ChoiceConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Tests\Field\AbstractFieldTest;
use EasyCorp\Bundle\EasyAdminBundle\Tests\Field\Fixtures\ChoiceField\PriorityUnitEnum;
use EasyCorp\Bundle\EasyAdminBundle\Tests\Field\Fixtures\ChoiceField\StatusBackedEnum;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\ProjectDomain\Project;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Model\Priority;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Model\Status;

class ChoiceConfiguratorTest extends AbstractFieldTest
{
private const ENTITY_CLASS = 'AppTestBundle\Entity\UnitTests\Category';
private const PROPERTY_NAME = 'foo';

private ?EntityDto $entity = null;
private EntityDto $projectDto;

protected function setUp(): void
{
parent::setUp();
/** @var EntityManagerInterface $entityManager */
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
$this->projectDto = new EntityDto(Project::class, $entityManager->getClassMetadata(Project::class));

$this->configurator = new ChoiceConfigurator();
}

$metadata = new ClassMetadata(self::ENTITY_CLASS);
$metadata->setIdentifier(['id']);
$this->entity = new EntityDto(self::ENTITY_CLASS, $metadata);
protected function getEntityDto(): EntityDto
{
return $this->projectDto;
}

/**
* @dataProvider fieldTypes
* @dataProvider fieldFqcns
*/
public function testSupportsField(string $fieldType, bool $expectedResult): void
public function testSupports(string $fieldFqcn, bool $expectedIsSupported): void
{
$field = new FieldDto();
$field->setFieldFqcn($fieldType);
$field->setFieldFqcn($fieldFqcn);

$this->assertSame($this->configurator->supports($field, $this->entity), $expectedResult);
$this->assertSame($expectedIsSupported, $this->configurator->supports($field, $this->projectDto));
}

public function testBackedEnumTypeChoices(): void
public static function fieldFqcns(): iterable
{
$field = ChoiceField::new(self::PROPERTY_NAME);
$field->getAsDto()->setDoctrineMetadata(['enumType' => StatusBackedEnum::class]);

$formChoices = array_combine(
array_column(StatusBackedEnum::cases(), 'name'),
StatusBackedEnum::cases(),
);

$this->assertSame($this->configure($field)->getFormTypeOption('choices'), $formChoices);
yield [ChoiceField::class, true];
yield [TextField::class, false];
yield [IdField::class, false];
}

public function testBackedEnumChoices(): void
public function testBackedEnum(): void
{
$field = ChoiceField::new(self::PROPERTY_NAME);
$field->setCustomOptions(['choices' => StatusBackedEnum::cases()]);

$expected = [];
foreach (StatusBackedEnum::cases() as $case) {
$expected[$case->name] = $case;
}

$this->assertSame($this->configure($field)->getFormTypeOption('choices'), $expected);
$field = ChoiceField::new('status');

$this->assertSame(
[
'Draft' => Status::Draft,
'Published' => Status::Published,
'Deleted' => Status::Deleted,
],
$this->configure($field)->getFormTypeOption('choices'),
);
}

public function testUnitEnumTypeChoices(): void
public function testBackedEnumWithCustomOptions(): void
{
$field = ChoiceField::new(self::PROPERTY_NAME);
$field->getAsDto()->setDoctrineMetadata(['enumType' => PriorityUnitEnum::class]);

$formChoices = array_combine(
array_column(PriorityUnitEnum::cases(), 'name'),
PriorityUnitEnum::cases(),
$field = ChoiceField::new('status')->setCustomOptions(['choices' => Status::cases()]);

$this->assertSame(
[
'Draft' => Status::Draft,
'Published' => Status::Published,
'Deleted' => Status::Deleted,
],
$this->configure($field)->getFormTypeOption('choices'),
);

$this->assertSame($this->configure($field)->getFormTypeOption('choices'), $formChoices);
}

public function testUnitEnumChoices(): void
public function testBackedEnumLabels(): void
{
$field = ChoiceField::new(self::PROPERTY_NAME);
$field->setCustomOptions(['choices' => PriorityUnitEnum::cases()]);

$expected = [];
foreach (PriorityUnitEnum::cases() as $case) {
$expected[$case->name] = $case;
}

$this->assertSame($this->configure($field)->getFormTypeOption('choices'), $expected);
$field = ChoiceField::new('status')->setCustomOptions(['choices' => [
'Draft label' => Status::Draft,
'Published label' => Status::Published,
'Deleted label' => Status::Deleted,
]]);

$this->assertSame(
[
'Draft label' => Status::Draft,
'Published label' => Status::Published,
'Deleted label' => Status::Deleted,
],
$this->configure($field)->getFormTypeOption('choices'),
);
}

public static function fieldTypes(): iterable
public function testUnitEnum(): void
{
yield [ChoiceField::class, true];
yield [TextField::class, false];
yield [IdField::class, false];
$field = ChoiceField::new('priority');

$this->assertSame(
[
'High' => Priority::High,
'Normal' => Priority::Normal,
'Low' => Priority::Low,
],
$this->configure($field)->getFormTypeOption('choices'),
);
}

public function testBackedEnumChoicesLabeled(): void
public function testUnitEnumChoicesWithCustomOptions(): void
{
$choices = [];
foreach (StatusBackedEnum::cases() as $case) {
$choices[$case->label()] = $case;
}

$field = ChoiceField::new(self::PROPERTY_NAME);
$field->setCustomOptions(['choices' => $choices]);

$this->assertSame($choices, $this->configure($field)->getFormTypeOption('choices'));
$field = ChoiceField::new('priority');
$field->setCustomOptions(['choices' => Priority::cases()]);

$this->assertSame(
[
'High' => Priority::High,
'Normal' => Priority::Normal,
'Low' => Priority::Low,
],
$this->configure($field)->getFormTypeOption('choices'),
);
}
}
4 changes: 2 additions & 2 deletions tests/Field/Configurator/CollectionConfiguratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ public static function failsOnOptionEntryUsesCrudFormIfOptionEntryTypeIsUsed():
*/
public function testFailsOnOptionRenderAsEmbeddedCrudFormIfNoCrudControllerCanBeFound(FieldInterface $field): void
{
$field->getAsDto()->setDoctrineMetadata((array) $this->projectDto->getClassMetadata()->getAssociationMapping($field->getAsDto()->getProperty()));
$field->getAsDto()->setDoctrineMetadata($associationMapping = (array) $this->projectDto->getClassMetadata()->getAssociationMapping($field->getAsDto()->getProperty()));
$field->setCustomOption(CollectionField::OPTION_ENTRY_USES_CRUD_FORM, true);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage(sprintf('The "%s" collection field of "%s" wants to render its entries using an EasyAdmin CRUD form. However, no CRUD form was found related to this field. You can either create a CRUD controller for the entity "%s" or pass the CRUD controller to use as the first argument of the "useEntryCrudForm()" method.',
$field->getAsDto()->getProperty(),
ProjectCrudController::class,
$field->getAsDto()->getDoctrineMetadata()->get('targetEntity'),
$associationMapping['targetEntity'],
));

$this->configure($field, controllerFqcn: ProjectCrudController::class);
Expand Down
12 changes: 10 additions & 2 deletions tests/TestApplication/src/Entity/ProjectDomain/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Model\Priority;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Model\Status;

#[ORM\Entity]
class Project implements \Stringable
Expand All @@ -30,13 +32,13 @@ class Project implements \Stringable
/**
* @var Collection<int, ProjectIssue>
*/
#[ORM\OneToMany(targetEntity: ProjectIssue::class, mappedBy: 'project', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectIssue::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection $projectIssues;

/**
* @var Collection<int, Developer>
*/
#[ORM\OneToMany(targetEntity: Developer::class, mappedBy: 'favouriteProject')]
#[ORM\OneToMany(mappedBy: 'favouriteProject', targetEntity: Developer::class)]
private Collection $favouriteProjectOf;

/**
Expand Down Expand Up @@ -96,6 +98,12 @@ class Project implements \Stringable
#[ORM\Column(type: Types::TIME_IMMUTABLE)]
private ?\DateTimeImmutable $startTimeImmutable = null;

#[ORM\Column(type: Types::STRING, enumType: Status::class)]
private Status $status;

#[ORM\Column(type: Types::STRING, enumType: Priority::class)]
private Status $priority;

public function __construct()
{
$this->price = (new Money())->setAmount(0)->setCurrency('EUR');
Expand Down
10 changes: 10 additions & 0 deletions tests/TestApplication/src/Model/Priority.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Model;

enum Priority
{
case High;
case Normal;
case Low;
}
19 changes: 19 additions & 0 deletions tests/TestApplication/src/Model/Status.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Model;

enum Status: string
{
case Draft = 'draft';
case Published = 'published';
case Deleted = 'deleted';

public function label(): string
{
return match ($this) {
self::Draft => 'Draft label',
self::Published => 'Published label',
self::Deleted => 'Deleted label',
};
}
}