*/
private function getSearchablePropertiesConfig(QueryBuilder $queryBuilder, SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields): array
{
$searchablePropertiesConfig = [];
$configuredSearchableProperties = $searchDto->getSearchableProperties();
- $searchableProperties = $configuredSearchableProperties === null || count($configuredSearchableProperties) === 0 ? $entityDto->getAllPropertyNames() : $configuredSearchableProperties;
+ $searchableProperties = $configuredSearchableProperties === null || count($configuredSearchableProperties) === 0 ? $entityDto->getClassMetadata()->getFieldNames() : $configuredSearchableProperties;
$entitiesAlreadyJoined = [];
foreach ($searchableProperties as $propertyName) {
- if ($entityDto->isAssociation($propertyName)) {
+ if ($this->isAssociation($entityDto, $propertyName)) {
// support arbitrarily nested associations (e.g. foo.bar.baz.qux)
$associatedProperties = explode('.', $propertyName);
$numAssociatedProperties = count($associatedProperties);
@@ -282,16 +303,15 @@ private function getSearchablePropertiesConfig(QueryBuilder $queryBuilder, Searc
throw new InvalidArgumentException(sprintf('The "%s" property included in the setSearchFields() method is not a valid search field. When using associated properties in search, you must also define the exact field used in the search (e.g. \'%s.id\', \'%s.name\', etc.)', $propertyName, $propertyName, $propertyName));
}
- $originalPropertyName = $associatedProperties[0];
- $originalPropertyMetadata = $entityDto->getPropertyMetadata($originalPropertyName);
+ $originalPropertyName = $associatedProperties[0];
$field = $fields->getByProperty($originalPropertyName);
if ($field !== null && $field->getFieldFqcn() === EntityField::class) {
$targetCrudControllerFqcn = Type\string()->coerce($field->getCustomOption(EntityField::OPTION_CRUD_CONTROLLER));
- $targetEntityFqcn = $this->crudControllerRegistry->findEntityFqcnByCrudFqcn($targetCrudControllerFqcn);
+ $targetEntityFqcn = Type\nonnull()->coerce($this->crudControllerRegistry->findEntityFqcnByCrudFqcn($targetCrudControllerFqcn));
} else {
- $targetEntityFqcn = $originalPropertyMetadata->get('targetEntity');
+ $targetEntityFqcn = $entityDto->getClassMetadata()->getAssociationTargetClass($associatedProperties[0]);
}
$associatedEntityDto = $this->entityFactory->create($targetEntityFqcn);
@@ -299,14 +319,14 @@ private function getSearchablePropertiesConfig(QueryBuilder $queryBuilder, Searc
$associatedEntityAlias = $associatedPropertyName = '';
for ($i = 0; $i < $numAssociatedProperties - 1; ++$i) {
$associatedEntityName = $associatedProperties[$i];
- $associatedEntityAlias = Escaper::escapeDqlAlias($associatedEntityName);
+ $associatedEntityAlias = $entitiesAlreadyJoined[$associatedEntityName] ?? Escaper::escapeDqlAlias($associatedEntityName) . ($i === 0 ? '' : $i);
$associatedPropertyName = $associatedProperties[$i + 1];
- if (! in_array($associatedEntityName, $entitiesAlreadyJoined, true)) {
- $parentEntityName = $i === 0 ? 'entity' : $associatedProperties[$i - 1];
+ if (! in_array($associatedEntityAlias, $entitiesAlreadyJoined, true)) {
+ $parentEntityName = $i === 0 ? 'entity' : $entitiesAlreadyJoined[$associatedProperties[$i - 1]];
if ($field !== null && $field->getFieldFqcn() === EntityField::class) {
- $associatedEntityPrimaryKeyName = Type\string()->coerce($associatedEntityDto->getPrimaryKeyName());
+ $associatedEntityPrimaryKeyName = Type\string()->coerce($associatedEntityDto->getClassMetadata()->getSingleIdentifierFieldName());
$queryBuilder->leftJoin(
$targetEntityFqcn,
@@ -324,24 +344,51 @@ private function getSearchablePropertiesConfig(QueryBuilder $queryBuilder, Searc
$queryBuilder->leftJoin(Escaper::escapeDqlAlias($parentEntityName) . '.' . $associatedEntityName, $associatedEntityAlias);
}
- $entitiesAlreadyJoined[] = $associatedEntityName;
+ $entitiesAlreadyJoined[$associatedEntityName] = $associatedEntityAlias;
}
if ($i >= $numAssociatedProperties - 2) {
continue;
}
- $propertyMetadata = $associatedEntityDto->getPropertyMetadata($associatedPropertyName);
- $targetEntity = $propertyMetadata->get('targetEntity');
+ $targetEntity = $associatedEntityDto->getClassMetadata()->getAssociationTargetClass($associatedPropertyName);
$associatedEntityDto = $this->entityFactory->create($targetEntity);
}
- $entityName = $associatedEntityAlias;
- $propertyName = $associatedPropertyName;
- $propertyDataType = $associatedEntityDto->getPropertyDataType($propertyName);
+ $entityName = $associatedEntityAlias;
+ $propertyName = $associatedPropertyName;
+ if (! isset($associatedEntityDto->getClassMetadata()->fieldMappings[$propertyName])) {
+ throw new InvalidArgumentException(sprintf('The "%s" property included in the setSearchFields() method is not a valid search field. When using associated properties in search, you must also define the exact field used in the search (e.g. \'%s.id\', \'%s.name\', etc.)', $propertyName, $propertyName, $propertyName));
+ }
+
+ // In Doctrine ORM 3.x, FieldMapping implements \ArrayAccess; in 4.x it's an object with properties
+ $fieldMapping = $associatedEntityDto->getClassMetadata()->getFieldMapping($propertyName);
+ // In Doctrine ORM 2.x, getFieldMapping() returns an array
+ /** @phpstan-ignore-next-line function.impossibleType */
+ if (is_array($fieldMapping)) {
+ /** @phpstan-ignore-next-line cast.useless */
+ $fieldMapping = (object) $fieldMapping;
+ }
+
+ /** @phpstan-ignore-next-line function.alreadyNarrowedType */
+ $propertyDataType = property_exists($fieldMapping, 'type') ? $fieldMapping->type : $fieldMapping['type'];
} else {
- $entityName = 'entity';
- $propertyDataType = $entityDto->getPropertyDataType($propertyName);
+ $entityName = 'entity';
+ if (! isset($entityDto->getClassMetadata()->fieldMappings[$propertyName])) {
+ throw new InvalidArgumentException(sprintf('The "%s" property included in the setSearchFields() method is not a valid search field. When using associated properties in search, you must also define the exact field used in the search (e.g. \'%s.id\', \'%s.name\', etc.)', $propertyName, $propertyName, $propertyName));
+ }
+
+ // In Doctrine ORM 3.x, FieldMapping implements \ArrayAccess; in 4.x it's an object with properties
+ $fieldMapping = $entityDto->getClassMetadata()->getFieldMapping($propertyName);
+ // In Doctrine ORM 2.x, getFieldMapping() returns an array
+ /** @phpstan-ignore-next-line function.impossibleType */
+ if (is_array($fieldMapping)) {
+ /** @phpstan-ignore-next-line cast.useless */
+ $fieldMapping = (object) $fieldMapping;
+ }
+
+ /** @phpstan-ignore-next-line function.alreadyNarrowedType */
+ $propertyDataType = property_exists($fieldMapping, 'type') ? $fieldMapping->type : $fieldMapping['type'];
}
$isBoolean = $propertyDataType === 'boolean';
@@ -349,7 +396,7 @@ private function getSearchablePropertiesConfig(QueryBuilder $queryBuilder, Searc
$isIntegerProperty = $propertyDataType === 'integer';
$isNumericProperty = in_array($propertyDataType, ['number', 'bigint', 'decimal', 'float'], true);
// 'citext' is a PostgreSQL extension (https://github.com/EasyCorp/EasyAdminBundle/issues/2556)
- $isTextProperty = in_array($propertyDataType, ['string', 'text', 'citext', 'array', 'simple_array'], true);
+ $isTextProperty = in_array($propertyDataType, ['ascii_string', 'string', 'text', 'citext', 'array', 'simple_array'], true);
$isGuidProperty = in_array($propertyDataType, ['guid', 'uuid'], true);
$isUlidProperty = $propertyDataType === 'ulid';
$isJsonProperty = $propertyDataType === 'json';
@@ -364,11 +411,24 @@ private function getSearchablePropertiesConfig(QueryBuilder $queryBuilder, Searc
&& ! $isUlidProperty
&& ! $isJsonProperty
) {
- $entityFqcn = $entityName !== 'entity' && isset($associatedEntityDto)
+ $entityFqcn = $entityName !== 'entity' && isset($associatedEntityDto)
? $associatedEntityDto->getFqcn()
: $entityDto->getFqcn();
- $idClassType = (new ReflectionProperty($entityFqcn, $propertyName))->getType();
- assert($idClassType instanceof ReflectionNamedType || $idClassType instanceof ReflectionUnionType || $idClassType === null);
+
+ /** @var ReflectionNamedType|ReflectionUnionType|null $idClassType */
+ $idClassType = null;
+ $reflectionClass = new ReflectionClass($entityFqcn);
+
+ // this is needed to handle inherited properties
+ while ($reflectionClass !== false) {
+ if ($reflectionClass->hasProperty($propertyName)) {
+ $reflection = $reflectionClass->getProperty($propertyName);
+ $idClassType = $reflection->getType();
+ break;
+ }
+
+ $reflectionClass = $reflectionClass->getParentClass();
+ }
if ($idClassType !== null) {
$idClassName = $idClassType->getName();
@@ -397,4 +457,19 @@ private function getSearchablePropertiesConfig(QueryBuilder $queryBuilder, Searc
return $searchablePropertiesConfig;
}
+
+ private function isAssociation(EntityDto $entityDto, string $propertyName): bool
+ {
+ if ($entityDto->getClassMetadata()->hasAssociation($propertyName)) {
+ return true;
+ }
+
+ if (! str_contains($propertyName, '.')) {
+ return false;
+ }
+
+ $propertyNameParts = explode('.', $propertyName, 2);
+
+ return ! isset($entityDto->getClassMetadata()->embeddedClasses[$propertyNameParts[0]]);
+ }
}
diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php
index 9b90e76..1a07ec4 100644
--- a/src/Resources/config/services.php
+++ b/src/Resources/config/services.php
@@ -7,10 +7,13 @@
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Menu\MenuItemMatcherInterface;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityPaginatorInterface;
+use EasyCorp\Bundle\EasyAdminBundle\Contracts\Provider\AdminContextProviderInterface;
use EasyCorp\Bundle\EasyAdminBundle\DependencyInjection\EasyAdminExtension;
use EasyCorp\Bundle\EasyAdminBundle\Menu\MenuItemMatcher;
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository as EasyAdminEntityRepository;
+use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
+use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGeneratorInterface;
use Protung\EasyAdminPlusBundle\Field\Configurator\CallbackConfigurableConfigurator;
use Protung\EasyAdminPlusBundle\Field\Configurator\CallbackConfigurableConfiguratorAfterCommonPostConfigurator;
use Protung\EasyAdminPlusBundle\Field\Configurator\CallbackConfigurableConfiguratorBeforeCommonPreConfigurator;
@@ -31,6 +34,9 @@
->load('Protung\\EasyAdminPlusBundle\\', '../../../src/*')
->exclude(['../../../src/Resources/**/*', '../../../src/Test/**/*']);
+ $services->alias(AdminContextProviderInterface::class, AdminContextProvider::class);
+ $services->alias(AdminUrlGeneratorInterface::class, AdminUrlGenerator::class);
+
$services->set(EntityRepository::class)
->decorate(EasyAdminEntityRepository::class)
->autoconfigure()
@@ -44,13 +50,11 @@
$services->set(EntityPaginator::class)
->decorate(EntityPaginatorInterface::class)
- ->arg('$adminUrlGenerator', service(AdminUrlGenerator::class))
->autowire()
->autoconfigure()
->private();
$services->set(AutocompleteActionAdminUrlGenerator::class)
- ->arg('$adminUrlGenerator', service(AdminUrlGenerator::class))
->autowire()
->autoconfigure()
->private();
diff --git a/src/Resources/views/crud/index.html.twig b/src/Resources/views/crud/index.html.twig
deleted file mode 100644
index 123227c..0000000
--- a/src/Resources/views/crud/index.html.twig
+++ /dev/null
@@ -1,38 +0,0 @@
-{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
-{% extends '@EasyAdmin/crud/index.html.twig' %}
-
-{% block entity_actions %}
- {% if ea.crud.showEntityActionsAsDropdown %}
- {% set dropdownActions = entity.actions|filter(action => action.getHtmlAttributes['data-protung-easyadmin-plus-extension-action-render-in-dropdown'] | default(1) == 1) %}
- {% set notDropdownActions = entity.actions|filter(action => action.getHtmlAttributes['data-protung-easyadmin-plus-extension-action-render-in-dropdown'] | default(1) == -1) %}
- |
- {% for action in notDropdownActions %}
-
- {{ include(action.templatePath, { action: action, entity: entity, isIncludedInDropdown: false }, with_context = false) }}
-
- {% endfor %}
- {% if dropdownActions|length > 0 %}
-
- {% endif %}
- |
- {% else %}
- {{ parent() }}
- {% endif %}
-{% endblock entity_actions %}
diff --git a/src/Test/Controller/IndexActionTestCase.php b/src/Test/Controller/IndexActionTestCase.php
index 2335718..b7c65eb 100644
--- a/src/Test/Controller/IndexActionTestCase.php
+++ b/src/Test/Controller/IndexActionTestCase.php
@@ -163,8 +163,13 @@ protected function extractDatagridHeaders(): array
*/
protected function extractGlobalActions(): array
{
+ $globalActions = $this->getClient()->getCrawler()->filter('.global-actions');
+ if ($globalActions->count() === 0) {
+ return [];
+ }
+
return $this->mapActions(
- $this->getClient()->getCrawler()->filter('.global-actions')->filter('[data-action-name]'),
+ $globalActions->children('[data-action-group-name], [data-action-name]'),
);
}
diff --git a/tests/Test/TestApplication/TestKernel.php b/tests/Test/TestApplication/TestKernel.php
index f01f757..0094932 100644
--- a/tests/Test/TestApplication/TestKernel.php
+++ b/tests/Test/TestApplication/TestKernel.php
@@ -90,7 +90,6 @@ public function configureContainer(ContainerConfigurator $container): void
'path' => '%kernel.cache_dir%/test_database.sqlite',
],
'orm' => [
- 'auto_generate_proxy_classes' => true,
'naming_strategy' => 'doctrine.orm.naming_strategy.underscore_number_aware',
'auto_mapping' => true,
],