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

Custom query operators support #161

Merged
merged 4 commits into from Apr 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
176 changes: 78 additions & 98 deletions lib/Query.php
@@ -1,4 +1,5 @@
<?php

namespace Spot;

/**
Expand Down Expand Up @@ -57,6 +58,41 @@ class Query implements \Countable, \IteratorAggregate, \ArrayAccess, \JsonSerial
*/
protected static $_customMethods = [];

/**
* @var array
*/
protected static $_whereOperators = [
'<' => 'Spot\Query\Operator\LessThan',
':lt' => 'Spot\Query\Operator\LessThan',
'<=' => 'Spot\Query\Operator\LessThanOrEqual',
':lte' => 'Spot\Query\Operator\LessThanOrEqual',
'>' => 'Spot\Query\Operator\GreaterThan',
':gt' => 'Spot\Query\Operator\GreaterThan',
'>=' => 'Spot\Query\Operator\GreaterThanOrEqual',
':gte' => 'Spot\Query\Operator\GreaterThanOrEqual',
'~=' => 'Spot\Query\Operator\RegExp',
'=~' => 'Spot\Query\Operator\RegExp',
':regex' => 'Spot\Query\Operator\RegExp',
':like' => 'Spot\Query\Operator\Like',
':fulltext' => 'Spot\Query\Operator\FullText',
':fulltext_boolean' => 'Spot\Query\Operator\FullTextBoolean',
'in' => 'Spot\Query\Operator\In',
':in' => 'Spot\Query\Operator\In',
'<>' => 'Spot\Query\Operator\Not',
'!=' => 'Spot\Query\Operator\Not',
':ne' => 'Spot\Query\Operator\Not',
':not' => 'Spot\Query\Operator\Not',
'=' => 'Spot\Query\Operator\Equals',
':eq' => 'Spot\Query\Operator\Equals',
];

/**
* Already instantiated operator objects
*
* @var array
*/
protected static $_whereOperatorObjects = [];

/**
* Constructor Method
*
Expand Down Expand Up @@ -138,6 +174,21 @@ public static function addMethod($method, callable $callback)
self::$_customMethods[$method] = $callback;
}

/**
* Adds a custom type to the type map.
*
* @param string $operator
* @param callable|string $action
*/
public static function addWhereOperator($operator, $action)
{
if (isset(self::$_whereOperators[$operator])) {
throw new \InvalidArgumentException("Where operator '" . $operator . "' already exists");
}

static::$_whereOperators[$operator] = $action;
}

/**
* Get current adapter object
*
Expand Down Expand Up @@ -317,121 +368,50 @@ private function parseWhereToSQLFragments(array $where, $useAlias = true)

$sqlFragments = [];
foreach ($where as $column => $value) {

$whereClause = "";
// Column name with comparison operator
$colData = explode(' ', $column);
$operator = isset($colData[1]) ? $colData[1] : '=';
if (count($colData) > 2) {
$operator = array_pop($colData);
$colData = [implode(' ', $colData), $operator];
}
$col = $colData[0];

// Prefix column name with alias
$operator = $this->getWhereOperatorCallable(strtolower($operator));
if (!$operator) {
throw new Exception("Unsupported operator '" . $operator . "' in WHERE clause");
}

$col = $colData[0];
if ($useAlias === true) {
// Prefix column name with alias
$col = $this->fieldWithAlias($col);
}

// Determine which operator to use based on custom and standard syntax
switch (strtolower($operator)) {
case '<':
case ':lt':
$operator = '<';
break;
case '<=':
case ':lte':
$operator = '<=';
break;
case '>':
case ':gt':
$operator = '>';
break;
case '>=':
case ':gte':
$operator = '>=';
break;
// REGEX matching
case '~=':
case '=~':
case ':regex':
$operator = "REGEXP";
break;
// LIKE
case ':like':
$operator = "LIKE";
break;
// FULLTEXT search
// MATCH(col) AGAINST(search)
case ':fulltext':
$whereClause = "MATCH(" . $col . ") AGAINST(" . $builder->createPositionalParameter($value) . ")";
break;
case ':fulltext_boolean':
$whereClause = "MATCH(" . $col . ") AGAINST(" . $builder->createPositionalParameter($value) . " IN BOOLEAN MODE)";
break;
// In
case 'in':
case ':in':
$operator = 'IN';
if (!is_array($value)) {
throw new Exception("Use of IN operator expects value to be array. Got " . gettype($value) . ".");
}
break;
// Not equal
case '<>':
case '!=':
case ':ne':
case ':not':
$operator = '!=';
if (is_array($value)) {
$operator = "NOT IN";
} elseif (is_null($value)) {
$operator = "IS NOT NULL";
}
break;
// Equals
case '=':
case ':eq':
$operator = '=';
if (is_array($value)) {
$operator = "IN";
} elseif (is_null($value)) {
$operator = "IS NULL";
}
break;
// Unsupported operator
default:
throw new Exception("Unsupported operator '" . $operator . "' in WHERE clause");
break;
}
$sqlFragments[] = $operator($builder, $col, $value);
}

// If WHERE clause not already set by the code above...
if (empty($whereClause)) {
if (is_array($value)) {
if (empty($value)) {
$whereClause = $col . (($operator === 'NOT IN') ? " IS NOT NULL" : " IS NULL");
} else {
$valueIn = "";
foreach ($value as $val) {
$valueIn .= $builder->createPositionalParameter($val) . ",";
}
$value = "(" . trim($valueIn, ',') . ")";
$whereClause = $col . " " . $operator . " " . $value;
}
} elseif (is_null($value)) {
$whereClause = $col . " " . $operator;
}
}
return $sqlFragments;
}

if (empty($whereClause)) {
// Add to binds array and add to WHERE clause
$whereClause = $col . " " . $operator . " " . $builder->createPositionalParameter($value) . "";
}
/**
* @param string $operator
* @return callable|false
*/
private function getWhereOperatorCallable($operator)
{
if (!isset(static::$_whereOperators[$operator])) {
return false;
}

$sqlFragments[] = $whereClause;
if (is_callable(static::$_whereOperators[$operator])) {
return static::$_whereOperators[$operator];
}

return $sqlFragments;
if (!isset(static::$_whereOperatorObjects[$operator])) {
static::$_whereOperatorObjects[$operator] = new static::$_whereOperators[$operator]();
}

return static::$_whereOperatorObjects[$operator];
}

/**
Expand Down
33 changes: 33 additions & 0 deletions lib/Query/Operator/Equals.php
@@ -0,0 +1,33 @@
<?php

namespace Spot\Query\Operator;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;
use Spot\Exception;

/**
* @package Spot\Query\Operator
*/
class Equals
{
/**
* @param QueryBuilder $builder
* @param $column
* @param $value
* @throws Exception
* @return string
*/
public function __invoke(QueryBuilder $builder, $column, $value)
{
if (is_array($value) && !empty($value)) {
return $column . ' IN (' . $builder->createPositionalParameter($value, Connection::PARAM_STR_ARRAY) . ')';
}

if ($value === null || (is_array($value) && empty($value))) {
return $column . ' IS NULL';
}

return $column . ' = ' . $builder->createPositionalParameter($value);
}
}
22 changes: 22 additions & 0 deletions lib/Query/Operator/FullText.php
@@ -0,0 +1,22 @@
<?php

namespace Spot\Query\Operator;

use Doctrine\DBAL\Query\QueryBuilder;

/**
* @package Spot\Query\Operator
*/
class FullText
{
/**
* @param QueryBuilder $builder
* @param $column
* @param $value
* @return string
*/
public function __invoke(QueryBuilder $builder, $column, $value)
{
return 'MATCH(' . $column . ') AGAINST (' . $builder->createPositionalParameter($value) . ')';
}
}
22 changes: 22 additions & 0 deletions lib/Query/Operator/FullTextBoolean.php
@@ -0,0 +1,22 @@
<?php

namespace Spot\Query\Operator;

use Doctrine\DBAL\Query\QueryBuilder;

/**
* @package Spot\Query\Operator
*/
class FullTextBoolean
{
/**
* @param QueryBuilder $builder
* @param $column
* @param $value
* @return string
*/
public function __invoke(QueryBuilder $builder, $column, $value)
{
return 'MATCH(' . $column . ') AGAINST (' . $builder->createPositionalParameter($value) . ' IN BOOLEAN MODE)';
}
}
22 changes: 22 additions & 0 deletions lib/Query/Operator/GreaterThan.php
@@ -0,0 +1,22 @@
<?php

namespace Spot\Query\Operator;

use Doctrine\DBAL\Query\QueryBuilder;

/**
* @package Spot\Query\Operator
*/
class GreaterThan
{
/**
* @param QueryBuilder $builder
* @param $column
* @param $value
* @return string
*/
public function __invoke(QueryBuilder $builder, $column, $value)
{
return $column . ' > ' . $builder->createPositionalParameter($value);
}
}
22 changes: 22 additions & 0 deletions lib/Query/Operator/GreaterThanOrEqual.php
@@ -0,0 +1,22 @@
<?php

namespace Spot\Query\Operator;

use Doctrine\DBAL\Query\QueryBuilder;

/**
* @package Spot\Query\Operator
*/
class GreaterThanOrEqual
{
/**
* @param QueryBuilder $builder
* @param $column
* @param $value
* @return string
*/
public function __invoke(QueryBuilder $builder, $column, $value)
{
return $column . ' >= ' . $builder->createPositionalParameter($value);
}
}
29 changes: 29 additions & 0 deletions lib/Query/Operator/In.php
@@ -0,0 +1,29 @@
<?php

namespace Spot\Query\Operator;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;
use Spot\Exception;

/**
* @package Spot\Query\Operator
*/
class In
{
/**
* @param QueryBuilder $builder
* @param $column
* @param $value
* @throws Exception
* @return string
*/
public function __invoke(QueryBuilder $builder, $column, $value)
{
if (!is_array($value)) {
throw new Exception("Use of IN operator expects value to be array. Got " . gettype($value) . ".");
}

return $column . ' IN (' . $builder->createPositionalParameter($value, Connection::PARAM_STR_ARRAY) . ')';
}
}
22 changes: 22 additions & 0 deletions lib/Query/Operator/LessThan.php
@@ -0,0 +1,22 @@
<?php

namespace Spot\Query\Operator;

use Doctrine\DBAL\Query\QueryBuilder;

/**
* @package Spot\Query\Operator
*/
class LessThan
{
/**
* @param QueryBuilder $builder
* @param $column
* @param $value
* @return string
*/
public function __invoke(QueryBuilder $builder, $column, $value)
{
return $column . ' < ' . $builder->createPositionalParameter($value);
}
}