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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ 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
lt | Lesser than | `1600` matches `1548` but not `1700`
lte | Lesser than or equalTo | `1600` matches `1600` and below
in | In array | `['Giordano', 'Bruno']` matches `Giordano` and `Bruno` but not `Giovanni`

**Special values**
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: 93 additions & 54 deletions src/EloquentBuilderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use DB;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Facades\Config;
use InvalidArgumentException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
Expand All @@ -13,83 +14,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 = Config::get('database.default');
Copy link
Owner

Choose a reason for hiding this comment

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

I do not think this is the most optimal way to get the connection type. The database key can be any arbitrary value.

What about getting it from the queryBuilder, something like $queryBuilder->getConnection()->getDriverName(). I just quickly looked it up, so I am not 100% sure it is a viable option - could you look into it? :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I looked for it and it works perfectly :D nice!


$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 +122,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,9 +134,15 @@ protected function applyFilter(Builder $query, array $filter, $or = false, array
case 'gt':
$clauseOperator = $not ? '<' : '>';
break;
case 'gte':
$clauseOperator = $not ? '<' : '>=';
break;
case 'bt':
$clauseOperator = 'between';
break;
case 'lte':
$clauseOperator = $not ? '>' : '<=';
break;
case 'lt':
$clauseOperator = $not ? '>' : '<';
break;
Expand All @@ -141,7 +165,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 +178,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 +212,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 +240,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