Skip to content

Commit

Permalink
Merge pull request #4 from chubbyphp/add-sort-to-collection
Browse files Browse the repository at this point in the history
add-sort-to-collection
  • Loading branch information
dominikzogg committed Oct 29, 2018
2 parents 9a6c7f3 + f24c6f0 commit 757699a
Show file tree
Hide file tree
Showing 17 changed files with 261 additions and 5 deletions.
21 changes: 21 additions & 0 deletions app/Collection/AbstractCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ abstract class AbstractCollection implements CollectionInterface
*/
protected $limit = self::LIMIT;

/**
* @var string[]
*/
protected $sort = [];

/**
* @var ModelInterface[]
*/
Expand Down Expand Up @@ -60,6 +65,22 @@ public function getLimit(): int
return $this->limit;
}

/**
* @param string[] $sort
*/
public function setSort(array $sort)
{
$this->sort = $sort;
}

/**
* @return string[]
*/
public function getSort(): array
{
return $this->sort;
}

/**
* @param int $count
*/
Expand Down
10 changes: 10 additions & 0 deletions app/Collection/CollectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ public function setLimit(int $limit);
*/
public function getLimit(): int;

/**
* @param string[] $sort
*/
public function setSort(array $sort);

/**
* @return string[]
*/
public function getSort(): array;

/**
* @param int $count
*/
Expand Down
1 change: 1 addition & 0 deletions app/Mapping/Deserialization/PetCollectionMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function getDenormalizationFieldMappings(string $path, string $type = nul
->getMapping(),
DenormalizationFieldMappingBuilder::createConvertType('limit', ConvertTypeFieldDenormalizer::TYPE_INT)
->getMapping(),
DenormalizationFieldMappingBuilder::create('sort')->getMapping(),
];
}
}
1 change: 1 addition & 0 deletions app/Mapping/Serialization/AbstractCollectionMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function getNormalizationFieldMappings(string $path): array
NormalizationFieldMappingBuilder::create('offset')->getMapping(),
NormalizationFieldMappingBuilder::create('limit')->getMapping(),
NormalizationFieldMappingBuilder::create('count')->getMapping(),
NormalizationFieldMappingBuilder::create('sort')->getMapping(),
];
}

Expand Down
87 changes: 87 additions & 0 deletions app/Mapping/Validation/Constraint/SortConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace App\Mapping\Validation\Constraint;

use Chubbyphp\Validation\Constraint\ConstraintInterface;
use Chubbyphp\Validation\ValidatorContextInterface;
use Chubbyphp\Validation\ValidatorInterface;
use Chubbyphp\Validation\ValidatorLogicException;
use Chubbyphp\Validation\Error\ErrorInterface;
use Chubbyphp\Validation\Error\Error;

final class SortConstraint implements ConstraintInterface
{
/**
* @var string[]
*/
private $allowedFields;

const ALLOWED_ORDERS = ['asc', 'desc'];

/**
* @param array $allowedFields
*/
public function __construct(array $allowedFields)
{
$this->allowedFields = $allowedFields;
}

/**
* @param string $path
* @param mixed $sort
* @param ValidatorContextInterface $context
* @param ValidatorInterface|null $validator
*
* @return ErrorInterface[]
*
* @throws ValidatorLogicException
*/
public function validate(
string $path,
$sort,
ValidatorContextInterface $context,
ValidatorInterface $validator = null
): array {
if (!is_array($sort)) {
return [new Error(
$path,
'constraint.sort.invalidtype',
['type' => is_object($sort) ? get_class($sort) : gettype($sort)]
)];
}

$errors = [];

foreach ($sort as $field => $order) {
if (!in_array($field, $this->allowedFields, true)) {
$errors[] = new Error(
$path,
'constraint.sort.field.notallowed',
['field' => $field, 'allowedFields' => $this->allowedFields]
);
}

if (!is_string($order)) {
$errors[] = new Error(
$path,
'constraint.sort.order.invalidtype',
['field' => $field, 'type' => is_object($order) ? get_class($order) : gettype($order)]
);

continue;
}

if (!in_array($order, self::ALLOWED_ORDERS, true)) {
$errors[] = new Error(
$path,
'constraint.sort.order.notallowed',
['field' => $field, 'order' => $order, 'allowedOrders' => self::ALLOWED_ORDERS]
);
}
}

return $errors;
}
}
4 changes: 4 additions & 0 deletions app/Mapping/Validation/PetCollectionMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Mapping\Validation;

use App\Collection\PetCollection;
use App\Mapping\Validation\Constraint\SortConstraint;
use Chubbyphp\Validation\Constraint\NotBlankConstraint;
use Chubbyphp\Validation\Constraint\TypeConstraint;
use Chubbyphp\Validation\Mapping\ValidationClassMappingInterface;
Expand Down Expand Up @@ -49,6 +50,9 @@ public function getValidationPropertyMappings(string $path, string $type = null)
new NotBlankConstraint(),
new TypeConstraint('integer'),
])->getMapping(),
ValidationPropertyMappingBuilder::create('sort', [
new SortConstraint(['name']),
])->getMapping(),
];
}
}
5 changes: 5 additions & 0 deletions app/Repository/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public function resolveCollection(CollectionInterface $collection)
$collection->setCount((int) $countQb->getQuery()->getSingleScalarResult());

$itemsQb = clone $qb;

foreach ($collection->getSort() as $field => $order) {
$itemsQb->addOrderBy(sprintf('m.%s', $field), $order);
}

$itemsQb->setFirstResult($collection->getOffset());
$itemsQb->setMaxResults($collection->getLimit());

Expand Down
16 changes: 14 additions & 2 deletions swagger/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ info:
servers:
- url: http://petstore.development
paths:
/pets:
/api/pets:
get:
summary: List all pets
operationId: listPets
Expand Down Expand Up @@ -35,6 +35,18 @@ paths:
schema:
type: integer
example: 20
- name: sort
in: query
description: How to sort the items
required: false
schema:
type: object
properties:
name:
type: string
example: asc
style: deepObject
explode: true
responses:
'200':
description: Pets
Expand Down Expand Up @@ -116,7 +128,7 @@ paths:
$ref: '#/components/schemas/ErrorValidation'
'500':
description: Server error
/pets/{id}:
/api/pets/{id}:
get:
summary: Read a pet
operationId: readPet
Expand Down
3 changes: 3 additions & 0 deletions tests/Unit/Collection/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ public function testGetSet()
self::assertSame(0, $collection->getOffset());
self::assertSame(20, $collection->getLimit());
self::assertSame(0, $collection->getCount());
self::assertSame([], $collection->getSort());
self::assertSame([], $collection->getItems());

$object = new \stdClass();

$collection->setOffset(5);
$collection->setLimit(15);
$collection->setCount(6);
$collection->setSort(['name' => 'asc']);
$collection->setItems([$object]);

self::assertSame(5, $collection->getOffset());
self::assertSame(15, $collection->getLimit());
self::assertSame(6, $collection->getCount());
self::assertSame(['name' => 'asc'], $collection->getSort());
self::assertSame([$object], $collection->getItems());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public function testGetDenormalizationFieldMappings()
->getMapping(),
DenormalizationFieldMappingBuilder::createConvertType('limit', ConvertTypeFieldDenormalizer::TYPE_INT)
->getMapping(),
DenormalizationFieldMappingBuilder::create('sort')->getMapping(),
], $fieldMappings);
}
}
1 change: 1 addition & 0 deletions tests/Unit/Mapping/Serialization/CollectionMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public function testGetNormalizationFieldMappings()
NormalizationFieldMappingBuilder::create('offset')->getMapping(),
NormalizationFieldMappingBuilder::create('limit')->getMapping(),
NormalizationFieldMappingBuilder::create('count')->getMapping(),
NormalizationFieldMappingBuilder::create('sort')->getMapping(),
], $fieldMappings);
}

Expand Down
104 changes: 104 additions & 0 deletions tests/Unit/Mapping/Validation/Constraint/SortConstraintTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace App\Tests\Unit\Mapping\Validation\Constraint;

use App\Mapping\Validation\Constraint\SortConstraint;
use Chubbyphp\Mock\MockByCallsTrait;
use Chubbyphp\Validation\ValidatorContextInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Chubbyphp\Validation\Error\Error;

/**
* @covers \App\Mapping\Validation\Constraint\SortConstraint
*/
final class SortConstraintTest extends TestCase
{
use MockByCallsTrait;

public function testValidateWithString()
{
/** @var ValidatorContextInterface|MockObject $context */
$context = $this->getMockByCalls(ValidatorContextInterface::class);

$constraint = new SortConstraint(['name']);

self::assertEquals(
[new Error('path', 'constraint.sort.invalidtype', ['type' => 'string'])],
$constraint->validate('path', 'name', $context)
);
}

public function testValidateWithStdClass()
{
/** @var ValidatorContextInterface|MockObject $context */
$context = $this->getMockByCalls(ValidatorContextInterface::class);

$constraint = new SortConstraint(['name']);

self::assertEquals(
[new Error('path', 'constraint.sort.invalidtype', ['type' => \stdClass::class])],
$constraint->validate('path', new \stdClass(), $context)
);
}

public function testValidateWithUnsupportedFieldAndUnsupportedOrder()
{
/** @var ValidatorContextInterface|MockObject $context */
$context = $this->getMockByCalls(ValidatorContextInterface::class);

$constraint = new SortConstraint(['name']);

self::assertEquals(
[
new Error(
'path',
'constraint.sort.field.notallowed',
['field' => 'unknown', 'allowedFields' => ['name']]
),
new Error(
'path',
'constraint.sort.order.notallowed',
['field' => 'unknown', 'order' => 'test', 'allowedOrders' => ['asc', 'desc']]
),
],
$constraint->validate('path', ['name' => 'asc', 'unknown' => 'test'], $context)
);
}

public function testValidateWithUnsupportedFieldAndUnsupportedOrderType()
{
/** @var ValidatorContextInterface|MockObject $context */
$context = $this->getMockByCalls(ValidatorContextInterface::class);

$constraint = new SortConstraint(['name']);

self::assertEquals(
[
new Error(
'path',
'constraint.sort.field.notallowed',
['field' => 'unknown', 'allowedFields' => ['name']]
),
new Error(
'path',
'constraint.sort.order.invalidtype',
['field' => 'unknown', 'type' => \stdClass::class]
),
],
$constraint->validate('path', ['name' => 'asc', 'unknown' => new \stdClass()], $context)
);
}

public function testValidate()
{
/** @var ValidatorContextInterface|MockObject $context */
$context = $this->getMockByCalls(ValidatorContextInterface::class);

$constraint = new SortConstraint(['name']);

self::assertSame([], $constraint->validate('path', ['name' => 'asc'], $context));
}
}
4 changes: 4 additions & 0 deletions tests/Unit/Mapping/Validation/PetCollectionMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Tests\Unit\Mapping\Validation;

use App\Collection\PetCollection;
use App\Mapping\Validation\Constraint\SortConstraint;
use App\Mapping\Validation\PetCollectionMapping;
use Chubbyphp\Validation\Constraint\NotBlankConstraint;
use Chubbyphp\Validation\Constraint\TypeConstraint;
Expand Down Expand Up @@ -45,6 +46,9 @@ public function testGetValidationPropertyMappings()
new NotBlankConstraint(),
new TypeConstraint('integer'),
])->getMapping(),
ValidationPropertyMappingBuilder::create('sort', [
new SortConstraint(['name']),
])->getMapping(),
], $propertyMappings);
}
}
2 changes: 2 additions & 0 deletions tests/Unit/Repository/RepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function testResolveCollection()
/** @var CollectionInterface|MockObject $collection */
$collection = $this->getMockByCalls(CollectionInterface::class, [
Call::create('setCount')->with(count($items)),
Call::create('getSort')->with()->willReturn(['name' => 'asc']),
Call::create('getOffset')->with()->willReturn(0),
Call::create('getLimit')->with()->willReturn(20),
Call::create('setItems')->with($items),
Expand Down Expand Up @@ -63,6 +64,7 @@ public function testResolveCollection()
Call::create('expr')->with()->willReturn($expr),
Call::create('select')->with($func),
Call::create('getQuery')->with()->willReturn($countQuery),
Call::create('addOrderBy')->with('m.name', 'asc'),
Call::create('setFirstResult')->with(0),
Call::create('setMaxResults')->with(20),
Call::create('getQuery')->with()->willReturn($itemsQuery),
Expand Down
Loading

0 comments on commit 757699a

Please sign in to comment.