Start to implement advanced search query language

This commit is contained in:
Frederic Guillot
2015-06-28 18:52:01 -04:00
parent 0fa64fc9bd
commit e22985df50
9 changed files with 781 additions and 3 deletions

View File

@@ -46,9 +46,11 @@ class Projectinfo extends Base
if ($search !== '') {
$paginator
->setQuery($this->taskFinder->getSearchQuery($project['id'], $search))
->calculate();
// $paginator
// ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search))
// ->calculate();
$paginator->setQuery($this->taskFilter->search($search)->filterByProject($project['id'])->getQuery())->calculate();
$nb_tasks = $paginator->getTotal();
}

142
app/Core/Lexer.php Normal file
View File

@@ -0,0 +1,142 @@
<?php
namespace Core;
/**
* Lexer
*
* @package core
* @author Frederic Guillot
*/
class Lexer
{
/**
* Current position
*
* @access private
* @var integer
*/
private $offset = 0;
/**
* Token map
*
* @access private
* @var array
*/
private $tokenMap = array(
"/^(assignee:)/" => 'T_ASSIGNEE',
"/^(color:)/" => 'T_COLOR',
"/^(due:)/" => 'T_DUE',
"/^(title:)/" => 'T_TITLE',
"/^(\s+)/" => 'T_WHITESPACE',
'/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
'/^(yesterday|tomorrow|today)/' => 'T_DATE',
'/^("(.*?)")/' => 'T_STRING',
"/^(\w+)/" => 'T_STRING',
);
/**
* Tokenize input string
*
* @access public
* @param string $input
* @return array
*/
public function tokenize($input)
{
$tokens = array();
$this->offset = 0;
while (isset($input[$this->offset])) {
$result = $this->match(substr($input, $this->offset));
if ($result === false) {
return array();
}
$tokens[] = $result;
}
return $tokens;
}
/**
* Find a token that match and move the offset
*
* @access public
* @param string $string
* @return array|boolean
*/
public function match($string)
{
foreach ($this->tokenMap as $pattern => $name) {
if (preg_match($pattern, $string, $matches)) {
$this->offset += strlen($matches[1]);
return array(
'match' => trim($matches[1], '"'),
'token' => $name,
);
}
}
return false;
}
/**
* Change the output of tokenizer to be easily parsed by the database filter
*
* Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title']
*
* @access public
* @param array $tokens
* @return array
*/
public function map(array $tokens)
{
$map = array(
'T_TITLE' => '',
);
while (false !== ($token = current($tokens))) {
switch ($token['token']) {
case 'T_ASSIGNEE':
case 'T_COLOR':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') {
$map[$token['token']][] = $next['match'];
}
break;
case 'T_DUE':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_DATE') {
$map[$token['token']] = $next['match'];
}
break;
default:
$map['T_TITLE'] .= $token['match'];
break;
}
next($tokens);
}
$map['T_TITLE'] = trim($map['T_TITLE']);
if (empty($map['T_TITLE'])) {
unset($map['T_TITLE']);
}
return $map;
}
}

View File

@@ -99,6 +99,29 @@ class Color extends Base
),
);
/**
* Find a color id from the name or the id
*
* @access public
* @param string $color
* @return string
*/
public function find($color)
{
$color = strtolower($color);
foreach ($this->default_colors as $color_id => $params) {
if ($color_id === $color) {
return $color_id;
}
else if ($color === strtolower($params['name'])) {
return $color_id;
}
}
return '';
}
/**
* Get available colors
*

View File

@@ -23,6 +23,42 @@ class TaskFilter extends Base
*/
public $query;
/**
* Apply filters according to the search input
*
* @access public
* @param string $input
* @return TaskFilter
*/
public function search($input)
{
$tree = $this->lexer->map($this->lexer->tokenize($input));
$this->query = $this->taskFinder->getExtendedQuery();
if (empty($tree)) {
$this->query->addCondition('1 = 0');
}
foreach ($tree as $filter => $value) {
switch ($filter) {
case 'T_ASSIGNEE':
$this->filterByAssignee($value);
break;
case 'T_COLOR':
$this->filterByColors($value);
break;
case 'T_DUE':
$this->filterByDueDate($value);
break;
case 'T_TITLE':
$this->filterByTitle($value);
break;
}
}
return $this;
}
/**
* Create a new query
*
@@ -163,6 +199,35 @@ class TaskFilter extends Base
return $this;
}
/**
* Filter by assignee names
*
* @access public
* @param array $values List of assignees
* @return TaskFilter
*/
public function filterByAssignee(array $values)
{
$this->query->beginOr();
foreach ($values as $assignee) {
switch ($assignee) {
case 'me':
$this->query->eq('owner_id', $this->userSession->getId());
break;
case 'nobody':
$this->query->eq('owner_id', 0);
break;
default:
$this->query->ilike(User::TABLE.'.username', '%'.$assignee.'%');
$this->query->ilike(User::TABLE.'.name', '%'.$assignee.'%');
}
}
$this->query->closeOr();
}
/**
* Filter by color
*
@@ -179,6 +244,26 @@ class TaskFilter extends Base
return $this;
}
/**
* Filter by colors
*
* @access public
* @param array $colors
* @return TaskFilter
*/
public function filterByColors(array $colors)
{
$this->query->beginOr();
foreach ($colors as $color) {
$this->filterByColor($this->color->find($color));
}
$this->query->closeOr();
return $this;
}
/**
* Filter by column
*
@@ -227,6 +312,18 @@ class TaskFilter extends Base
return $this;
}
/**
* Filter by due date
*
* @access public
* @param string $date ISO8601 date format
* @return TaskFilter
*/
public function filterByDueDate($date)
{
return $this->filterWithOperator('date_due', $date, true);
}
/**
* Filter by due date (range)
*
@@ -294,6 +391,17 @@ class TaskFilter extends Base
return $this->query->findAll();
}
/**
* Get the PicoDb query
*
* @access public
* @return \PicoDb\Table
*/
public function getQuery()
{
return $this->query;
}
/**
* Format the results to the ajax autocompletion
*
@@ -465,4 +573,36 @@ class TaskFilter extends Base
return $vEvent;
}
/**
* Filter with an operator
*
* @access public
* @param string $field
* @param string $value
* @param boolean $is_date
* @return TaskFilter
*/
private function filterWithOperator($field, $value, $is_date)
{
$operators = array(
'<=' => 'lte',
'>=' => 'gte',
'<' => 'lt',
'>' => 'gt',
);
foreach ($operators as $operator => $method) {
if (strpos($value, $operator) === 0) {
$value = substr($value, strlen($operator));
$this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
return $this;
}
}
$this->query->eq($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
return $this;
}
}

View File

@@ -67,6 +67,7 @@ class ClassProvider implements ServiceProviderInterface
'EmailClient',
'Helper',
'HttpClient',
'Lexer',
'MemoryCache',
'Request',
'Session',