Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to MySQL for keyword LIKE, add support to laravel 5.4.* for relation keys and other minor fixes #20

Merged
merged 10 commits into from
Aug 3, 2017
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ To get started with Bruno I highly recommend my article on

## Installation

For Laravel 5.3 and below
```bash
composer require optimus/bruno ~1.0
composer require optimus/bruno ~2.0
```

For Laravel 5.4 and above
```bash
composer require optimus/bruno ~3.0
```

## Usage
Expand Down Expand Up @@ -181,6 +187,8 @@ sw | Starts with | `Gior` matches `Giordano Bruno` but not `Giovanni`
ew | Ends with | `uno` matches `Giordano Bruno` but not `Giovanni`
eq | Equals | `Giordano Bruno` matches `Giordano Bruno` but not `Bruno`
gt | Greater than | `1548` matches `1600` but not `1400`
gte| Greater than or equalTo | `1548` matches `1548` and above (ony for Laravel 5.4 and above)
lte | Lesser than or equalTo | `1600` matches `1600` and below (ony for Laravel 5.4 and above)
lt | Lesser than | `1600` matches `1548` but not `1700`
in | In array | `['Giordano', 'Bruno']` matches `Giordano` and `Bruno` but not `Giovanni`

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
}
},
"require": {
"laravel/framework": "~5.1",
"laravel/framework": "~5.4",
"optimus/architect": "~1.0"
},
"require-dev": {
Expand Down
147 changes: 91 additions & 56 deletions src/EloquentBuilderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,83 +13,99 @@ trait EloquentBuilderTrait
{
/**
* Apply resource options to a query builder
* @param Builder $query
* @param Builder $queryBuilder
* @param array $options
* @return Builder
*/
protected function applyResourceOptions(Builder $query, array $options = [])
protected function applyResourceOptions(Builder $queryBuilder, array $options = [])
{
if (!empty($options)) {
extract($options);

if (isset($includes)) {
if (!is_array($includes)) {
throw new InvalidArgumentException('Includes should be an array.');
}
if (empty($options)) {
return $queryBuilder;
}

$query->with($includes);
}
extract($options);

if (isset($filter_groups)) {
$filterJoins = $this->applyFilterGroups($query, $filter_groups);
if (isset($includes)) {
if (!is_array($includes)) {
throw new InvalidArgumentException('Includes should be an array.');
}

if (isset($sort)) {
if (!is_array($sort)) {
throw new InvalidArgumentException('Sort should be an array.');
}
$queryBuilder->with($includes);
}

if (!isset($filterJoins)) {
$filterJoins = [];
}
if (isset($filter_groups)) {
$filterJoins = $this->applyFilterGroups($queryBuilder, $filter_groups);
}

$sortingJoins = $this->applySorting($query, $sort, $filterJoins);
if (isset($sort)) {
if (!is_array($sort)) {
throw new InvalidArgumentException('Sort should be an array.');
}

if (isset($limit)) {
$query->limit($limit);
if (!isset($filterJoins)) {
$filterJoins = [];
}

if (isset($page)) {
$query->offset($page*$limit);
}
$sortingJoins = $this->applySorting($queryBuilder, $sort, $filterJoins);
}

if (isset($limit)) {
$queryBuilder->limit($limit);
}

if (isset($page)) {
$queryBuilder->offset($page*$limit);
}

return $query;
return $queryBuilder;
}

protected function applyFilterGroups(Builder $query, array $filterGroups = [], array $previouslyJoined = [])
/**
* @param Builder $queryBuilder
* @param array $filterGroups
* @param array $previouslyJoined
* @return array
*/
protected function applyFilterGroups(Builder $queryBuilder, array $filterGroups = [], array $previouslyJoined = [])
{
$joins = [];
foreach ($filterGroups as $group) {
$or = $group['or'];
$filters = $group['filters'];

$query->where(function (Builder $query) use ($filters, $or, &$joins) {
$queryBuilder->where(function (Builder $query) use ($filters, $or, &$joins) {
foreach ($filters as $filter) {
$this->applyFilter($query, $filter, $or, $joins);
}
});
}

foreach(array_diff($joins, $previouslyJoined) as $join) {
$this->joinRelatedModelIfExists($query, $join);
$this->joinRelatedModelIfExists($queryBuilder, $join);
}

return $joins;
}

protected function applyFilter(Builder $query, array $filter, $or = false, array &$joins)
/**
* @param Builder $queryBuilder
* @param array $filter
* @param bool|false $or
* @param array $joins
*/
protected function applyFilter(Builder $queryBuilder, array $filter, $or = false, array &$joins)
{
// $value, $not, $key, $operator
extract($filter);

$table = $query->getModel()->getTable();
$dbType = $queryBuilder->getConnection()->getDriverName();

$table = $queryBuilder->getModel()->getTable();

if ($value === 'null' || $value === '') {
$method = $not ? 'WhereNotNull' : 'WhereNull';

call_user_func([$query, $method], sprintf('%s.%s', $table, $key));
call_user_func([$queryBuilder, $method], sprintf('%s.%s', $table, $key));
} else {
$method = filter_var($or, FILTER_VALIDATE_BOOLEAN) ? 'orWhere' : 'where';
$clauseOperator = null;
Expand All @@ -105,8 +121,9 @@ protected function applyFilter(Builder $query, array $filter, $or = false, array
'sw' => $value.'%' // starts with
];

$databaseField = DB::raw(sprintf('CAST(%s.%s AS TEXT)', $table, $key));
$clauseOperator = $not ? 'NOT ILIKE' : 'ILIKE';
$castToText = (($dbType === 'postgres') ? 'TEXT' : 'CHAR');
$databaseField = DB::raw(sprintf('CAST(%s.%s AS ' . $castToText . ')', $table, $key));
$clauseOperator = ($not ? 'NOT':'') . (($dbType === 'postgres') ? 'ILIKE' : 'LIKE');
$value = $valueString[$operator];
break;
case 'eq':
Expand All @@ -116,8 +133,11 @@ protected function applyFilter(Builder $query, array $filter, $or = false, array
case 'gt':
$clauseOperator = $not ? '<' : '>';
break;
case 'bt':
$clauseOperator = 'between';
case 'gte':
$clauseOperator = $not ? '<' : '>=';
break;
case 'lte':
$clauseOperator = $not ? '>' : '<=';
break;
case 'lt':
$clauseOperator = $not ? '>' : '<';
Expand All @@ -141,7 +161,7 @@ protected function applyFilter(Builder $query, array $filter, $or = false, array
$customFilterMethod = $this->hasCustomMethod('filter', $key);
if ($customFilterMethod) {
call_user_func_array([$this, $customFilterMethod], [
$query,
$queryBuilder,
$method,
$clauseOperator,
$value,
Expand All @@ -154,19 +174,25 @@ protected function applyFilter(Builder $query, array $filter, $or = false, array
} else {
// In operations do not have an operator
if ($operator === 'in') {
call_user_func_array([$query, $method], [
call_user_func_array([$queryBuilder, $method], [
$databaseField, $value
]);
} else {
call_user_func_array([$query, $method], [
call_user_func_array([$queryBuilder, $method], [
$databaseField, $clauseOperator, $value
]);
}
}
}
}

protected function applySorting(Builder $query, array $sorting, array $previouslyJoined = [])
/**
* @param Builder $queryBuilder
* @param array $sorting
* @param array $previouslyJoined
* @return array
*/
protected function applySorting(Builder $queryBuilder, array $sorting, array $previouslyJoined = [])
{
$joins = [];
foreach($sorting as $sortRule) {
Expand All @@ -182,19 +208,24 @@ protected function applySorting(Builder $query, array $sorting, array $previousl
if ($customSortMethod) {
$joins[] = $key;

call_user_func([$this, $customSortMethod], $query, $direction);
call_user_func([$this, $customSortMethod], $queryBuilder, $direction);
} else {
$query->orderBy($key, $direction);
$queryBuilder->orderBy($key, $direction);
}
}

foreach(array_diff($joins, $previouslyJoined) as $join) {
$this->joinRelatedModelIfExists($query, $join);
$this->joinRelatedModelIfExists($queryBuilder, $join);
}

return $joins;
}

/**
* @param $type
* @param $key
* @return bool|string
*/
private function hasCustomMethod($type, $key)
{
$methodName = sprintf('%s%s', $type, Str::studly($key));
Expand All @@ -205,50 +236,54 @@ private function hasCustomMethod($type, $key)
return false;
}

private function joinRelatedModelIfExists(Builder $query, $key)
/**
* @param Builder $queryBuilder
* @param $key
*/
private function joinRelatedModelIfExists(Builder $queryBuilder, $key)
{
$model = $query->getModel();
$model = $queryBuilder->getModel();

// relationship exists, join to make special sort
if (method_exists($model, $key)) {
$relation = $model->$key();
$type = 'inner';

if ($relation instanceof BelongsTo) {
$query->join(
$queryBuilder->join(
$relation->getRelated()->getTable(),
$model->getTable().'.'.$relation->getForeignKey(),
$model->getTable().'.'.$relation->getQualifiedForeignKeyName(),
'=',
$relation->getRelated()->getTable().'.'.$relation->getOtherKey(),
$relation->getRelated()->getTable().'.'.$relation->getOwnerKey(),
$type
);
} elseif ($relation instanceof BelongsToMany) {
$query->join(
$queryBuilder->join(
$relation->getTable(),
$relation->getQualifiedParentKeyName(),
'=',
$relation->getForeignKey(),
$relation->getQualifiedForeignKeyName(),
$type
);
$query->join(
$queryBuilder->join(
$relation->getRelated()->getTable(),
$relation->getRelated()->getTable().'.'.$relation->getRelated()->getKeyName(),
'=',
$relation->getOtherKey(),
$relation->getQualifiedRelatedKeyName(),
$type
);
} else {
$query->join(
$queryBuilder->join(
$relation->getRelated()->getTable(),
$relation->getQualifiedParentKeyName(),
'=',
$relation->getForeignKey(),
$relation->getQualifiedForeignKeyName(),
$type
);
}

$table = $model->getTable();
$query->select(sprintf('%s.*', $table));
$queryBuilder->select(sprintf('%s.*', $table));
}
}
}
15 changes: 13 additions & 2 deletions src/LaravelController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

namespace Optimus\Bruno;

use JsonSerializable;
use InvalidArgumentException;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Routing\Controller;
use Illuminate\Routing\Router;
use Illuminate\Http\JsonResponse;
use Optimus\Architect\Architect;

abstract class LaravelController extends Controller
{
/**
* Defaults
* @var array
*/
protected $defaults = [];

/**
Expand Down Expand Up @@ -37,11 +43,16 @@ protected function response($data, $statusCode = 200, array $headers = [])
*/
protected function parseData($data, array $options, $key = null)
{
$architect = new Architect;
$architect = new Architect();

return $architect->parseData($data, $options['modes'], $key);
}

/**
* Page sort
* @param array $sort
* @return array
*/
protected function parseSort(array $sort) {
return array_map(function($sort) {
if (!isset($sort['direction'])) {
Expand Down Expand Up @@ -82,7 +93,7 @@ protected function parseIncludes(array $includes)
* Parse filter group strings into filters
* Filters are formatted as key:operator(value)
* Example: name:eq(esben)
* @param array $filters
* @param array $filter_groups
* @return array
*/
protected function parseFilterGroups(array $filter_groups)
Expand Down