Skip to content

Commit

Permalink
[FRAM-88] Handle pagination (#2)
Browse files Browse the repository at this point in the history
* feat: Handle pagination (#FRAM-88)

* ci: Improve mutation coverage

* test: Improve coverage of FilterForm
  • Loading branch information
vincent4vx committed May 2, 2023
1 parent 38fb700 commit 042f3c1
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 1 deletion.
53 changes: 52 additions & 1 deletion src/FilterForm.php
Expand Up @@ -7,10 +7,13 @@
use Bdf\Form\Custom\CustomForm;
use Bdf\Prime\Entity\Criteria as PrimeCriteria;
use Bdf\Prime\Locatorizable;
use Bdf\Prime\Query\Contract\Whereable;
use Bdf\Prime\Query\Contract\Limitable;
use Bdf\Prime\Query\Contract\Paginable;
use Bdf\Prime\Query\Pagination\PaginatorInterface;
use Bdf\Prime\Query\QueryInterface;
use Bdf\Prime\Repository\RepositoryInterface;
use Bdf\Prime\ServiceLocator;
use InvalidArgumentException;

/**
* Base type for declare a filter form
Expand Down Expand Up @@ -129,6 +132,54 @@ final public function query(): QueryInterface
return $this->apply($this->repository()->queries()->builder());
}

/**
* Execute the query and get a paginator
* Page and per page fields should be defined in the form
*
* <code>
* class MyFilters extends FilterForm
* {
* public function configureFilters(FilterFormBuilder $builder): void
* {
* // Configure the entity
* $this->setEntity(MyEntity::class);
* // Configure filters...
*
* // Pagination fields
* $builder->page();
* $builder->perPage();
* }
* }
*
* // Instantiate the form using container to ensure that the prime service locator is injected
* $form = $this->container->get(MyFilters::class);
*
* // Submit filters and paginate
* $paginator = $form->submit($request->query->all())->query()->paginate();
* </code>
*
* @param QueryInterface|null $query The query to paginate. If null, the query will be created from the form.
*
* @return PaginatorInterface
*
* @see FilterFormBuilder::page()
* @see FilterFormBuilder::perPage()
*/
public final function paginate(?QueryInterface $query = null): PaginatorInterface
{
if ($query) {
$this->apply($query);
} else {
$query = $this->query();
}

if (!$query instanceof Paginable || !$query instanceof Limitable) {

Check warning on line 176 in src/FilterForm.php

View workflow job for this annotation

GitHub Actions / Analysis

Escaped Mutant: --- Original +++ New @@ @@ } else { $query = $this->query(); } - if (!$query instanceof Paginable || !$query instanceof Limitable) { + if (!true || !$query instanceof Limitable) { throw new InvalidArgumentException('The query must be Paginable'); } return $query->paginate($query->getLimit(), $query->getPage());

Check warning on line 176 in src/FilterForm.php

View workflow job for this annotation

GitHub Actions / Analysis

Escaped Mutant: --- Original +++ New @@ @@ } else { $query = $this->query(); } - if (!$query instanceof Paginable || !$query instanceof Limitable) { + if (!$query instanceof Paginable || !true) { throw new InvalidArgumentException('The query must be Paginable'); } return $query->paginate($query->getLimit(), $query->getPage());

Check warning on line 176 in src/FilterForm.php

View workflow job for this annotation

GitHub Actions / Analysis

Escaped Mutant: --- Original +++ New @@ @@ } else { $query = $this->query(); } - if (!$query instanceof Paginable || !$query instanceof Limitable) { + if (!$query instanceof Paginable && !$query instanceof Limitable) { throw new InvalidArgumentException('The query must be Paginable'); } return $query->paginate($query->getLimit(), $query->getPage());
throw new InvalidArgumentException('The query must be Paginable');
}

return $query->paginate($query->getLimit(), $query->getPage());
}

/**
* Define the handled entity
* This is used by the `query()` method to create the query
Expand Down
51 changes: 51 additions & 0 deletions src/FilterFormBuilder.php
Expand Up @@ -19,6 +19,9 @@
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

use function is_numeric;
use function max;

/**
* Wrap a form builder for build prime filters
*
Expand All @@ -40,6 +43,11 @@ class FilterFormBuilder implements FormBuilderInterface
*/
private $inner;

/**
* @var BaseFilterForm
*/
private $form;

/**
* FilterFormBuilder constructor.
* @param FormBuilderInterface $inner
Expand Down Expand Up @@ -332,4 +340,47 @@ public function searchContains(string $name, ?string $default = null)
{
return $this->string($name, $default)->contains();
}

/**
* Configure field "page" for pagination
*
* @param non-empty-string $name Page field name. Default to "page"
*
* @return FilterChildBuilder|IntegerElementBuilder
*/
public function page(string $name = 'page')
{
return $this->integer($name)
->filter(function ($value) {
if (!is_numeric($value)) {
return 1;
}

return max(1, (int) $value);

Check warning on line 359 in src/FilterFormBuilder.php

View workflow job for this annotation

GitHub Actions / Analysis

Escaped Mutant: --- Original +++ New @@ @@ if (!is_numeric($value)) { return 1; } - return max(1, (int) $value); + return max(1, $value); })->setter('page'); } /**
})
->setter('page')
;
}

/**
* Configure field "perPage" for pagination
*
* @param non-empty-string $name Per page field name. Default to "perPage"
* @param int $default Default row count. Default to 10
*
* @return FilterChildBuilder|IntegerElementBuilder
*/
public function perPage(string $name = 'perPage', int $default = 10)
{
return $this->integer($name)
->filter(function ($value) use ($default) {
if (!is_numeric($value)) {
return $default;
}

return max(1, (int) $value);

Check warning on line 381 in src/FilterFormBuilder.php

View workflow job for this annotation

GitHub Actions / Analysis

Escaped Mutant: --- Original +++ New @@ @@ if (!is_numeric($value)) { return $default; } - return max(1, (int) $value); + return max(1, $value); })->setter('pageMaxRows'); } }
})
->setter('pageMaxRows')
;
}
}
21 changes: 21 additions & 0 deletions tests/FilterFormBuilderTest.php
Expand Up @@ -286,4 +286,25 @@ public function test_searchContains()
$form->attach(new PrimeCriteria())->submit(['foo' => 'bar']);
$this->assertEquals(new PrimeCriteria(['foo' => (new Like('bar'))->contains()->escape()]), $form->value());
}

/**
*
*/
public function test_page_and_perPage()
{
$this->builder->page('foo');
$this->builder->perPage('bar', 15);
$this->builder->generates(PrimeCriteria::class);

$form = $this->builder->buildElement();

$this->assertInstanceOf(IntegerElement::class, $form['foo']->element());
$this->assertInstanceOf(IntegerElement::class, $form['bar']->element());

$this->assertEquals(new PrimeCriteria([':limitPage' => [1, 15]]), $form->submit([])->value());
$this->assertEquals(new PrimeCriteria([':limitPage' => [5, 15]]), $form->submit(['foo' => 5])->value());
$this->assertEquals(new PrimeCriteria([':limitPage' => [1, 5]]), $form->submit(['bar' => 5])->value());
$this->assertEquals(new PrimeCriteria([':limitPage' => [1, 1]]), $form->submit(['foo' => -10, 'bar' => -5])->value());
$this->assertEquals(new PrimeCriteria([':limitPage' => [1, 15]]), $form->submit(['foo' => 'invalid', 'bar' => 'invalid'])->value());
}
}
105 changes: 105 additions & 0 deletions tests/FilterFormTest.php
Expand Up @@ -8,8 +8,10 @@
use Bdf\Prime\Entity\Criteria as PrimeCriteria;
use Bdf\Prime\Locatorizable;
use Bdf\Prime\Mapper\Mapper;
use Bdf\Prime\Query\Contract\Whereable;
use Bdf\Prime\Query\Expression\Like;
use Bdf\Prime\Query\Query;
use Bdf\Prime\Query\QueryInterface;
use Bdf\Prime\Repository\RepositoryInterface;
use Bdf\Prime\ServiceLocator;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -226,6 +228,109 @@ protected function configureFilters(FilterFormBuilder $builder): void
$form->submit([]);
$this->assertSame($builder, $form->builder);
}

public function test_paginate()
{
$prime = new ServiceLocator(new ConnectionManager(new ConnectionRegistry(['test' => 'sqlite::memory:'])));
$prime->repository(Person::class)->schema()->migrate();
$form = new class(null, $prime) extends PersonFormFilter {
protected function configureFilters(FilterFormBuilder $builder): void
{
parent::configureFilters($builder);

$builder->page();
$builder->perPage();
}
};

$form->submit([
'firstName' => 'J',
'lastName' => 'Smi',
'age' => [20, 55],
'page' => 3,
'perPage' => 15,
]);

$paginator = $form->paginate();

$this->assertSame(3, $paginator->page());
$this->assertSame(15, $paginator->pageMaxRows());
$this->assertSame('SELECT t0.* FROM person t0 WHERE t0.firstName LIKE \'J%\' AND t0.lastName LIKE \'Smi%\' AND t0.age BETWEEN 20 AND 55 LIMIT 15 OFFSET 30', $paginator->query()->toRawSql());

$form->submit([
'firstName' => 'J',
'lastName' => 'Smi',
'age' => [20, 55],
]);

$paginator = $form->paginate();

$this->assertSame(1, $paginator->page());
$this->assertSame(10, $paginator->pageMaxRows());
$this->assertSame('SELECT t0.* FROM person t0 WHERE t0.firstName LIKE \'J%\' AND t0.lastName LIKE \'Smi%\' AND t0.age BETWEEN 20 AND 55 LIMIT 10', $paginator->query()->toRawSql());
}

public function test_paginate_with_custom_query()
{
$prime = new ServiceLocator(new ConnectionManager(new ConnectionRegistry(['test' => 'sqlite::memory:'])));
$prime->repository(Person::class)->schema()->migrate();
$form = new class(null, $prime) extends PersonFormFilter {
protected function configureFilters(FilterFormBuilder $builder): void
{
parent::configureFilters($builder);

$builder->page();
$builder->perPage();
}
};

$form->submit([
'firstName' => 'J',
'lastName' => 'Smi',
'age' => [20, 55],
'page' => 3,
'perPage' => 15,
]);

$query = $prime->repository(Person::class)->builder()->where('firstName', '<', 'ZZZ');

$paginator = $form->paginate($query);

$this->assertSame(3, $paginator->page());
$this->assertSame(15, $paginator->pageMaxRows());
$this->assertSame('SELECT t0.* FROM person t0 WHERE t0.firstName < \'ZZZ\' AND (t0.firstName LIKE \'J%\' AND t0.lastName LIKE \'Smi%\' AND t0.age BETWEEN 20 AND 55) LIMIT 15 OFFSET 30', $paginator->query()->toRawSql());
}

public function test_paginate_query_not_paginable()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The query must be Paginable');

$prime = new ServiceLocator(new ConnectionManager(new ConnectionRegistry(['test' => 'sqlite::memory:'])));
$prime->repository(Person::class)->schema()->migrate();
$form = new class(null, $prime) extends PersonFormFilter {
protected function configureFilters(FilterFormBuilder $builder): void
{
parent::configureFilters($builder);

$builder->page();
$builder->perPage();
}
};

$form->submit([
'firstName' => 'J',
'lastName' => 'Smi',
'age' => [20, 55],
'page' => 3,
'perPage' => 15,
]);

$query = $this->createMock(QueryInterface::class);
$query->method('where')->willReturnSelf();

$form->paginate($query);
}
}

class PersonFormFilter extends FilterForm
Expand Down

0 comments on commit 042f3c1

Please sign in to comment.