Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-8433: Added content tree children filtering #1290

Merged
merged 13 commits into from
Aug 20, 2024
Merged
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"ibexa/user": "~4.6.0@dev",
"ibexa/fieldtype-richtext": "~4.6.0@dev",
"ibexa/rest": "~4.6.0@dev",
"ibexa/polyfill-php82": "^1.0",
"ibexa/search": "~4.6.x-dev",
"babdev/pagerfanta-bundle": "^2.1",
"knplabs/knp-menu-bundle": "^3.0",
Expand Down
6 changes: 4 additions & 2 deletions src/bundle/Controller/Content/ContentTreeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public function loadChildrenAction(
Request $request,
int $parentLocationId,
int $limit,
int $offset
int $offset,
Query\Criterion $filter
): Node {
$location = $this->locationService->loadLocation($parentLocationId);
$loadSubtreeRequestNode = new LoadSubtreeRequestNode($parentLocationId, $limit, $offset);
Expand All @@ -91,7 +92,8 @@ public function loadChildrenAction(
true,
0,
$sortClause,
$sortOrder
$sortOrder,
$filter
);
}

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

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

konradoboza marked this conversation as resolved.
Show resolved Hide resolved
namespace Ibexa\Bundle\AdminUi\ControllerArgumentResolver;

use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\LogicalAnd;
use Ibexa\Contracts\Rest\Input\Parser\Query\Criterion\CriterionProcessorInterface;
use function Ibexa\PolyfillPhp82\iterator_to_array;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

/**
* @phpstan-import-type TCriterionProcessor from \Ibexa\AdminUi\REST\Input\Parser\CriterionProcessor
*/
final class ContentTreeChildrenQueryArgumentResolver implements ArgumentValueResolverInterface
{
/** @phpstan-var TCriterionProcessor */
private CriterionProcessorInterface $criterionProcessor;

/**
* @phpstan-param TCriterionProcessor $criterionProcessor
*/
public function __construct(
CriterionProcessorInterface $criterionProcessor
) {
$this->criterionProcessor = $criterionProcessor;
}

public function supports(Request $request, ArgumentMetadata $argument): bool
{
return Criterion::class === $argument->getType()
&& 'filter' === $argument->getName();
}

/**
* @return iterable<\Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion>
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
yield new LogicalAnd($this->processFilterQueryCriteria($request));
}

/**
* @return array<\Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion>
*/
private function processFilterQueryCriteria(Request $request): array
{
if (!$request->query->has('filter')) {
return [];
}

/** @var array<string, array<mixed>> $criteriaData */
$criteriaData = $request->query->all('filter');
if (empty($criteriaData)) {
return [];
}

$criteria = $this->criterionProcessor->processCriteria($criteriaData);

return iterator_to_array($criteria);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ services:
Ibexa\Bundle\AdminUi\ControllerArgumentResolver\UniversalDiscoveryRequestQueryArgumentResolver:
tags:
- { name: controller.argument_value_resolver, priority: 50 }

Ibexa\Bundle\AdminUi\ControllerArgumentResolver\ContentTreeChildrenQueryArgumentResolver:
arguments:
$criterionProcessor: '@Ibexa\AdminUi\REST\Input\Parser\CriterionProcessor'
tags:
- { name: controller.argument_value_resolver, priority: 50 }
4 changes: 4 additions & 0 deletions src/bundle/Resources/config/services/rest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ services:
$applicationConfigRestGeneratorRegistry: '@Ibexa\Contracts\AdminUi\REST\ApplicationConfigRestGeneratorRegistryInterface'
tags:
- { name: ibexa.rest.output.value_object.visitor, type: Ibexa\AdminUi\REST\Value\ApplicationConfig }

Ibexa\AdminUi\REST\Input\Parser\CriterionProcessor:
parent: Ibexa\Contracts\Rest\Input\Parser\Query\Criterion\BaseCriterionProcessor

35 changes: 35 additions & 0 deletions src/lib/REST/Input/Parser/CriterionProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

konradoboza marked this conversation as resolved.
Show resolved Hide resolved
namespace Ibexa\AdminUi\REST\Input\Parser;

use Ibexa\Contracts\Rest\Input\Parser\Query\Criterion\BaseCriterionProcessor;

/**
* @phpstan-type TCriterionProcessor \Ibexa\Contracts\Rest\Input\Parser\Query\Criterion\CriterionProcessorInterface<
* \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion
* >
*
* @extends \Ibexa\Contracts\Rest\Input\Parser\Query\Criterion\BaseCriterionProcessor<
* \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion
* >
*
* @internal
*/
final class CriterionProcessor extends BaseCriterionProcessor
{
protected function getMediaTypePrefix(): string
{
return 'application/vnd.ibexa.api.internal.criterion';
}

protected function getParserInvalidCriterionMessage(string $criterionName): string
{
return "Invalid Criterion <$criterionName>";
}
}
6 changes: 3 additions & 3 deletions src/lib/REST/Output/ValueObjectVisitor/ContentTree/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ public function visit(Visitor $visitor, Generator $generator, $data)
$generator->startValueElement('contentTypeIdentifier', $data->contentTypeIdentifier);
$generator->endValueElement('contentTypeIdentifier');

$generator->startValueElement('isContainer', $data->isContainer);
$generator->startValueElement('isContainer', $generator->serializeBool($data->isContainer));
$generator->endValueElement('isContainer');

$generator->startValueElement('isInvisible', $data->isInvisible);
$generator->startValueElement('isInvisible', $generator->serializeBool($data->isInvisible));
$generator->endValueElement('isInvisible');

$generator->startValueElement('displayLimit', $data->displayLimit);
Expand All @@ -63,7 +63,7 @@ public function visit(Visitor $visitor, Generator $generator, $data)

$generator->valueElement('reverseRelationsCount', $data->reverseRelationsCount);

$generator->valueElement('isBookmarked', $data->isBookmarked);
$generator->valueElement('isBookmarked', $generator->serializeBool($data->isBookmarked));

$generator->startList('children');

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

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

konradoboza marked this conversation as resolved.
Show resolved Hide resolved
namespace Ibexa\Tests\Bundle\AdminUi\ControllerArgumentResolver;

use ArrayIterator;
use Generator;
use Ibexa\Bundle\AdminUi\ControllerArgumentResolver\ContentTreeChildrenQueryArgumentResolver;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\ContentTypeIdentifier;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\LogicalAnd;
use Ibexa\Contracts\Rest\Input\Parser\Query\Criterion\CriterionProcessorInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Traversable;

/**
* @phpstan-type TCriterionProcessor \Ibexa\Contracts\Rest\Input\Parser\Query\Criterion\CriterionProcessorInterface<
* \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion
* >
*
* @covers \Ibexa\Bundle\AdminUi\ControllerArgumentResolver\ContentTreeChildrenQueryArgumentResolver
*/
final class ContentTreeChildrenQueryArgumentResolverTest extends TestCase
{
private ArgumentValueResolverInterface $resolver;

/** @phpstan-var TCriterionProcessor&\PHPUnit\Framework\MockObject\MockObject */
private CriterionProcessorInterface $criterionProcessor;

protected function setUp(): void
{
$this->criterionProcessor = $this->createMock(CriterionProcessorInterface::class);
$this->resolver = new ContentTreeChildrenQueryArgumentResolver(
$this->criterionProcessor
);
}

/**
* @dataProvider provideDataForTestSupports
*/
public function testSupports(
bool $expected,
ArgumentMetadata $argumentMetadata
): void {
self::assertSame(
$expected,
$this->resolver->supports(
new Request(),
$argumentMetadata
)
);
}

/**
* @return iterable<array{
* bool,
* \Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata
* }>
*/
public function provideDataForTestSupports(): iterable
{
yield 'Not supported' => [
false,
$this->createMock(ArgumentMetadata::class),
];

yield 'Not supported - invalid argument type' => [
false,
$this->createArgumentMetadata(
'filter',
'foo',
),
];

yield 'Not supported - invalid argument name' => [
false,
$this->createArgumentMetadata(
'foo',
Criterion::class,
),
];

yield 'Supported' => [
true,
$this->createArgumentMetadata(
'filter',
Criterion::class,
),
];
}

/**
* @dataProvider provideDataForTestResolve
*
* @param array<string, string|array<mixed>> $criteriaToProcess
* @param Traversable<\Ibexa\Contracts\Core\Repository\Values\Content\Query\CriterionInterface> $expectedCriteria
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
*/
public function testResolve(
Criterion $expected,
Request $request,
Traversable $expectedCriteria,
array $criteriaToProcess = []
): void {
if (!empty($criteriaToProcess)) {
$this->mockCriterionProcessorProcessCriteria($criteriaToProcess, $expectedCriteria);
}

$generator = $this->resolver->resolve(
$request,
$this->createMock(ArgumentMetadata::class)
);

self::assertInstanceOf(Generator::class, $generator);
$resolvedArguments = iterator_to_array($generator);

self::assertCount(1, $resolvedArguments);

self::assertEquals(
$expected,
$resolvedArguments[0]
);
}

/**
* @return iterable<array{
* \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion,
* \Symfony\Component\HttpFoundation\Request,
* \Traversable<\Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion>,
* 3?: array<string, string>,
* }>
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidCriterionArgumentException
*/
public function provideDataForTestResolve(): iterable
{
yield 'Return null - missing filter query param' => [
new LogicalAnd([]),
$this->createRequest(null),
new ArrayIterator(),
];

yield 'Return null - empty value for filter query param' => [
new LogicalAnd([]),
$this->createRequest([]),
new ArrayIterator(),
];

$criteriaToProcess = [
'ContentTypeIdentifierCriterion' => 'folder',
];
$expectedCriteria = [
new ContentTypeIdentifier('folder'),
];

yield 'Return filter with ContentTypeIdentifier criterion' => [
new LogicalAnd($expectedCriteria),
$this->createRequest($criteriaToProcess),
new ArrayIterator($expectedCriteria),
$criteriaToProcess,
];
}

/**
* @param array<string, string|array<mixed>> $criteriaToProcess
* @param Traversable<\Ibexa\Contracts\Core\Repository\Values\Content\Query\CriterionInterface> $expectedCriteria
*/
private function mockCriterionProcessorProcessCriteria(
?array $criteriaToProcess,
Traversable $expectedCriteria
): void {
$this->criterionProcessor
->method('processCriteria')
->with($criteriaToProcess)
->willReturn($expectedCriteria);
}

/**
* @param array<mixed>|null $filter
*/
private function createRequest(?array $filter): Request
{
$request = Request::create('/');

if (null !== $filter) {
$request->query->set('filter', $filter);
}

return $request;
}

private function createArgumentMetadata(
string $name,
string $type
): ArgumentMetadata {
return new ArgumentMetadata(
$name,
$type,
true,
false,
''
);
}
}
Loading
Loading