diff --git a/CHANGELOG.md b/CHANGELOG.md index 317c56d..b174142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#77](https://github.com/itk-dev/devops_itksites/pull/77) Fix SemverFilter: respect value2 with directional operators - +- [#76](https://github.com/itk-dev/devops_itksites/pull/76) + Add server type filter and sort on Installation, Site, Domain - [#75](https://github.com/itk-dev/devops_itksites/pull/75) Add semver-aware filter on every admin version column, make version column semver sortable diff --git a/src/Controller/Admin/DomainCrudController.php b/src/Controller/Admin/DomainCrudController.php index 060e8ef..d1a98ab 100644 --- a/src/Controller/Admin/DomainCrudController.php +++ b/src/Controller/Admin/DomainCrudController.php @@ -8,6 +8,7 @@ use App\Admin\Field\ServerTypeField; use App\Admin\Field\SiteTypeField; use App\Entity\Domain; +use App\Form\Type\Admin\ServerTypeFilter; use App\Trait\ExportCrudControllerTrait; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; @@ -52,7 +53,7 @@ public function configureFields(string $pageName): iterable yield DomainField::new('address')->setColumns(12); yield SiteTypeField::new('site.type')->hideOnIndex(); yield AssociationField::new('site')->hideOnIndex(); - yield ServerTypeField::new('server.type')->setLabel('Type'); + yield ServerTypeField::new('server.type')->setLabel('Type')->setSortable(true); yield AssociationField::new('server'); yield AssociationField::new('detectionResult')->hideOnIndex(); yield DateTimeField::new('createdAt')->hideOnIndex(); @@ -66,6 +67,7 @@ public function configureFilters(Filters $filters): Filters ->add('address') ->add('site') ->add('server') + ->add(ServerTypeFilter::new('server.type', 'Server type')) ; } } diff --git a/src/Controller/Admin/InstallationCrudController.php b/src/Controller/Admin/InstallationCrudController.php index beb5c0e..cad1a95 100644 --- a/src/Controller/Admin/InstallationCrudController.php +++ b/src/Controller/Admin/InstallationCrudController.php @@ -12,6 +12,7 @@ use App\Entity\Installation; use App\Form\Type\Admin\FrameworkFilter; use App\Form\Type\Admin\SemverFilter; +use App\Form\Type\Admin\ServerTypeFilter; use App\Trait\ExportCrudControllerTrait; use App\Trait\SemverSortableCrudControllerTrait; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; @@ -68,7 +69,7 @@ public function configureFields(string $pageName): iterable yield CodeEditorField::new('gitChanges')->hideOnIndex(); yield AssociationField::new('sites')->hideOnIndex(); yield RootDirField::new('rootDir')->setColumns(12); - yield ServerTypeField::new('server.type')->setLabel('Type'); + yield ServerTypeField::new('server.type')->setLabel('Type')->setSortable(true); yield AssociationField::new('server'); yield AssociationField::new('detectionResult')->hideOnIndex(); yield DateTimeField::new('createdAt')->hideOnIndex(); @@ -86,6 +87,7 @@ public function configureFilters(Filters $filters): Filters ->add(SemverFilter::new('composerVersion', 'Comp.')) ->add('rootDir') ->add('server') + ->add(ServerTypeFilter::new('server.type', 'Server type')) // ->add(SystemFilter::new('system')->mapped(false)) ; } diff --git a/src/Controller/Admin/SiteCrudController.php b/src/Controller/Admin/SiteCrudController.php index aabaa03..4d49bb4 100644 --- a/src/Controller/Admin/SiteCrudController.php +++ b/src/Controller/Admin/SiteCrudController.php @@ -13,6 +13,7 @@ use App\Admin\Field\VersionField; use App\Entity\Site; use App\Form\Type\Admin\SemverFilter; +use App\Form\Type\Admin\ServerTypeFilter; use App\Trait\ExportCrudControllerTrait; use App\Trait\SemverSortableCrudControllerTrait; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; @@ -67,7 +68,7 @@ public function configureFields(string $pageName): iterable yield RootDirField::new('rootDir')->setColumns(12)->hideOnIndex(); yield VersionField::new('phpVersion')->setLabel('PHP'); yield AssociationField::new('installation')->hideOnIndex(); - yield ServerTypeField::new('server.type')->setLabel('Type'); + yield ServerTypeField::new('server.type')->setLabel('Type')->setSortable(true); yield AssociationField::new('server'); yield AssociationField::new('detectionResult')->hideOnIndex(); yield DateTimeField::new('createdAt')->hideOnIndex(); @@ -80,7 +81,8 @@ public function configureFilters(Filters $filters): Filters ->add('primaryDomain') ->add('configFilePath') ->add(SemverFilter::new('phpVersion', 'PHP')) - ->add('server'); + ->add('server') + ->add(ServerTypeFilter::new('server.type', 'Server type')); } #[\Override] diff --git a/src/Form/Type/Admin/ServerTypeFilter.php b/src/Form/Type/Admin/ServerTypeFilter.php index 502dd91..9c78a0d 100644 --- a/src/Form/Type/Admin/ServerTypeFilter.php +++ b/src/Form/Type/Admin/ServerTypeFilter.php @@ -27,7 +27,32 @@ public static function new(string $propertyName, false|string|TranslatableInterf public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void { - $queryBuilder->andWhere(sprintf('%s.%s = :type', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty())) - ->setParameter('type', $filterDataDto->getValue()); + $rootAlias = $filterDataDto->getEntityAlias(); + $property = $filterDataDto->getProperty(); + $parameter = $filterDataDto->getParameterName(); + $value = $filterDataDto->getValue(); + + // EasyAdmin doesn't auto-join nested-property filters; if the + // property crosses a relation (e.g. "server.type"), join it + // ourselves and reference the joined alias. We reuse the plain + // relation name as the join alias to match EA's own sort-side + // auto-join (see EntityRepository::addOrderClause), so combining + // sort + filter on the same association produces a single JOIN. + // Background: https://github.com/EasyCorp/EasyAdminBundle/issues/4120. + if (str_contains($property, '.')) { + [$relation, $leafProperty] = explode('.', $property, 2); + if (!in_array($relation, $queryBuilder->getAllAliases(), true)) { + $queryBuilder->leftJoin($rootAlias.'.'.$relation, $relation); + } + $queryBuilder + ->andWhere(sprintf('%s.%s = :%s', $relation, $leafProperty, $parameter)) + ->setParameter($parameter, $value); + + return; + } + + $queryBuilder + ->andWhere(sprintf('%s.%s = :%s', $rootAlias, $property, $parameter)) + ->setParameter($parameter, $value); } } diff --git a/tests/Form/Type/Admin/ServerTypeFilterTest.php b/tests/Form/Type/Admin/ServerTypeFilterTest.php new file mode 100644 index 0000000..8efde63 --- /dev/null +++ b/tests/Form/Type/Admin/ServerTypeFilterTest.php @@ -0,0 +1,81 @@ +makeInstallationQueryBuilder(); + + $this->apply($qb, 'type', 'prod'); + + $dql = $qb->getDQL(); + self::assertStringContainsString('entity.type = :type_0', $dql); + self::assertStringNotContainsString('JOIN', $dql, 'A flat property must not trigger any JOIN'); + self::assertSame('prod', $qb->getParameter('type_0')?->getValue()); + } + + public function testApplyOnNestedPropertyJoinsTheRelation(): void + { + $qb = $this->makeInstallationQueryBuilder(); + + $this->apply($qb, 'server.type', 'stg'); + + $dql = $qb->getDQL(); + self::assertStringContainsString('LEFT JOIN entity.server server', $dql, 'Nested filter must auto-join the relation'); + self::assertStringContainsString('server.type = :server_type_0', $dql); + self::assertSame('stg', $qb->getParameter('server_type_0')?->getValue()); + } + + public function testApplyReusesExistingJoinAliasInsteadOfDuplicating(): void + { + $qb = $this->makeInstallationQueryBuilder() + ->leftJoin('entity.server', 'server'); + + $this->apply($qb, 'server.type', 'devops'); + + $dql = $qb->getDQL(); + self::assertSame(1, substr_count($dql, 'LEFT JOIN entity.server'), 'No duplicate join when alias already exists'); + self::assertStringContainsString('server.type = :server_type_0', $dql); + } + + private function apply(QueryBuilder $qb, string $property, string $value): void + { + $filterDto = new FilterDto(); + $filterDto->setProperty($property); + $filterDto->setFormType(ServerTypeFilterType::class); + + ServerTypeFilter::new($property)->apply( + $qb, + FilterDataDto::new(0, $filterDto, 'entity', [ + 'comparison' => '=', + 'value' => $value, + ]), + null, + new EntityDto(Installation::class, $this->getEntityManager()->getClassMetadata(Installation::class)), + ); + } + + private function makeInstallationQueryBuilder(): QueryBuilder + { + return $this->getEntityManager()->getRepository(Installation::class)->createQueryBuilder('entity'); + } + + private function getEntityManager(): EntityManagerInterface + { + return static::getContainer()->get('doctrine')->getManager(); + } +}