Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
Johannes Schobel committed Jan 8, 2017
1 parent 1cd3bd1 commit 9ebdd9a
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 116 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

#### 2017-08-01
Doing some refactoring. Plugin is not compatible with old version anymore.
Please see the installation instruction on the readme file.

The plugin, however, still provides the same functionality.

#### 2016-09-09
Initial version
* configuration file
Expand Down
103 changes: 50 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,65 @@
# dingo-query-mapper
Uses Dingo/API Request Query Parameters to filter Laravel Models
Uses Dingo/API Request Query Parameters to filter Laravel Collections.
For example, you are able to automatically filter, sort, and limit collections based on query parameters of the URI.

# Installation
First, add the respective line to your composer file
```json
``` json
"require" : {
...,
"johannesschobel/dingoquerymapper": "dev-master"
... ,
"johannesschobel/dingoquerymapper": "dev-master" ,
}
```
and run `composer install` to install the new component.

and run `composer install` to install the new component.

Then add respective `ServiceProvider` from the package to your `config/app.php` configuration file, like this:
```php

``` php
'providers' => [
...,
... ,
JohannesSchobel\DingoQueryMapper\DingoQueryMapperServiceProvider::class,
],
```

If you want, you can overwrite the basic configuration using the following command:

```php
php artisan vendor:publish --provider="JohannesSchobel\DingoQueryMapper\DingoQueryMapperServiceProvider" --tag="config"
```

This will copy the `dingoquerymapper` configuration file to your `config` folder. Using this file, you can
customize the `limit` parameter or the query parameters to be excluded from the service provider.
This is handy, if you need to omit parameters from automatically parsed for filtering (e.g., you may want to
exclude the `token` parameter if you are using `JWT`).

# Usage
## Example
In order to use the plugin, simply create a new instance from `DingoQueryMapperBuilder` and pass the request.
The rest is handled by the `Builder` itself.
In order to use the plugin, simply create a new instance from `DingoQueryMapper` and pass the request.
Furthermore, you want to call respective `createFromXXX()` method from the `DingoQueryMapper`, which basically
allows to create the mapper from various inputs (e.g., `Collection`, `Builder`, ...)

Consider the following example:

```php
public function getAllUsers(Request $request) {
public function getAllUsers(Request $request) {

$users = new DingoQueryMapperBuilder(new User, $request);
$users = $users->build()->getAndPaginate();
$users = User::all()->get();
$qm = new DingoQueryMapper($request);
$users = $qm->createFromCollection($users)->paginate();

// now return the result
return response->json($users);
}
// now return the result
return response->json($users);
}
```

If you call the respective URI, for example like so:
```php
/index?name=j*&age>=18&limit=10
/index?name=j*&age>=18&limit=10&page=3
```
the `$users` will only contain (maximum) `10` users, where the `name` starts with `j` and the `age` is above `18`.

The corresponding eloquent request would look like this:
```php
$users = User::where('name', 'like', 'j%')
->where('age', '>=', '18')
->take(10);
```

Of course you can pass an existing `Builder` instead of a `Model`. This way, you can append to the builder.

Consider the following example:
```php
public function getAllActiveUsers(Request $request) {

$builder = User::where('is_active', '=', true);
$users = new DingoQueryMapperBuilder($builder, $request);
$users = $users->build()->getAndPaginate();

// now return the result
return response->json($users);
}
```

would first filter all `active` users and then apply the custom filters (or sort order), like this:
```php
$users = User::where('is_active', '=', true)
->where('name', 'like', 'j%')
->where('age', '>=', '18')
->take(10);
```
it will output (a maximum of) `10 User`s per page, where the `name` attribute starts with `j` and the `age` is
greater or equal than `18` starting on `page 3`.

## Dingo/API Example
If you use [Dingo/API](https://github.com/dingo/api) as your preferred API framework, you can use this package right
Expand All @@ -89,8 +70,17 @@ varies because you need to use Dingo's response objects.

You can simply return your results using
```php
public function getAllUsers(Request $request) {

$users = User::all()->get();
$qm = new DingoQueryMapper($request);
$users = $qm->createFromCollection($users)->paginate();

// now return the result

return $this->response
->paginator($users, new UserTransformer());
}
```

That's all - really!
Expand All @@ -105,12 +95,12 @@ In order to limit the amount of response elements, simply add respective `limit`
/index?limit=20
```

This will only return the (first) `20` entries of the resultset.
In order to request the next `20` entries, simply add a `page` parameter, for example
This will only return the (first) `20` entries of the result.
In order to request the next `20` entries, simply add a `page` parameter to the query, like this:
```php
/index?limit=20&page=2
```
will return the next `20` entries, located on page `2`.
will return the next `20` entries that are located on `page 2`.


### `sort`
Expand All @@ -120,7 +110,7 @@ For example
```php
/index?sort=age,-name
```
sorts the results by age (ascending), then by name (descending; note the `-`before the `name` field!)
sorts the results by age (ascending), then by name (descending; note the `-` before the `name` field!)

### `custom filters`
Of course you may pass custom query parameters to the builder in order to `filter` the requested data.
Expand All @@ -130,12 +120,19 @@ For example:
* `age>=18` : filters all elements, where the age is `18` or higher,
* `city!=berlin` : filters all elements, where the city is not `berlin`

If you try to filter with a column that does not exist in the respective model, an `UnknownColumnException` will be thrown.
If you try to filter using a column that does not exist in the respective model, it will be ignored.

Thereby, this plugin offers the following operators: `=`, `!=`, `<`, `<=`, `=>`. `>`.
At the moment, this plugin offers the following operators:
* `=` --> compare equality
* `!=` --> compare unequality
* `<` --> smaller than
* `<=` --> smaller or equal than
* `=>` --> greater or equal than
* `>` --> greater than

Of course you can combine the filters with the other query parameters:
```php
/index?name=j*&age>=18&sort=age,-name&limit=20
```
would return the first `20` elements, where the `name` starts with `j`, the `age` is `>= 18` sorted by `age ASC` and `name DESC`.
would return the first `20` elements, where the `name` starts with `j`, the `age` is `>= 18`
sorted by `age ASC` and `name DESC`.
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@

class DingoQueryMapperServiceProvider extends ServiceProvider
{
public function boot()
{
public function boot() {
$this->publishes([
__DIR__.'/../../config/dingoquerymapper.php' => config_path('dingoquerymapper.php'),
], 'config');
}

public function register()
{
public function register() {
$this->setupConfig();
}

Expand Down
12 changes: 0 additions & 12 deletions src/JohannesSchobel/DingoQueryMapper/LumenServiceProvider.php

This file was deleted.

114 changes: 114 additions & 0 deletions src/JohannesSchobel/DingoQueryMapper/Operators/CollectionOperator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace JohannesSchobel\DingoQueryMapper\Operators;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use JohannesSchobel\DingoQueryMapper\Exceptions\UnknownColumnException;

class CollectionOperator implements Operations
{
protected $collection;
protected $request;

public function __construct(Collection $collection, Request $request) {
$this->collection = $collection;
$this->request = $request;
}

public function get() {
return $this->collection;
}

public function paginate($page, $limit) {
$slice = $this->collection->forPage($page, $limit);

return new LengthAwarePaginator(
$slice, // only the items needed
count($this->collection), // total amount of items
$limit, // items per page
$page, // current page
['path' => $this->request->url(), 'query' => $this->request->query()] // We need this so we can keep all old query parameters from the url
);
}

public function sort(array $sorts) {
$comparer = $this->callbackSearchable($sorts);
$this->collection = $this->collection->sort($comparer);
}

public function filter(array $filters) {
$filterer = $this->callbackFilterable($filters);
$this->collection = $this->collection->filter($filterer);
}

private function callbackSearchable($criteria) {
$callback = function ($first, $second) use ($criteria) {
foreach ($criteria as $c) {
// normalize sort direction
$orderType = strtolower($c['direction']);
if ($first[$c['column']] < $second[$c['column']]) {
return $orderType === "asc" ? -1 : 1;
} else if ($first[$c['column']] > $second[$c['column']]) {
return $orderType === "asc" ? 1 : -1;
}
}
// all elements were equal
return 0;
};
return $callback;
}

private function callbackFilterable($criteria) {
$callback = function($item) use ($criteria) {
$attributes = $item->getAttributes();

foreach ($criteria as $c) {
// check, if the criteria to check is present
if(! array_key_exists($c['key'], $attributes)) {
// attribute does not exist - continue with the next one
continue;
}

// the attribute exists - so check the operator and value
$attribute = $item->getAttribute($c['key']);
$rule = $this->createEvaluationRule($attribute, $c['operator'], $c['value']);

$evalString = 'return(' . $rule . ');';
$result = (boolean) eval($evalString);

if($result === false) {
return false;
}
}

return true;
};

return $callback;
}

private function createEvaluationRule($key, $operator, $value) {
// first, check the operator type!
if($operator == '=') $operator = '==';
$rule = "'%s' %s '%s'"; // key, operator, value
$rule = sprintf($rule, $key, $operator, $value);

// now check if the operator was "(not) like"?
if (strpos($operator, 'like') !== false) {
$value = str_replace('%', '', $value);
$rule = "substr('%s', 0, strlen('%s')) %s '%s'"; // haystack, $needle, $comparable, $needle
$expectedResult = '===';

if(stripos($operator, 'not') !== false) {
// it is a NOT LIKE operator
$expectedResult = '!==';
}

$rule = sprintf($rule, $key, $value, $expectedResult, $value);
}

return $rule;
}

}
38 changes: 38 additions & 0 deletions src/JohannesSchobel/DingoQueryMapper/Operators/Operations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace JohannesSchobel\DingoQueryMapper\Operators;

interface Operations
{
/**
* Get the entire result
*
* @return mixed
*/
public function get();

/**
* Get the result paginated
*
* @param $page
* @param $limit
* @return mixed
*/
public function paginate($page, $limit);

/**
* Sort the result using sort criteria
*
* @param array $sorts
* @return mixed
*/
public function sort(array $sorts);

/**
* Filter the result using filter criteria
*
* @param array $filters
* @return mixed
*/
public function filter(array $filters);
}
Loading

0 comments on commit 9ebdd9a

Please sign in to comment.