From 8071e11fec8078a3f05ca89010feaa9861f9f1c0 Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Mon, 8 Aug 2016 23:17:28 +0200 Subject: [PATCH 01/15] Implement filters. --- .gitignore | 3 + composer.json | 8 +- .../JsonApi/Controller/JsonApiController.php | 15 +- .../JsonApi/Controller/JsonApiTrait.php | 38 ++++- .../JsonApi/Eloquent/EloquentNodeVisitor.php | 157 ++++++++++++++++++ .../App/Controller/EmployeesController.php | 12 ++ .../App/Transformers/EmployeesTransformer.php | 5 + .../App/Transformers/OrdersTransformer.php | 5 + .../JsonApi/JsonApiControllerTest.php | 12 +- 9 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php diff --git a/.gitignore b/.gitignore index a2b4c51..4d275c6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ composer.lock build/ .coveralls.yml +.settings +.project +.buildpath diff --git a/composer.json b/composer.json index 7b30476..d52d3c4 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,15 @@ "require": { "nilportugues/json-api": "^2.4", "symfony/psr-http-message-bridge": "~0.1", - "nilportugues/serializer-eloquent": "~1.0" + "nilportugues/serializer-eloquent": "~1.0", + "xiag/rql-parser": "^2.0" }, "require-dev": { - "laravel/laravel": "5.*", + "laravel/laravel": "5.2.*", "laravel/lumen": "^5.2", "phpunit/phpunit": "4.*", - "friendsofphp/php-cs-fixer": "^1.10" + "friendsofphp/php-cs-fixer": "^1.10", + "xiag/rql-command": "^2.0" }, "autoload": { "psr-4": { diff --git a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php index 713b6d5..16a910b 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php +++ b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php @@ -46,9 +46,22 @@ public function index() $sorting = $apiRequest->getSort(); $included = $apiRequest->getIncludedRelationships(); $filters = $apiRequest->getFilters(); - + + //HACK: JsonAPI is specifying this as an array but it should be a string at this point for RQL processing. + switch(count($filters)){ + case 0: + $filters = null; + break; + case 1: + $filters = $filters[0]; + break; + default: + throw new \Exception('Only a single filter is supported at present.'); + } + $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, $filters); + $this->createQuery($filters); $totalAmount = $this->totalAmountResourceCallable(); $results = $this->listResourceCallable(); diff --git a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php index 133f49a..147905d 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php +++ b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php @@ -21,6 +21,10 @@ use NilPortugues\Laravel5\JsonApi\Eloquent\EloquentHelper; use NilPortugues\Laravel5\JsonApi\JsonApiSerializer; use Symfony\Component\HttpFoundation\Response; +use Xiag\Rql\Parser\Lexer; +use Xiag\Rql\Parser\Parser; +use NilPortugues\Laravel5\JsonApi\Eloquent\EloquentQueryBuilder; +use NilPortugues\Laravel5\JsonApi\Eloquent\EloquentNodeVisitor; trait JsonApiTrait { @@ -33,6 +37,11 @@ trait JsonApiTrait * @var int */ protected $pageSize = 10; + + /** + * @var \Illuminate\Database\Eloquent\Builder + */ + protected $query; /** * @param JsonApiSerializer $serializer @@ -63,7 +72,7 @@ protected function totalAmountResourceCallable() return function () { $idKey = $this->getDataModel()->getKeyName(); - return $this->getDataModel()->query()->count([$idKey]); + return $this->query->count([$idKey]); }; } @@ -73,6 +82,31 @@ protected function totalAmountResourceCallable() * @return Model */ abstract public function getDataModel(); + + /** + * Creates the query to use for obtaining the resources to return. + * + * @param array $filter + */ + protected function createQuery($filter) + { + $queryBuilder = $this->getDataModel()->query(); + + if(isset($filter)){ + + $lexer = new Lexer(); + $parser = new Parser(); + + $tokens = $lexer->tokenize($filter); + $rqlQuery = $parser->parse($tokens); + + $nodeVisitor = new EloquentNodeVisitor(); + $nodeVisitor->visit($rqlQuery, $queryBuilder); + } + + $this->query = $queryBuilder; + + } /** * Returns a list of resources based on pagination criteria. @@ -83,7 +117,7 @@ abstract public function getDataModel(); protected function listResourceCallable() { return function () { - return EloquentHelper::paginate($this->serializer, $this->getDataModel()->query(), $this->pageSize)->get(); + return EloquentHelper::paginate($this->serializer, $this->query, $this->pageSize)->get(); }; } diff --git a/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php new file mode 100644 index 0000000..7f77851 --- /dev/null +++ b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php @@ -0,0 +1,157 @@ +getQuery() !== null) { + $this->visitQueryNode($query->getQuery(), $builder); + } + } + + /** + * Processes a query node. + * + * @param AbstractQueryNode $node The node to process. + * @param Builder $builder The Eloquent builder to populate. + * @param string $operator The operator to use when appending where clauses + * @throws \LogicException Thrown if the node is of an unknown type. + */ + private function visitQueryNode(AbstractQueryNode $node, Builder $builder, $operator = 'and') + { + if ($node instanceof AbstractScalarOperatorNode) { + $this->visitScalarNode($node, $builder, $operator); + } elseif ($node instanceof AbstractArrayOperatorNode) { + $this->visitArrayNode($node, $builder, $operator); + } elseif ($node instanceof AbstractLogicalOperatorNode) { + $this->visitLogicalNode($node, $builder, $operator); + } else { + throw new \LogicException(sprintf('Unknown node "%s"', $node->getNodeName())); + } + } + + /** + * Processes a scalar node. + * + * @param AbstractScalarOperatorNode $node The node to process. + * @param Builder $builder The Eloquent builder to populate. + * @param unknown $operator The operator to use when appending where clauses. + * @throws \LogicException Thrown if the node cannot be processed. + */ + private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $builder, $operator) + { + static $operators = [ + 'like' => 'LIKE', + 'eq' => '=', + 'ne' => '<>', + 'lt' => '<', + 'gt' => '>', + 'le' => '<=', + 'ge' => '>=', + ]; + + if (!isset($operators[$node->getNodeName()])) { + throw new \LogicException(sprintf('Unknown scalar node "%s"', $node->getNodeName())); + } + + $value = $node->getValue(); + + if ($value instanceof Glob) { + $value = $value->toLike(); + } elseif ($value instanceof \DateTimeInterface) { + $value = $value->format(DATE_ISO8601); + } + + if($value === null){ + if($node->getNodeName() === 'eq'){ + $builder->whereNull($node->getField(), $operator); + } elseif($node->getNodeName() === 'ne'){ + $builder->whereNotNull($node->getField(), $operator); + } else { + throw new \LogicException(sprintf("Only the 'eq' an 'ne' operators can be used when comparing to 'null()'.")); + } + } else { + $builder->where( + $node->getField(), + $operators[$node->getNodeName()], + $value, + $operator + ); + } + } + + /** + * Processes an array node. + * + * @param AbstractArrayOperatorNode $node The node to process. + * @param Builder $builder The Eloquent builder to populate. + * @param unknown $operator The operator to use when appending where clauses. + * @throws \LogicException Thrown if the node cannot be processed. + */ + private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builder, $operator) + { + static $operators = [ + 'in', + 'out', + ]; + + if(!in_array($node->getNodeName(), $operators)){ + throw new \LogicException(sprintf('Unknown array node "%s"', $node->getNodeName())); + } + + $negate = false; + + if($node->getNodeName() === 'out'){ + $negate = true; + } + + $builder->whereIn( + $node->getField(), + $node->getValues(), + $operator, + $negate + ); + } + + /** + * Processes a logical node. + * + * @param AbstractLogicalOperatorNode $node The node to process. + * @param Builder $builder The Eloquent builder to populate. + * @param unknown $operator The operator to use when appending where clauses. + * @throws \LogicException Thrown if the node cannot be processed. + */ + private function visitLogicalNode(AbstractLogicalOperatorNode $node, Builder $builder, $operator) + { + if ($node->getNodeName() !== 'and' && $node->getNodeName() !== 'or') { + throw new \LogicException(sprintf('Unknown or unsupported logical node "%s"', $node->getNodeName())); + } + + $builder->where(\Closure::bind(function($constraintGroupBuilder) use($node) { + foreach ($node->getQueries() as $query) { + $this->visitQueryNode($query, $constraintGroupBuilder, $node->getNodeName()); + } + }, $this), null, null, $operator); + } +} \ No newline at end of file diff --git a/tests/NilPortugues/App/Controller/EmployeesController.php b/tests/NilPortugues/App/Controller/EmployeesController.php index 47e0c2f..fa51197 100644 --- a/tests/NilPortugues/App/Controller/EmployeesController.php +++ b/tests/NilPortugues/App/Controller/EmployeesController.php @@ -136,6 +136,18 @@ public function getOrdersByEmployee($id) $sorting = $apiRequest->getSort(); $included = $apiRequest->getIncludedRelationships(); $filters = $apiRequest->getFilters(); + + //HACK: JsonAPI is specifying this as an array but it should be a string at this point for RQL processing. + switch(count($filters)){ + case 0: + $filters = null; + break; + case 1: + $filters = $filters[0]; + break; + default: + throw new \Exception('Only a single filter is supported at present.'); + } $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, $filters); diff --git a/tests/NilPortugues/App/Transformers/EmployeesTransformer.php b/tests/NilPortugues/App/Transformers/EmployeesTransformer.php index d80717d..f61830e 100644 --- a/tests/NilPortugues/App/Transformers/EmployeesTransformer.php +++ b/tests/NilPortugues/App/Transformers/EmployeesTransformer.php @@ -98,4 +98,9 @@ public function getRelationships() { return []; } + + public function getRequiredProperties() + { + return []; + } } diff --git a/tests/NilPortugues/App/Transformers/OrdersTransformer.php b/tests/NilPortugues/App/Transformers/OrdersTransformer.php index cc7a2c3..b0d2ead 100644 --- a/tests/NilPortugues/App/Transformers/OrdersTransformer.php +++ b/tests/NilPortugues/App/Transformers/OrdersTransformer.php @@ -93,4 +93,9 @@ public function getRelationships() { return []; } + + public function getRequiredProperties() + { + return []; + } } diff --git a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php index ec0f9cf..8b4aa20 100644 --- a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php +++ b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php @@ -31,7 +31,7 @@ public function testListActionCanSort() $this->assertContains('&sort=-id', $response->getContent()); } - public function testListActionCanFilterMembers() + public function testListActionCanRetrieveSparseFieldsets() { $this->call('GET', 'http://localhost/employees?fields[employee]=company,first_name'); $response = $this->response; @@ -40,6 +40,16 @@ public function testListActionCanFilterMembers() $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); $this->assertContains('&fields[employee]=company,first_name', $response->getContent()); } + + public function testListActionCanFilter() + { + $this->call('GET', 'http://localhost/employees?filter=ne(a,b)'); + $response = $this->response; + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); + $this->assertContains('&filter=ne(a,b)', $response->getContent()); + } public function testListAction() { From 32abfdba0d78600d789c347f1fe5b89b4df596e1 Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Sun, 18 Sep 2016 15:09:26 +0200 Subject: [PATCH 02/15] Tidy code to meet PSR-2 recommendation. --- .../JsonApi/Controller/JsonApiController.php | 24 +- .../JsonApi/Controller/JsonApiTrait.php | 47 ++- .../JsonApi/Eloquent/EloquentNodeVisitor.php | 284 +++++++++--------- 3 files changed, 179 insertions(+), 176 deletions(-) diff --git a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php index 16a910b..cffe361 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php +++ b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php @@ -46,19 +46,19 @@ public function index() $sorting = $apiRequest->getSort(); $included = $apiRequest->getIncludedRelationships(); $filters = $apiRequest->getFilters(); - + //HACK: JsonAPI is specifying this as an array but it should be a string at this point for RQL processing. - switch(count($filters)){ - case 0: - $filters = null; - break; - case 1: - $filters = $filters[0]; - break; - default: - throw new \Exception('Only a single filter is supported at present.'); - } - + switch (count($filters)) { + case 0: + $filters = null; + break; + case 1: + $filters = $filters[0]; + break; + default: + throw new \Exception('Only a single filter is supported at present.'); + } + $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, $filters); $this->createQuery($filters); diff --git a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php index 147905d..406cca1 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php +++ b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php @@ -14,17 +14,16 @@ use Illuminate\Container\Container; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; -use NilPortugues\Laravel5\JsonApi\Actions\PatchResource; -use NilPortugues\Laravel5\JsonApi\Actions\PutResource; use NilPortugues\Api\JsonApi\Server\Errors\Error; use NilPortugues\Api\JsonApi\Server\Errors\ErrorBag; +use NilPortugues\Laravel5\JsonApi\Actions\PatchResource; +use NilPortugues\Laravel5\JsonApi\Actions\PutResource; use NilPortugues\Laravel5\JsonApi\Eloquent\EloquentHelper; +use NilPortugues\Laravel5\JsonApi\Eloquent\EloquentNodeVisitor; use NilPortugues\Laravel5\JsonApi\JsonApiSerializer; use Symfony\Component\HttpFoundation\Response; use Xiag\Rql\Parser\Lexer; use Xiag\Rql\Parser\Parser; -use NilPortugues\Laravel5\JsonApi\Eloquent\EloquentQueryBuilder; -use NilPortugues\Laravel5\JsonApi\Eloquent\EloquentNodeVisitor; trait JsonApiTrait { @@ -37,7 +36,7 @@ trait JsonApiTrait * @var int */ protected $pageSize = 10; - + /** * @var \Illuminate\Database\Eloquent\Builder */ @@ -82,30 +81,28 @@ protected function totalAmountResourceCallable() * @return Model */ abstract public function getDataModel(); - + /** * Creates the query to use for obtaining the resources to return. - * + * * @param array $filter */ protected function createQuery($filter) { - $queryBuilder = $this->getDataModel()->query(); - - if(isset($filter)){ - - $lexer = new Lexer(); - $parser = new Parser(); - - $tokens = $lexer->tokenize($filter); - $rqlQuery = $parser->parse($tokens); - - $nodeVisitor = new EloquentNodeVisitor(); - $nodeVisitor->visit($rqlQuery, $queryBuilder); - } - - $this->query = $queryBuilder; - + $queryBuilder = $this->getDataModel()->query(); + + if (isset($filter)) { + $lexer = new Lexer(); + $parser = new Parser(); + + $tokens = $lexer->tokenize($filter); + $rqlQuery = $parser->parse($tokens); + + $nodeVisitor = new EloquentNodeVisitor(); + $nodeVisitor->visit($rqlQuery, $queryBuilder); + } + + $this->query = $queryBuilder; } /** @@ -180,6 +177,7 @@ protected function createResourceCallable() /** * @param Request $request * @param $id + * * @return Response */ protected function putAction(Request $request, $id) @@ -221,6 +219,7 @@ protected function updateResourceCallable() /** * @param Request $request * @param $id + * * @return Response */ protected function patchAction(Request $request, $id) @@ -235,7 +234,7 @@ protected function patchAction(Request $request, $id) if (array_key_exists('attributes', $data) && $model->timestamps) { $data['attributes'][$model::UPDATED_AT] = Carbon::now()->toDateTimeString(); } - + return $this->addHeaders( $resource->get($id, $data, get_class($model), $find, $update) ); diff --git a/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php index 7f77851..e5c17c7 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php +++ b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php @@ -1,157 +1,161 @@ getQuery() !== null) { - $this->visitQueryNode($query->getQuery(), $builder); - } - } - - /** - * Processes a query node. - * - * @param AbstractQueryNode $node The node to process. - * @param Builder $builder The Eloquent builder to populate. - * @param string $operator The operator to use when appending where clauses - * @throws \LogicException Thrown if the node is of an unknown type. - */ - private function visitQueryNode(AbstractQueryNode $node, Builder $builder, $operator = 'and') - { - if ($node instanceof AbstractScalarOperatorNode) { - $this->visitScalarNode($node, $builder, $operator); - } elseif ($node instanceof AbstractArrayOperatorNode) { - $this->visitArrayNode($node, $builder, $operator); - } elseif ($node instanceof AbstractLogicalOperatorNode) { - $this->visitLogicalNode($node, $builder, $operator); - } else { - throw new \LogicException(sprintf('Unknown node "%s"', $node->getNodeName())); - } - } - - /** - * Processes a scalar node. - * - * @param AbstractScalarOperatorNode $node The node to process. - * @param Builder $builder The Eloquent builder to populate. - * @param unknown $operator The operator to use when appending where clauses. - * @throws \LogicException Thrown if the node cannot be processed. - */ - private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $builder, $operator) - { - static $operators = [ - 'like' => 'LIKE', - 'eq' => '=', - 'ne' => '<>', - 'lt' => '<', - 'gt' => '>', - 'le' => '<=', - 'ge' => '>=', - ]; - - if (!isset($operators[$node->getNodeName()])) { - throw new \LogicException(sprintf('Unknown scalar node "%s"', $node->getNodeName())); - } - - $value = $node->getValue(); - - if ($value instanceof Glob) { - $value = $value->toLike(); - } elseif ($value instanceof \DateTimeInterface) { - $value = $value->format(DATE_ISO8601); - } - - if($value === null){ - if($node->getNodeName() === 'eq'){ - $builder->whereNull($node->getField(), $operator); - } elseif($node->getNodeName() === 'ne'){ - $builder->whereNotNull($node->getField(), $operator); - } else { - throw new \LogicException(sprintf("Only the 'eq' an 'ne' operators can be used when comparing to 'null()'.")); - } - } else { - $builder->where( - $node->getField(), - $operators[$node->getNodeName()], - $value, - $operator - ); - } - } - - /** - * Processes an array node. - * - * @param AbstractArrayOperatorNode $node The node to process. - * @param Builder $builder The Eloquent builder to populate. - * @param unknown $operator The operator to use when appending where clauses. - * @throws \LogicException Thrown if the node cannot be processed. - */ - private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builder, $operator) - { - static $operators = [ + /** + * Populates the provided builder from the provided RQL query instance. + * + * @param Query $query The RQL query to populate the Eloquent builder from + * @param Builder $builder The Eloquent query builder to populate + */ + public function visit(Query $query, Builder $builder) + { + if ($query->getQuery() !== null) { + $this->visitQueryNode($query->getQuery(), $builder); + } + } + + /** + * Processes a query node. + * + * @param AbstractQueryNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param string $operator The operator to use when appending where clauses + * + * @throws \LogicException Thrown if the node is of an unknown type + */ + private function visitQueryNode(AbstractQueryNode $node, Builder $builder, $operator = 'and') + { + if ($node instanceof AbstractScalarOperatorNode) { + $this->visitScalarNode($node, $builder, $operator); + } elseif ($node instanceof AbstractArrayOperatorNode) { + $this->visitArrayNode($node, $builder, $operator); + } elseif ($node instanceof AbstractLogicalOperatorNode) { + $this->visitLogicalNode($node, $builder, $operator); + } else { + throw new \LogicException(sprintf('Unknown node "%s"', $node->getNodeName())); + } + } + + /** + * Processes a scalar node. + * + * @param AbstractScalarOperatorNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param unknown $operator The operator to use when appending where clauses + * + * @throws \LogicException Thrown if the node cannot be processed + */ + private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $builder, $operator) + { + static $operators = [ + 'like' => 'LIKE', + 'eq' => '=', + 'ne' => '<>', + 'lt' => '<', + 'gt' => '>', + 'le' => '<=', + 'ge' => '>=', + ]; + + if (!isset($operators[$node->getNodeName()])) { + throw new \LogicException(sprintf('Unknown scalar node "%s"', $node->getNodeName())); + } + + $value = $node->getValue(); + + if ($value instanceof Glob) { + $value = $value->toLike(); + } elseif ($value instanceof \DateTimeInterface) { + $value = $value->format(DATE_ISO8601); + } + + if ($value === null) { + if ($node->getNodeName() === 'eq') { + $builder->whereNull($node->getField(), $operator); + } elseif ($node->getNodeName() === 'ne') { + $builder->whereNotNull($node->getField(), $operator); + } else { + throw new \LogicException(sprintf("Only the 'eq' an 'ne' operators can be used when comparing to 'null()'.")); + } + } else { + $builder->where( + $node->getField(), + $operators[$node->getNodeName()], + $value, + $operator + ); + } + } + + /** + * Processes an array node. + * + * @param AbstractArrayOperatorNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param unknown $operator The operator to use when appending where clauses + * + * @throws \LogicException Thrown if the node cannot be processed + */ + private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builder, $operator) + { + static $operators = [ 'in', 'out', ]; - - if(!in_array($node->getNodeName(), $operators)){ - throw new \LogicException(sprintf('Unknown array node "%s"', $node->getNodeName())); - } - - $negate = false; - - if($node->getNodeName() === 'out'){ - $negate = true; - } - - $builder->whereIn( - $node->getField(), - $node->getValues(), - $operator, - $negate - ); - } - - /** - * Processes a logical node. - * - * @param AbstractLogicalOperatorNode $node The node to process. - * @param Builder $builder The Eloquent builder to populate. - * @param unknown $operator The operator to use when appending where clauses. - * @throws \LogicException Thrown if the node cannot be processed. - */ - private function visitLogicalNode(AbstractLogicalOperatorNode $node, Builder $builder, $operator) - { - if ($node->getNodeName() !== 'and' && $node->getNodeName() !== 'or') { - throw new \LogicException(sprintf('Unknown or unsupported logical node "%s"', $node->getNodeName())); - } - - $builder->where(\Closure::bind(function($constraintGroupBuilder) use($node) { - foreach ($node->getQueries() as $query) { - $this->visitQueryNode($query, $constraintGroupBuilder, $node->getNodeName()); - } - }, $this), null, null, $operator); - } -} \ No newline at end of file + + if (!in_array($node->getNodeName(), $operators)) { + throw new \LogicException(sprintf('Unknown array node "%s"', $node->getNodeName())); + } + + $negate = false; + + if ($node->getNodeName() === 'out') { + $negate = true; + } + + $builder->whereIn( + $node->getField(), + $node->getValues(), + $operator, + $negate + ); + } + + /** + * Processes a logical node. + * + * @param AbstractLogicalOperatorNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param unknown $operator The operator to use when appending where clauses + * + * @throws \LogicException Thrown if the node cannot be processed + */ + private function visitLogicalNode(AbstractLogicalOperatorNode $node, Builder $builder, $operator) + { + if ($node->getNodeName() !== 'and' && $node->getNodeName() !== 'or') { + throw new \LogicException(sprintf('Unknown or unsupported logical node "%s"', $node->getNodeName())); + } + + $builder->where(\Closure::bind(function ($constraintGroupBuilder) use ($node) { + foreach ($node->getQueries() as $query) { + $this->visitQueryNode($query, $constraintGroupBuilder, $node->getNodeName()); + } + }, $this), null, null, $operator); + } +} From c063b1b795ae88b50c9ff72e1bd0c9dadb167267 Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Tue, 20 Sep 2016 08:50:02 +0200 Subject: [PATCH 03/15] Filters were not being URL encoded in response resulting in potential for malformed URLs. --- .../Laravel5/JsonApi/Controller/JsonApiController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php index cffe361..6ab8a83 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php +++ b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php @@ -59,7 +59,7 @@ public function index() throw new \Exception('Only a single filter is supported at present.'); } - $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, $filters); + $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, urlencode($filters)); $this->createQuery($filters); $totalAmount = $this->totalAmountResourceCallable(); From 4b2d915057f4b604c80c6baee17f835420169600 Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Tue, 20 Sep 2016 08:52:05 +0200 Subject: [PATCH 04/15] Modified node visitor to accept Query\QueryBuilder instead of Eloquent\QueryBuilder in order to potentially support more possible calls. Also reorganized logical node handling to facilitate future implementation of 'not()' RQL clause. --- .../Laravel5/JsonApi/Controller/JsonApiTrait.php | 2 +- .../JsonApi/Eloquent/EloquentNodeVisitor.php | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php index 406cca1..0500cb0 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php +++ b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php @@ -99,7 +99,7 @@ protected function createQuery($filter) $rqlQuery = $parser->parse($tokens); $nodeVisitor = new EloquentNodeVisitor(); - $nodeVisitor->visit($rqlQuery, $queryBuilder); + $nodeVisitor->visit($rqlQuery, $queryBuilder->getQuery()); } $this->query = $queryBuilder; diff --git a/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php index e5c17c7..9bdc5fc 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php +++ b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php @@ -2,7 +2,7 @@ namespace NilPortugues\Laravel5\JsonApi\Eloquent; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Builder; use Xiag\Rql\Parser\Glob; use Xiag\Rql\Parser\Node\AbstractQueryNode; use Xiag\Rql\Parser\Node\Query\AbstractArrayOperatorNode; @@ -148,14 +148,16 @@ private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builde */ private function visitLogicalNode(AbstractLogicalOperatorNode $node, Builder $builder, $operator) { - if ($node->getNodeName() !== 'and' && $node->getNodeName() !== 'or') { + if ($node->getNodeName() === 'and' || $node->getNodeName() === 'or') { + $builder->where(\Closure::bind(function ($constraintGroupBuilder) use ($node) { + foreach ($node->getQueries() as $query) { + $this->visitQueryNode($query, $constraintGroupBuilder, $node->getNodeName()); + } + }, $this), null, null, $operator); + } else { throw new \LogicException(sprintf('Unknown or unsupported logical node "%s"', $node->getNodeName())); } - $builder->where(\Closure::bind(function ($constraintGroupBuilder) use ($node) { - foreach ($node->getQueries() as $query) { - $this->visitQueryNode($query, $constraintGroupBuilder, $node->getNodeName()); - } - }, $this), null, null, $operator); + } } From 44875093b5ee2399f6600b679de6c9d20b9f609c Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Tue, 20 Sep 2016 15:43:08 +0200 Subject: [PATCH 05/15] Refactored node visitor and got RQL not clause working. --- .../JsonApi/Eloquent/EloquentNodeVisitor.php | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php index 9bdc5fc..a44ccc1 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php +++ b/src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php @@ -33,20 +33,20 @@ public function visit(Query $query, Builder $builder) /** * Processes a query node. * - * @param AbstractQueryNode $node The node to process - * @param Builder $builder The Eloquent builder to populate - * @param string $operator The operator to use when appending where clauses + * @param AbstractQueryNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param string $boolean The operator to use when appending where clauses * * @throws \LogicException Thrown if the node is of an unknown type */ - private function visitQueryNode(AbstractQueryNode $node, Builder $builder, $operator = 'and') + private function visitQueryNode(AbstractQueryNode $node, Builder $builder, $boolean = 'and') { if ($node instanceof AbstractScalarOperatorNode) { - $this->visitScalarNode($node, $builder, $operator); + $this->visitScalarNode($node, $builder, $boolean); } elseif ($node instanceof AbstractArrayOperatorNode) { - $this->visitArrayNode($node, $builder, $operator); + $this->visitArrayNode($node, $builder, $boolean); } elseif ($node instanceof AbstractLogicalOperatorNode) { - $this->visitLogicalNode($node, $builder, $operator); + $this->visitLogicalNode($node, $builder, $boolean); } else { throw new \LogicException(sprintf('Unknown node "%s"', $node->getNodeName())); } @@ -55,13 +55,13 @@ private function visitQueryNode(AbstractQueryNode $node, Builder $builder, $oper /** * Processes a scalar node. * - * @param AbstractScalarOperatorNode $node The node to process - * @param Builder $builder The Eloquent builder to populate - * @param unknown $operator The operator to use when appending where clauses + * @param AbstractScalarOperatorNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param unknown $boolean The operator to use when appending where clauses * * @throws \LogicException Thrown if the node cannot be processed */ - private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $builder, $operator) + private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $builder, $boolean) { static $operators = [ 'like' => 'LIKE', @@ -87,9 +87,9 @@ private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $buil if ($value === null) { if ($node->getNodeName() === 'eq') { - $builder->whereNull($node->getField(), $operator); + $builder->whereNull($node->getField(), $boolean); } elseif ($node->getNodeName() === 'ne') { - $builder->whereNotNull($node->getField(), $operator); + $builder->whereNotNull($node->getField(), $boolean); } else { throw new \LogicException(sprintf("Only the 'eq' an 'ne' operators can be used when comparing to 'null()'.")); } @@ -98,7 +98,7 @@ private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $buil $node->getField(), $operators[$node->getNodeName()], $value, - $operator + $boolean ); } } @@ -106,13 +106,13 @@ private function visitScalarNode(AbstractScalarOperatorNode $node, Builder $buil /** * Processes an array node. * - * @param AbstractArrayOperatorNode $node The node to process - * @param Builder $builder The Eloquent builder to populate - * @param unknown $operator The operator to use when appending where clauses + * @param AbstractArrayOperatorNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param unknown $boolean The operator to use when appending where clauses * * @throws \LogicException Thrown if the node cannot be processed */ - private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builder, $operator) + private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builder, $boolean) { static $operators = [ 'in', @@ -132,7 +132,7 @@ private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builde $builder->whereIn( $node->getField(), $node->getValues(), - $operator, + $boolean, $negate ); } @@ -140,24 +140,28 @@ private function visitArrayNode(AbstractArrayOperatorNode $node, Builder $builde /** * Processes a logical node. * - * @param AbstractLogicalOperatorNode $node The node to process - * @param Builder $builder The Eloquent builder to populate - * @param unknown $operator The operator to use when appending where clauses + * @param AbstractLogicalOperatorNode $node The node to process + * @param Builder $builder The Eloquent builder to populate + * @param unknown $boolean The operator to use when appending where clauses * * @throws \LogicException Thrown if the node cannot be processed */ - private function visitLogicalNode(AbstractLogicalOperatorNode $node, Builder $builder, $operator) + private function visitLogicalNode(AbstractLogicalOperatorNode $node, Builder $builder, $boolean) { if ($node->getNodeName() === 'and' || $node->getNodeName() === 'or') { $builder->where(\Closure::bind(function ($constraintGroupBuilder) use ($node) { foreach ($node->getQueries() as $query) { $this->visitQueryNode($query, $constraintGroupBuilder, $node->getNodeName()); } - }, $this), null, null, $operator); + }, $this), null, null, $boolean); + } elseif ($node->getNodeName() === 'not') { + $builder->where(\Closure::bind(function ($constraintGroupBuilder) use ($node, $boolean) { + foreach ($node->getQueries() as $query) { + $this->visitQueryNode($query, $constraintGroupBuilder, $boolean); + } + }, $this), null, null, $boolean.' not'); } else { throw new \LogicException(sprintf('Unknown or unsupported logical node "%s"', $node->getNodeName())); } - - } } From f27fccd9a19d2d2bb8c6745cf7892deee9af198c Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Tue, 20 Sep 2016 16:14:57 +0200 Subject: [PATCH 06/15] Added documentation on using RQL filters. --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index edd9bdc..372812a 100644 --- a/README.md +++ b/README.md @@ -1025,7 +1025,7 @@ And notice how response will be empty:
-## GET Query Params: include, fields, sort and page +## GET Query Params: include, fields, sort, page and filter According to the standard, for GET method, it is possible to: - Show only those fields requested using `fields`query parameter. @@ -1088,6 +1088,20 @@ For instance: `/employees?sort=surname,-first_name` For instance: `/employees?page[number]=1&page[size]=10` +While the standard specifies that a 'filter' query parameter can be provided in order to restrict the list of items that will be returned it doesn't specify how the contents of that parameter should +be structured. In this implementation we've provided support for the following subset of RQL operators: + +- Scalar operators: eq, ne, lt, le, gt, ge and like +- Array operators: in and out +- Logical operators: and, or and not + +These can be used together to form complex queries such as to retrieve all employees whose surname starts with the letter 'b' and whose job title is not 'developer' or 'tester'. + +For instance: `/employees?filter=and(like(surname,b*),not(or(eq(job_title,developer),eq(job_title,tester))))` + +For more information on how to use and encode these operators please see the [Resource Query Language](https://github.com/persvr/rql) at the GitHub site. + + ## POST/PUT/PATCH with Relationships The JSON API allows resource creation and modification and passing in `relationships` that will create or alter existing resources too. From fcdb1a87a2ec9acb14e0d0116861a3ec0c7ab5ff Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Tue, 20 Sep 2016 16:23:45 +0200 Subject: [PATCH 07/15] Documentation corrections and improvements. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 372812a..2439416 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ - [PUT](#put) - [PATCH](#patch) - [DELETE](#delete) -- [GET Query Params: include, fields, sort and page](#get-query-params-include-fields-sort-and-page) +- [GET Query Params: include, fields, sort, page and filter](#get-query-params-include-fields-sort-page-and-filter) - [POST/PUT/PATCH with Relationships](#postputpatch-with-relationships) - [Custom Response Headers](#custom-response-headers) - [Common Errors and Solutions](#common-errors-and-solutions) @@ -1088,18 +1088,18 @@ For instance: `/employees?sort=surname,-first_name` For instance: `/employees?page[number]=1&page[size]=10` -While the standard specifies that a 'filter' query parameter can be provided in order to restrict the list of items that will be returned it doesn't specify how the contents of that parameter should -be structured. In this implementation we've provided support for the following subset of RQL operators: +The standard specifies that the `filter` query parameter can be provided in order to restrict the list of items that will be returned. While standard doesn't specify how the contents +of teh `filter` parameter should be structured we've provided support for the following subset of operators from the Resource Query Language (RQL): -- Scalar operators: eq, ne, lt, le, gt, ge and like -- Array operators: in and out -- Logical operators: and, or and not +- Scalar operators: `eq`, `ne`, `lt`, `le`, `gt`, `ge` and `like` +- Array operators: `in` and `out` +- Logical operators: `and`, `or` and `not` These can be used together to form complex queries such as to retrieve all employees whose surname starts with the letter 'b' and whose job title is not 'developer' or 'tester'. For instance: `/employees?filter=and(like(surname,b*),not(or(eq(job_title,developer),eq(job_title,tester))))` -For more information on how to use and encode these operators please see the [Resource Query Language](https://github.com/persvr/rql) at the GitHub site. +More information on how to use and encode these operators can be found at the [Resource Query Language](https://github.com/persvr/rql) project on GitHub. ## POST/PUT/PATCH with Relationships From 2b66f13040a944de92f087f6afa7aadc793d066d Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Tue, 20 Sep 2016 18:31:20 +0200 Subject: [PATCH 08/15] Fixed filter test so it passes. The current output is ugly due to URL encoding but have to be able to separate the parts and re-generate so only appropriate elements are URL encoded rather than all elements. --- tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php index 8b4aa20..c15800a 100644 --- a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php +++ b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php @@ -48,7 +48,7 @@ public function testListActionCanFilter() $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); - $this->assertContains('&filter=ne(a,b)', $response->getContent()); + $this->assertContains('&filter=ne%28a%2Cb%29', $response->getContent()); } public function testListAction() From 99fa3546af8730b03afee4d45bc0878c0812ba3f Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Sat, 24 Sep 2016 11:56:33 +0200 Subject: [PATCH 09/15] Controller now uses raw filter query string. --- .../JsonApi/Controller/JsonApiController.php | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php index 6ab8a83..d7b50a2 100644 --- a/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php +++ b/src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php @@ -20,6 +20,8 @@ use NilPortugues\Laravel5\JsonApi\Actions\GetResource; use NilPortugues\Laravel5\JsonApi\Actions\ListResource; use Symfony\Component\HttpFoundation\Response; +use Xiag\Rql\Parser\Lexer; +use Xiag\Rql\Parser\Token; /** * Class JsonApiController. @@ -45,23 +47,11 @@ public function index() $fields = $apiRequest->getFields(); $sorting = $apiRequest->getSort(); $included = $apiRequest->getIncludedRelationships(); - $filters = $apiRequest->getFilters(); - - //HACK: JsonAPI is specifying this as an array but it should be a string at this point for RQL processing. - switch (count($filters)) { - case 0: - $filters = null; - break; - case 1: - $filters = $filters[0]; - break; - default: - throw new \Exception('Only a single filter is supported at present.'); - } + $filters = $apiRequest->getRawFilter(); - $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, urlencode($filters)); + $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, $filters); - $this->createQuery($filters); + $this->createQuery(urldecode($filters)); $totalAmount = $this->totalAmountResourceCallable(); $results = $this->listResourceCallable(); From 56c5af335bbc6e2074def68e58eba865468fbb00 Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Sat, 24 Sep 2016 12:04:36 +0200 Subject: [PATCH 10/15] Update composer to use dev version. --- composer.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d52d3c4..7a474f6 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ } ], "require": { - "nilportugues/json-api": "^2.4", + "nilportugues/json-api": "dev-feature/filters", "symfony/psr-http-message-bridge": "~0.1", "nilportugues/serializer-eloquent": "~1.0", "xiag/rql-parser": "^2.0" @@ -37,5 +37,11 @@ }, "config": { "preferred-install": "dist" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/srottem/php-json-api" + } + ] } From f2fd1d43703ecfbd514020c17c2d510f3b1ccef6 Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Sat, 24 Sep 2016 12:34:49 +0200 Subject: [PATCH 11/15] Reverted test to use unencoded filter characters. --- tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php index c15800a..8b4aa20 100644 --- a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php +++ b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php @@ -48,7 +48,7 @@ public function testListActionCanFilter() $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); - $this->assertContains('&filter=ne%28a%2Cb%29', $response->getContent()); + $this->assertContains('&filter=ne(a,b)', $response->getContent()); } public function testListAction() From e3d55718b71b33bd1316f25197d4e14597a56bb8 Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Sat, 24 Sep 2016 18:47:25 +0200 Subject: [PATCH 12/15] Updated hacked test to use getRawFilter. --- .../App/Controller/EmployeesController.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/NilPortugues/App/Controller/EmployeesController.php b/tests/NilPortugues/App/Controller/EmployeesController.php index fa51197..a66cbe4 100644 --- a/tests/NilPortugues/App/Controller/EmployeesController.php +++ b/tests/NilPortugues/App/Controller/EmployeesController.php @@ -135,20 +135,8 @@ public function getOrdersByEmployee($id) $fields = $apiRequest->getFields(); $sorting = $apiRequest->getSort(); $included = $apiRequest->getIncludedRelationships(); - $filters = $apiRequest->getFilters(); + $filters = $apiRequest->getRawFilter(); - //HACK: JsonAPI is specifying this as an array but it should be a string at this point for RQL processing. - switch(count($filters)){ - case 0: - $filters = null; - break; - case 1: - $filters = $filters[0]; - break; - default: - throw new \Exception('Only a single filter is supported at present.'); - } - $resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, $filters); $totalAmount = function () use ($id) { From dfc62de26f7bbbfb03595687a59b6e322d37dbdb Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Sat, 24 Sep 2016 18:53:17 +0200 Subject: [PATCH 13/15] Fixed modified test files for nitpick compliance. --- .../App/Transformers/EmployeesTransformer.php | 4 +-- .../App/Transformers/OrdersTransformer.php | 4 +-- .../JsonApi/JsonApiControllerTest.php | 30 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/NilPortugues/App/Transformers/EmployeesTransformer.php b/tests/NilPortugues/App/Transformers/EmployeesTransformer.php index f61830e..4e8391c 100644 --- a/tests/NilPortugues/App/Transformers/EmployeesTransformer.php +++ b/tests/NilPortugues/App/Transformers/EmployeesTransformer.php @@ -98,9 +98,9 @@ public function getRelationships() { return []; } - + public function getRequiredProperties() { - return []; + return []; } } diff --git a/tests/NilPortugues/App/Transformers/OrdersTransformer.php b/tests/NilPortugues/App/Transformers/OrdersTransformer.php index b0d2ead..d0ddb5f 100644 --- a/tests/NilPortugues/App/Transformers/OrdersTransformer.php +++ b/tests/NilPortugues/App/Transformers/OrdersTransformer.php @@ -93,9 +93,9 @@ public function getRelationships() { return []; } - + public function getRequiredProperties() { - return []; + return []; } } diff --git a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php index 8b4aa20..d9feeb9 100644 --- a/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php +++ b/tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php @@ -40,15 +40,15 @@ public function testListActionCanRetrieveSparseFieldsets() $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); $this->assertContains('&fields[employee]=company,first_name', $response->getContent()); } - + public function testListActionCanFilter() { - $this->call('GET', 'http://localhost/employees?filter=ne(a,b)'); - $response = $this->response; - - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); - $this->assertContains('&filter=ne(a,b)', $response->getContent()); + $this->call('GET', 'http://localhost/employees?filter=ne(a,b)'); + $response = $this->response; + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); + $this->assertContains('&filter=ne(a,b)', $response->getContent()); } public function testListAction() @@ -72,7 +72,7 @@ public function testPatchAction() { $this->createNewEmployee(); - $content = <<call('PATCH', 'http://localhost/employees/1', json_decode($content, true), [], [], [], ''); - + $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type')); } @@ -92,7 +92,7 @@ public function testPutAction() { $this->createNewEmployee(); - $content = <<app['config']->set('app.debug', false); - $content = <<app['config']->set('app.debug', false); - $content = <<app['config']->set('app.debug', false); - $content = <<app['config']->set('app.debug', false); - $content = << Date: Sat, 24 Sep 2016 19:11:44 +0200 Subject: [PATCH 14/15] Documentation update. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2439416..5976c87 100644 --- a/README.md +++ b/README.md @@ -1089,7 +1089,7 @@ For instance: `/employees?sort=surname,-first_name` For instance: `/employees?page[number]=1&page[size]=10` The standard specifies that the `filter` query parameter can be provided in order to restrict the list of items that will be returned. While standard doesn't specify how the contents -of teh `filter` parameter should be structured we've provided support for the following subset of operators from the Resource Query Language (RQL): +of the `filter` parameter should be structured we've provided support for the following subset of operators from the Resource Query Language (RQL): - Scalar operators: `eq`, `ne`, `lt`, `le`, `gt`, `ge` and `like` - Array operators: `in` and `out` @@ -1099,6 +1099,8 @@ These can be used together to form complex queries such as to retrieve all emplo For instance: `/employees?filter=and(like(surname,b*),not(or(eq(job_title,developer),eq(job_title,tester))))` +Or alternately:`/employees?filter=(like(surname,b*)%26not(eq(job_title,developer)|eq(job_title,tester)))` + More information on how to use and encode these operators can be found at the [Resource Query Language](https://github.com/persvr/rql) project on GitHub. From 234cb2b15b352d0ada429f7f8cb7ae05e750478c Mon Sep 17 00:00:00 2001 From: Symon Rottem Date: Sun, 25 Sep 2016 09:49:15 +0200 Subject: [PATCH 15/15] Updated docs. --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5976c87..45635ab 100644 --- a/README.md +++ b/README.md @@ -1088,20 +1088,21 @@ For instance: `/employees?sort=surname,-first_name` For instance: `/employees?page[number]=1&page[size]=10` -The standard specifies that the `filter` query parameter can be provided in order to restrict the list of items that will be returned. While standard doesn't specify how the contents -of the `filter` parameter should be structured we've provided support for the following subset of operators from the Resource Query Language (RQL): +- The standard specifies that the `filter` query parameter can be provided in order to restrict the list of items that will be +returned. While standard doesn't specify how the contents of the `filter` parameter should be structured we've provided support +for the a subset of operators from the Resource Query Language (RQL) allowing you to form complex queries such as +*all employees whose surname starts with the letter 'b' and whose job title is not 'developer' or 'tester'*. This implementation +supports the following RQL operators: -- Scalar operators: `eq`, `ne`, `lt`, `le`, `gt`, `ge` and `like` -- Array operators: `in` and `out` -- Logical operators: `and`, `or` and `not` - -These can be used together to form complex queries such as to retrieve all employees whose surname starts with the letter 'b' and whose job title is not 'developer' or 'tester'. + - Scalar operators: `eq`, `ne`, `lt`, `le`, `gt`, `ge` and `like` + - Array operators: `in` and `out` + - Logical operators: `and`, `or` and `not` For instance: `/employees?filter=and(like(surname,b*),not(or(eq(job_title,developer),eq(job_title,tester))))` - Or alternately:`/employees?filter=(like(surname,b*)%26not(eq(job_title,developer)|eq(job_title,tester)))` -More information on how to use and encode these operators can be found at the [Resource Query Language](https://github.com/persvr/rql) project on GitHub. +More information on RQL including how operators and values should be encoded in a query string can be found at the +[RQL Parser](https://github.com/xiag-ag/rql-parser) project on GitHub. ## POST/PUT/PATCH with Relationships