Skip to content

Commit

Permalink
Feature/add filter types (#30)
Browse files Browse the repository at this point in the history
* add filter types

* Refactor filter type expectations and test changes

* Add missing format to typecaster call

* Filter type enumerator support

* Enumerator filter test

* Documentation update to reflect type refactor changes

* Documentation update to reflect type refactor changes2

* Refactor now supports arrays, removed expectsMany

* Update tests to match refactor

* Update docs to match refactor

* some pr comments resolved

* pr comment resolved

* pr comment enum array support

* Filter adjustment in test so association test can filter on an array of inputs again

* phpcomment to reflect removal of expectarray

* php comment fix in test

* backslash before global php function

* Fix final feedback for PR

Co-authored-by: Lex <[email protected]>
  • Loading branch information
drtheuns and learn2draw authored Mar 9, 2020
1 parent a0fcd4b commit 412f136
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 55 deletions.
22 changes: 11 additions & 11 deletions docs/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ is constructed when fetching the data for the response.

Each filter defines a type that they expect as input and the number of
parameters they expect. By default, a filter will expect a single string value.
The `expect` and `expectMany` methods may be used to alter these expectations.
The `expect` method may be used to alter these expectations.
For example:

```php
Expand All @@ -18,19 +18,19 @@ public function filters(): array
// but we'll get to that in the next section.
return [
// Expect a single string value. This is the default.
'name' => $this->filter()->expect('string'),
'name' => $this->filter()->expect()->string(),

// Expect a single date.
'created_after' => $this->filter()->expect('date'),
'created_after' => $this->filter()->expect()->date(),

// Expect a single date with a custom format.
'created_before' => $this->filter()->expect('date', 'd-m-Y')
'created_before' => $this->filter()->expect()->date('d-m-Y'),

// Expects an array of UUIDs
'groups' => $this->filter()->expectMany('uuid'),
'groups' => $this->filter()->expect()->array()->whereEach()->uuid(),

// Expect a single boolean value
'is_active' => $this->filter()->expect('boolean'),
'is_active' => $this->filter()->expect()->boolean(),
];
}
```
Expand Down Expand Up @@ -110,7 +110,7 @@ fields. It uses the `LikeFilter` class from the previous section.
public function filters(): array
{
return [
'search' => $this->filter()->search(['first_name', 'last_name'']),
'search' => $this->filter()->search(['first_name', 'last_name'])->expect()->string(),
];
}
```
Expand All @@ -128,8 +128,8 @@ is the comparison operator.
public function filters(): array
{
return [
'published_after' => $this->filter()->expect('date')->byField('published', '>'),
'statuses' => $this->filter()->expectMany('string')->byField('status'),
'published_after' => $this->filter()->expect()->date()->byField('published', '>'),
'statuses' => $this->filter()->expect()->array()->whereEach()->string()->byField('status'),
];
}
```
Expand All @@ -140,7 +140,7 @@ Filtering by association allows you to answer API calls such as: "get all users
that are part of organization X":

```
/users?filter[organization]=5bd9aaba-0928-4c01-93c2-b438beca934d
/users?filters[organization]=5bd9aaba-0928-4c01-93c2-b438beca934d
```

```php
Expand Down Expand Up @@ -169,7 +169,7 @@ public function filters(): array
return [
'name' => $this->filter()
->search('name')
->description('Search the name field for the given input');
->description('Search the name field for the given input')->expect()->string();
];
}
```
9 changes: 9 additions & 0 deletions src/Apitizer/Exceptions/DefinitionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ public static function filterHandlerNotDefined(QueryBuilder $queryBuilder, Filte
return new static($message, $queryBuilder, 'filter', $filter->getName());
}

public static function filterExpectRequired(QueryBuilder $queryBuilder, Filter $filter): self
{
$class = get_class($queryBuilder);
$name = $filter->getName();
$message = "Filter [$name] on [$class] should call expect() before defining array element type";

return new static($message, $queryBuilder, 'filter', $filter->getName());
}

/**
* @param QueryBuilder $queryBuilder
* @param string $name
Expand Down
10 changes: 8 additions & 2 deletions src/Apitizer/Support/TypeCaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ private static function doCast($value, $type, $format)
// Null values should evaluate to "false" in the boolean cast,
// which is why this check comes before the null check.
if ($type === 'bool' || $type === 'boolean') {
return (bool) \filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$converted = \filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);

if (\is_null($converted)) {
throw new CastException($value, $type, $format);
}

return $converted;
}

if (is_null($value)) {
if (\is_null($value)) {
return $value;
}

Expand Down
106 changes: 77 additions & 29 deletions src/Apitizer/Types/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Apitizer\Types;

use Apitizer\Exceptions\DefinitionException;
use Apitizer\Exceptions\InvalidInputException;
use Apitizer\Exceptions\CastException;
use Apitizer\Filters\AssociationFilter;
Expand All @@ -23,6 +24,11 @@ class Filter extends Factory
*/
protected $format = null;

/**
* @var array<string> the available enumators.
*/
protected $enums = null;

/**
* If we expect an array of values or just one.
*
Expand All @@ -45,39 +51,22 @@ class Filter extends Factory
*
* To expect an array of types, look at `expectMany`.
*
* @param string $type
* @param null|string $format the format for date(time) value. Defaults to
* 'Y-m-d' for dates, and 'Y-m-d H:i:s' for datetimes. This format is
* ignored for any other type.
*
* @return $this
* @return FilterTypePicker
*/
public function expect(string $type, string $format = null): self
public function expect(): FilterTypePicker
{
$this->expectArray = false;
$this->type = $type;
$this->format = $format;

return $this;
return new FilterTypePicker($this);
}

/**
* Expect an array of the given type as input to the filter.
*
* @param string $type
* @param null|string $format the format for date(time) value. Defaults to
* 'Y-m-d' for dates, and 'Y-m-d H:i:s' for datetimes. This format is
* ignored for any other type.
*
* @return $this
*/
public function expectMany(string $type, string $format = null): self
public function whereEach(): FilterTypePicker
{
$this->expectArray = true;
$this->type = $type;
$this->format = $format;
if ($this->type != "array") {
throw DefinitionException::filterExpectRequired($this->getQueryBuilder(), $this);
}

return $this;
return new FilterTypePicker($this);
}

/**
Expand All @@ -103,7 +92,7 @@ public function handleUsing(callable $handler): self
/**
* Filter by field and operator.
*
* If `expectMany` is used, the operator will be ignored in favour of a
* If an array of input is given, the operator will be ignored in favour of a
* `whereIn` query.
*
* @param string $field
Expand Down Expand Up @@ -153,7 +142,7 @@ public function byAssociation(string $relation, string $key = null): self
*/
public function search($fields): self
{
$this->expect('string');
$this->expect()->string();
$this->handleUsing(new LikeFilter($fields));
$this->description('Search based on the input string');

Expand All @@ -172,20 +161,34 @@ public function search($fields): self
*/
protected function validateInput($input)
{
// Array-input
if ($this->expectArray) {
if (! \is_array($input)) {
if (!\is_array($input)) {
throw InvalidInputException::filterTypeError($this, $input);
}

return array_map(function ($value) {
if ($this->enums) {
foreach ($input as $value) {
if (! in_array($value, $this->enums)) {
throw InvalidInputException::filterTypeError($this, $input);
}
}
}

return \array_map(function ($value) {
return TypeCaster::cast($value, $this->type, $this->format);
}, $input);
}

// Non-array input
if (\is_array($input)) {
throw InvalidInputException::filterTypeError($this, $input);
}

if ($this->enums && ! in_array($input, $this->enums)) {
throw InvalidInputException::filterTypeError($this, $input);
}

return TypeCaster::cast($input, $this->type, $this->format);
}

Expand Down Expand Up @@ -216,6 +219,51 @@ public function setValue($value): self
return $this;
}

/**
* @internal used by FilterTypePicker to set the type of the filter,
*
* @param string $type
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}

/**
* @internal used by FilterTypePicker to set whether or not to expect an array,
*
* @param bool $expectArray
* @return self
*/
public function setExpectArray(bool $expectArray): self
{
$this->expectArray = $expectArray;
return $this;
}

/**
* @internal used by FilterTypePicker to set the formatting.
*
* @param string $format
* @return self
*/
public function setFormatting(string $format): self
{
$this->format = $format;
return $this;
}

/**
* @param array<string> $enums, This is used to check whether the value is an available option.
*/
public function setEnumerators(array $enums): self
{
$this->enums = $enums;
return $this;
}

/**
* Get the expected input type. This is formatted for the documentation.
*/
Expand Down
Loading

0 comments on commit 412f136

Please sign in to comment.