Skip to content

Commit

Permalink
add default_order option to doctrine processors
Browse files Browse the repository at this point in the history
  • Loading branch information
alekitto committed Jan 24, 2020
1 parent f2c5050 commit 4f4b65b
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class StringToExpresionTransformer implements DataTransformerInterface

public function __construct(?Grammar $grammar = null)
{
$this->grammar = $grammar ?? new Grammar();
$this->grammar = $grammar ?? Grammar::getInstance();
}

/**
Expand Down
25 changes: 24 additions & 1 deletion src/QueryLanguage/Form/QueryType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
namespace Fazland\ApiPlatformBundle\QueryLanguage\Form;

use Fazland\ApiPlatformBundle\Form\PageTokenType;
use Fazland\ApiPlatformBundle\QueryLanguage\Expression\OrderExpression;
use Fazland\ApiPlatformBundle\QueryLanguage\Form\DTO\Query;
use Fazland\ApiPlatformBundle\QueryLanguage\Processor\ColumnInterface;
use Fazland\ApiPlatformBundle\QueryLanguage\Validator\Expression;
use Fazland\ApiPlatformBundle\QueryLanguage\Walker\Validation\OrderWalker;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
Expand All @@ -23,11 +26,29 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (null !== $options['order_field']) {
$builder->add($options['order_field'], FieldType::class, [
'data_class' => null,
'property_path' => 'ordering',
'constraints' => [
new Expression(new OrderWalker($options['orderable_columns'])),
],
]);

if (isset($options['default_order'])) {
$default = $options['default_order'];
$field = $options['order_field'];
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
static function (FormEvent $event) use ($default, $field): void {
$data = $event->getData();
if (isset($data[$field])) {
return;
}

$data[$field] = $default;
$event->setData($data);
}
);
}
}

if (null !== $options['continuation_token_field']) {
Expand Down Expand Up @@ -65,16 +86,18 @@ public function configureOptions(OptionsResolver $resolver): void
'limit_field' => null,
'continuation_token_field' => null,
'order_field' => null,
'default_order' => null,
'allow_extra_fields' => true,
'method' => Request::METHOD_GET,
'orderable_columns' => function (Options $options) {
'orderable_columns' => static function (Options $options) {
return \array_keys($options['columns']);
},
])
->setAllowedTypes('skip_field', ['null', 'string'])
->setAllowedTypes('limit_field', ['null', 'string'])
->setAllowedTypes('continuation_token_field', ['null', 'string'])
->setAllowedTypes('order_field', ['null', 'string'])
->setAllowedTypes('default_order', ['null', OrderExpression::class])
->setRequired('columns')
;
}
Expand Down
16 changes: 16 additions & 0 deletions src/QueryLanguage/Grammar/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@

final class Grammar extends AbstractGrammar
{
private static ?self $instance = null;

/**
* Gets the Grammar class singleton.
*
* @return self
*/
public static function getInstance(): self
{
if (null === self::$instance) {
self::$instance = new self();
}

return self::$instance;
}

/**
* Parses an expression into an AST.
*
Expand Down
22 changes: 21 additions & 1 deletion src/QueryLanguage/Processor/Doctrine/AbstractProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Fazland\ApiPlatformBundle\QueryLanguage\Expression\OrderExpression;
use Fazland\ApiPlatformBundle\QueryLanguage\Form\DTO\Query;
use Fazland\ApiPlatformBundle\QueryLanguage\Form\QueryType;
use Fazland\ApiPlatformBundle\QueryLanguage\Grammar\Grammar;
use Fazland\ApiPlatformBundle\QueryLanguage\Processor\ColumnInterface;
use Fazland\ApiPlatformBundle\QueryLanguage\Processor\Doctrine\ORM\Column as ORMColumn;
use Fazland\ApiPlatformBundle\QueryLanguage\Processor\Doctrine\PhpCr\Column as PhpCrColumn;
Expand Down Expand Up @@ -98,6 +99,7 @@ protected function handleRequest(Request $request)
'limit_field' => $this->options['limit_field'],
'skip_field' => $this->options['skip_field'],
'order_field' => $this->options['order_field'],
'default_order' => $this->options['default_order'],
'continuation_token_field' => $this->options['continuation_token']['field'] ?? null,
'columns' => $this->columns,
'orderable_columns' => \array_keys(\array_filter($this->columns, static function (ColumnInterface $column): bool {
Expand Down Expand Up @@ -168,7 +170,7 @@ private function resolveOptions(array $options): array
{
$resolver = new OptionsResolver();

foreach (['order_field', 'skip_field', 'limit_field'] as $field) {
foreach (['order_field', 'skip_field', 'limit_field', 'default_order'] as $field) {
$resolver
->setDefault($field, null)
->setAllowedTypes($field, ['null', 'string'])
Expand Down Expand Up @@ -197,6 +199,24 @@ private function resolveOptions(array $options): array

return $value;
})
->setNormalizer('default_order', static function (Options $options, $value): ?OrderExpression {
if (empty($value)) {
return null;
}

if (false === \strpos($value, '$')) {
$value = '$order('.$value.')';
}

$grammar = Grammar::getInstance();
$expression = $grammar->parse($value);

if (! $expression instanceof OrderExpression) {
throw new InvalidOptionsException('Invalid default order specified');
}

return $expression;
})
;

return $resolver->resolve($options);
Expand Down
71 changes: 68 additions & 3 deletions tests/QueryLanguage/Processor/Doctrine/ORM/ProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormFactoryBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\Validator\ValidatorBuilder;

class ProcessorTest extends TestCase
Expand Down Expand Up @@ -86,9 +88,9 @@ public function testColumnWithFieldInRelatedEntityWorks(): void

public function provideParamsForPageSize(): iterable
{
yield [ [] ];
yield [ ['order' => '$order(name)'] ];
yield [ ['order' => '$order(name)', 'continue' => '=YmF6_1_10tf9ny'] ];
yield [[]];
yield [['order' => '$order(name)']];
yield [['order' => '$order(name)', 'continue' => '=YmF6_1_10tf9ny']];
}

/**
Expand All @@ -106,6 +108,69 @@ public function testPageSizeOptionShouldWork(array $params): void
self::assertCount(3, $result);
}

public function testOrderByDefaultFieldShouldThrowOnInvalidOptions(): void
{
$this->expectException(InvalidOptionsException::class);

$formFactory = (new FormFactoryBuilder(true))
->addExtension(new ValidatorExtension((new ValidatorBuilder())->getValidator()))
->addTypeExtension(new FormTypeHttpFoundationExtension(new AutoSubmitRequestHandler()))
->getFormFactory();

$this->processor = new Processor(
self::$entityManager->getRepository(User::class)->createQueryBuilder('u'),
$formFactory,
[
'default_order' => '$eq(name)',
'order_field' => 'order',
'continuation_token' => true,
],
);

$this->processor->addColumn('name');
$this->processor->setDefaultPageSize(3);
$this->processor->processRequest(new Request([]));
}

public function provideParamsForDefaultOrder(): iterable
{
yield [true, '$order(name)'];
yield [true, 'name'];
yield [true, 'name, desc'];
yield [false, '$order(nonexistent, asc)'];
}

/**
* @dataProvider provideParamsForDefaultOrder
*/
public function testOrderByDefaultFieldShouldWork(bool $valid, string $defaultOrder): void
{
$formFactory = (new FormFactoryBuilder(true))
->addExtension(new ValidatorExtension((new ValidatorBuilder())->getValidator()))
->addTypeExtension(new FormTypeHttpFoundationExtension(new AutoSubmitRequestHandler()))
->getFormFactory();

$this->processor = new Processor(
self::$entityManager->getRepository(User::class)->createQueryBuilder('u'),
$formFactory,
[
'default_order' => $defaultOrder,
'order_field' => 'order',
'continuation_token' => true,
],
);

$this->processor->addColumn('name');
$this->processor->setDefaultPageSize(3);
$itr = $this->processor->processRequest(new Request([]));

if (! $valid) {
self::assertInstanceOf(FormInterface::class, $itr);
} else {
self::assertInstanceOf(PagerIterator::class, $itr);
}
}

public function testCustomColumnWorks(): void
{
$this->processor->addColumn('foobar', new class(self::$entityManager) implements ColumnInterface {
Expand Down
72 changes: 69 additions & 3 deletions tests/QueryLanguage/Processor/Doctrine/PhpCr/ProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormFactoryBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\Validator\ValidatorBuilder;

class ProcessorTest extends TestCase
Expand Down Expand Up @@ -200,9 +202,9 @@ public function testColumnWithFieldInRelatedEntityWorks(): void

public function provideParamsForPageSize(): iterable
{
yield [ [] ];
yield [ ['order' => '$order(name)'] ];
yield [ ['order' => '$order(name)', 'continue' => '=YmF6_1_10tf9ny'] ];
yield [[]];
yield [['order' => '$order(name)']];
yield [['order' => '$order(name)', 'continue' => '=YmF6_1_10tf9ny']];
}

/**
Expand Down Expand Up @@ -235,6 +237,70 @@ public function testPageSizeOptionShouldWork(array $params): void
self::assertCount(3, $result);
}

public function testOrderByDefaultFieldShouldThrowOnInvalidOptions(): void
{
$this->expectException(InvalidOptionsException::class);
$formFactory = (new FormFactoryBuilder(true))
->addExtension(new ValidatorExtension((new ValidatorBuilder())->getValidator()))
->addTypeExtension(new FormTypeHttpFoundationExtension(new AutoSubmitRequestHandler()))
->getFormFactory();

$this->processor = new Processor(
self::$documentManager->getRepository(User::class)->createQueryBuilder('u'),
self::$documentManager,
$formFactory,
[
'default_order' => '$eq(name)',
'order_field' => 'order',
'continuation_token' => true,
],
);

$this->processor->addColumn('name');
$this->processor->setDefaultPageSize(3);
$this->processor->processRequest(new Request([]));
}

public function provideParamsForDefaultOrder(): iterable
{
yield [true, '$order(name)'];
yield [true, 'name'];
yield [true, 'name, desc'];
yield [false, '$order(nonexistent, asc)'];
}

/**
* @dataProvider provideParamsForDefaultOrder
*/
public function testOrderByDefaultFieldShouldWork(bool $valid, string $defaultOrder): void
{
$formFactory = (new FormFactoryBuilder(true))
->addExtension(new ValidatorExtension((new ValidatorBuilder())->getValidator()))
->addTypeExtension(new FormTypeHttpFoundationExtension(new AutoSubmitRequestHandler()))
->getFormFactory();

$this->processor = new Processor(
self::$documentManager->getRepository(User::class)->createQueryBuilder('u'),
self::$documentManager,
$formFactory,
[
'default_order' => $defaultOrder,
'order_field' => 'order',
'continuation_token' => true,
],
);

$this->processor->addColumn('name');
$this->processor->setDefaultPageSize(3);
$itr = $this->processor->processRequest(new Request([]));

if (! $valid) {
self::assertInstanceOf(FormInterface::class, $itr);
} else {
self::assertInstanceOf(PagerIterator::class, $itr);
}
}

public function testCustomColumnWorks(): void
{
$this->processor->addColumn('foobar', new class(self::$documentManager) implements ColumnInterface {
Expand Down

0 comments on commit 4f4b65b

Please sign in to comment.