Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
composer.lock
build/
.coveralls.yml
.settings
.project
.buildpath
8 changes: 5 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
13 changes: 13 additions & 0 deletions src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,21 @@ public function index()
$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();

Expand Down
43 changes: 38 additions & 5 deletions src/NilPortugues/Laravel5/JsonApi/Controller/JsonApiTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +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;

trait JsonApiTrait
{
Expand All @@ -34,6 +37,11 @@ trait JsonApiTrait
*/
protected $pageSize = 10;

/**
* @var \Illuminate\Database\Eloquent\Builder
*/
protected $query;

/**
* @param JsonApiSerializer $serializer
*/
Expand Down Expand Up @@ -63,7 +71,7 @@ protected function totalAmountResourceCallable()
return function () {
$idKey = $this->getDataModel()->getKeyName();

return $this->getDataModel()->query()->count([$idKey]);
return $this->query->count([$idKey]);
};
}

Expand All @@ -74,6 +82,29 @@ protected function totalAmountResourceCallable()
*/
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.
*
Expand All @@ -83,7 +114,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();
};
}

Expand Down Expand Up @@ -146,6 +177,7 @@ protected function createResourceCallable()
/**
* @param Request $request
* @param $id
*
* @return Response
*/
protected function putAction(Request $request, $id)
Expand Down Expand Up @@ -187,6 +219,7 @@ protected function updateResourceCallable()
/**
* @param Request $request
* @param $id
*
* @return Response
*/
protected function patchAction(Request $request, $id)
Expand All @@ -201,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)
);
Expand Down
161 changes: 161 additions & 0 deletions src/NilPortugues/Laravel5/JsonApi/Eloquent/EloquentNodeVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

namespace NilPortugues\Laravel5\JsonApi\Eloquent;

use Illuminate\Database\Eloquent\Builder;
use Xiag\Rql\Parser\Glob;
use Xiag\Rql\Parser\Node\AbstractQueryNode;
use Xiag\Rql\Parser\Node\Query\AbstractArrayOperatorNode;
use Xiag\Rql\Parser\Node\Query\AbstractLogicalOperatorNode;
use Xiag\Rql\Parser\Node\Query\AbstractScalarOperatorNode;
use Xiag\Rql\Parser\Query;

/**
* RQL node visitor for constructing Eloquent queries.
*
* @author srottem
*/
class EloquentNodeVisitor
{
/**
* 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);
}
}
12 changes: 12 additions & 0 deletions tests/NilPortugues/App/Controller/EmployeesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)){

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Spaces must be used to indent lines; tabs are not allowed
  • Expected 1 space after SWITCH keyword; 0 found
  • Expected 1 space after closing parenthesis; found 0

case 0:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

$filters = null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

break;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

case 1:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

$filters = $filters[0];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

break;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

default:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

throw new \Exception('Only a single filter is supported at present.');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace found at end of line


$resource = new ListResource($this->serializer, $page, $fields, $sorting, $included, $filters);

Expand Down
5 changes: 5 additions & 0 deletions tests/NilPortugues/App/Transformers/EmployeesTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,9 @@ public function getRelationships()
{
return [];
}

public function getRequiredProperties()
{
return [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

}
}
5 changes: 5 additions & 0 deletions tests/NilPortugues/App/Transformers/OrdersTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,9 @@ public function getRelationships()
{
return [];
}

public function getRequiredProperties()
{
return [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

}
}
12 changes: 11 additions & 1 deletion tests/NilPortugues/Laravel5/JsonApi/JsonApiControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

$response = $this->response;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed


$this->assertEquals(200, $response->getStatusCode());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

$this->assertEquals('application/vnd.api+json', $response->headers->get('Content-type'));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

$this->assertContains('&filter=ne(a,b)', $response->getContent());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces must be used to indent lines; tabs are not allowed

}

public function testListAction()
{
Expand Down