diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-06-28 18:52:01 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-06-28 18:52:01 -0400 |
commit | e22985df50d3a0a2ac883c43749542e41e425927 (patch) | |
tree | 784830150173ad36ad40037b9c938f09f6c77025 /app | |
parent | 0fa64fc9bd947e2f82f60d63d57479fa4189ef68 (diff) |
Start to implement advanced search query language
Diffstat (limited to 'app')
-rw-r--r-- | app/Controller/Projectinfo.php | 8 | ||||
-rw-r--r-- | app/Core/Lexer.php | 142 | ||||
-rw-r--r-- | app/Model/Color.php | 23 | ||||
-rw-r--r-- | app/Model/TaskFilter.php | 140 | ||||
-rw-r--r-- | app/ServiceProvider/ClassProvider.php | 1 |
5 files changed, 311 insertions, 3 deletions
diff --git a/app/Controller/Projectinfo.php b/app/Controller/Projectinfo.php index a9498f43..c30c1652 100644 --- a/app/Controller/Projectinfo.php +++ b/app/Controller/Projectinfo.php @@ -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(); } diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php new file mode 100644 index 00000000..a81965c5 --- /dev/null +++ b/app/Core/Lexer.php @@ -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; + } +} diff --git a/app/Model/Color.php b/app/Model/Color.php index 9a21f691..a35aff8f 100644 --- a/app/Model/Color.php +++ b/app/Model/Color.php @@ -100,6 +100,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 * * @access public diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php index bd03a8bc..4f306b14 100644 --- a/app/Model/TaskFilter.php +++ b/app/Model/TaskFilter.php @@ -24,6 +24,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 * * @access public @@ -164,6 +200,35 @@ class TaskFilter extends Base } /** + * 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 * * @access public @@ -180,6 +245,26 @@ class TaskFilter extends Base } /** + * 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 * * @access public @@ -228,6 +313,18 @@ class TaskFilter extends Base } /** + * 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) * * @access public @@ -295,6 +392,17 @@ class TaskFilter extends Base } /** + * Get the PicoDb query + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->query; + } + + /** * Format the results to the ajax autocompletion * * @access public @@ -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; + } } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 4ecd357b..1fa0d0ef 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -67,6 +67,7 @@ class ClassProvider implements ServiceProviderInterface 'EmailClient', 'Helper', 'HttpClient', + 'Lexer', 'MemoryCache', 'Request', 'Session', |