Filter refactoring

This commit is contained in:
Frederic Guillot
2016-04-09 22:42:17 -04:00
parent 42813d702d
commit 11858be4e8
101 changed files with 3235 additions and 2841 deletions

View File

@@ -18,7 +18,7 @@ class ActionManager extends Base
* List of automatic actions
*
* @access private
* @var array
* @var ActionBase[]
*/
private $actions = array();

View File

@@ -48,16 +48,8 @@ use Pimple\Container;
* @property \Kanboard\Core\User\UserSession $userSession
* @property \Kanboard\Core\DateParser $dateParser
* @property \Kanboard\Core\Helper $helper
* @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
* @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter
* @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter
* @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
* @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter
* @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter
* @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
* @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
* @property \Kanboard\Model\ActionParameter $actionParameter
* @property \Kanboard\Model\AvatarFile $avatarFile
@@ -85,7 +77,6 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission
* @property \Kanboard\Model\ProjectUserRole $projectUserRole
* @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter
* @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
@@ -99,7 +90,6 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskDuplication $taskDuplication
* @property \Kanboard\Model\TaskExternalLink $taskExternalLink
* @property \Kanboard\Model\TaskFinder $taskFinder
* @property \Kanboard\Model\TaskFilter $taskFilter
* @property \Kanboard\Model\TaskLink $taskLink
* @property \Kanboard\Model\TaskModification $taskModification
* @property \Kanboard\Model\TaskPermission $taskPermission
@@ -137,6 +127,12 @@ use Pimple\Container;
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
* @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery
* @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery
* @property \Kanboard\Core\Filter\QueryBuilder $userQuery
* @property \Kanboard\Core\Filter\QueryBuilder $projectQuery
* @property \Kanboard\Core\Filter\QueryBuilder $taskQuery
* @property \Kanboard\Core\Filter\LexerBuilder $taskLexer
* @property \Psr\Log\LoggerInterface $logger
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
@@ -173,4 +169,18 @@ abstract class Base
{
return $this->container[$name];
}
/**
* Get object instance
*
* @static
* @access public
* @param Container $container
* @return static
*/
public static function getInstance(Container $container)
{
$self = new static($container);
return $self;
}
}

View File

@@ -23,7 +23,7 @@ class ExternalLinkManager extends Base
* Registered providers
*
* @access private
* @var array
* @var ExternalLinkProviderInterface[]
*/
private $providers = array();

View File

@@ -0,0 +1,40 @@
<?php
namespace Kanboard\Core\Filter;
use PicoDb\Table;
/**
* Criteria Interface
*
* @package filter
* @author Frederic Guillot
*/
interface CriteriaInterface
{
/**
* Set the Query
*
* @access public
* @param Table $query
* @return CriteriaInterface
*/
public function withQuery(Table $query);
/**
* Set filter
*
* @access public
* @param FilterInterface $filter
* @return CriteriaInterface
*/
public function withFilter(FilterInterface $filter);
/**
* Apply condition
*
* @access public
* @return CriteriaInterface
*/
public function apply();
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Kanboard\Core\Filter;
use PicoDb\Table;
/**
* Filter Interface
*
* @package filter
* @author Frederic Guillot
*/
interface FilterInterface
{
/**
* BaseFilter constructor
*
* @access public
* @param mixed $value
*/
public function __construct($value = null);
/**
* Set the value
*
* @access public
* @param string $value
* @return FilterInterface
*/
public function withValue($value);
/**
* Set query
*
* @access public
* @param Table $query
* @return FilterInterface
*/
public function withQuery(Table $query);
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes();
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply();
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Kanboard\Core\Filter;
use PicoDb\Table;
/**
* Formatter interface
*
* @package filter
* @author Frederic Guillot
*/
interface FormatterInterface
{
/**
* Set query
*
* @access public
* @param Table $query
* @return FormatterInterface
*/
public function withQuery(Table $query);
/**
* Apply formatter
*
* @access public
* @return mixed
*/
public function format();
}

153
app/Core/Filter/Lexer.php Normal file
View File

@@ -0,0 +1,153 @@
<?php
namespace Kanboard\Core\Filter;
/**
* Lexer
*
* @package filter
* @author Frederic Guillot
*/
class Lexer
{
/**
* Current position
*
* @access private
* @var integer
*/
private $offset = 0;
/**
* Token map
*
* @access private
* @var array
*/
private $tokenMap = array(
"/^(\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',
"/^(#\d+)/" => 'T_STRING',
);
/**
* Default token
*
* @access private
* @var string
*/
private $defaultToken = '';
/**
* Add token
*
* @access public
* @param string $regex
* @param string $token
* @return $this
*/
public function addToken($regex, $token)
{
$this->tokenMap = array($regex => $token) + $this->tokenMap;
return $this;
}
/**
* Set default token
*
* @access public
* @param string $token
* @return $this
*/
public function setDefaultToken($token)
{
$this->defaultToken = $token;
return $this;
}
/**
* 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 $this->map($tokens);
}
/**
* Find a token that match and move the offset
*
* @access protected
* @param string $string
* @return array|boolean
*/
protected 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;
}
/**
* Build map of tokens and matches
*
* @access protected
* @param array $tokens
* @return array
*/
protected function map(array $tokens)
{
$map = array();
$leftOver = '';
while (false !== ($token = current($tokens))) {
if ($token['token'] === 'T_STRING' || $token['token'] === 'T_WHITESPACE') {
$leftOver .= $token['match'];
} else {
$next = next($tokens);
if ($next !== false && in_array($next['token'], array('T_STRING', 'T_DATE'))) {
$map[$token['token']][] = $next['match'];
}
}
next($tokens);
}
$leftOver = trim($leftOver);
if ($this->defaultToken !== '' && $leftOver !== '') {
$map[$this->defaultToken] = array($leftOver);
}
return $map;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Kanboard\Core\Filter;
use PicoDb\Table;
/**
* Lexer Builder
*
* @package filter
* @author Frederic Guillot
*/
class LexerBuilder
{
/**
* Lexer object
*
* @access protected
* @var Lexer
*/
protected $lexer;
/**
* Query object
*
* @access protected
* @var Table
*/
protected $query;
/**
* List of filters
*
* @access protected
* @var FilterInterface[]
*/
protected $filters;
/**
* QueryBuilder object
*
* @access protected
* @var QueryBuilder
*/
protected $queryBuilder;
/**
* Constructor
*
* @access public
*/
public function __construct()
{
$this->lexer = new Lexer;
$this->queryBuilder = new QueryBuilder();
}
/**
* Add a filter
*
* @access public
* @param FilterInterface $filter
* @param bool $default
* @return LexerBuilder
*/
public function withFilter(FilterInterface $filter, $default = false)
{
$attributes = $filter->getAttributes();
foreach ($attributes as $attribute) {
$this->filters[$attribute] = $filter;
$this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute);
if ($default) {
$this->lexer->setDefaultToken($attribute);
}
}
return $this;
}
/**
* Set the query
*
* @access public
* @param Table $query
* @return LexerBuilder
*/
public function withQuery(Table $query)
{
$this->query = $query;
$this->queryBuilder->withQuery($this->query);
return $this;
}
/**
* Parse the input and build the query
*
* @access public
* @param string $input
* @return QueryBuilder
*/
public function build($input)
{
$tokens = $this->lexer->tokenize($input);
foreach ($tokens as $token => $values) {
if (isset($this->filters[$token])) {
$this->applyFilters($this->filters[$token], $values);
}
}
return $this->queryBuilder;
}
/**
* Apply filters to the query
*
* @access protected
* @param FilterInterface $filter
* @param array $values
*/
protected function applyFilters(FilterInterface $filter, array $values)
{
$len = count($values);
if ($len > 1) {
$criteria = new OrCriteria();
$criteria->withQuery($this->query);
foreach ($values as $value) {
$currentFilter = clone($filter);
$criteria->withFilter($currentFilter->withValue($value));
}
$this->queryBuilder->withCriteria($criteria);
} elseif ($len === 1) {
$this->queryBuilder->withFilter($filter->withValue($values[0]));
}
}
/**
* Clone object with deep copy
*/
public function __clone()
{
$this->lexer = clone $this->lexer;
$this->query = clone $this->query;
$this->queryBuilder = clone $this->queryBuilder;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Kanboard\Core\Filter;
use PicoDb\Table;
/**
* OR criteria
*
* @package filter
* @author Frederic Guillot
*/
class OrCriteria implements CriteriaInterface
{
/**
* @var Table
*/
protected $query;
/**
* @var FilterInterface[]
*/
protected $filters = array();
/**
* Set the Query
*
* @access public
* @param Table $query
* @return CriteriaInterface
*/
public function withQuery(Table $query)
{
$this->query = $query;
return $this;
}
/**
* Set filter
*
* @access public
* @param FilterInterface $filter
* @return CriteriaInterface
*/
public function withFilter(FilterInterface $filter)
{
$this->filters[] = $filter;
return $this;
}
/**
* Apply condition
*
* @access public
* @return CriteriaInterface
*/
public function apply()
{
$this->query->beginOr();
foreach ($this->filters as $filter) {
$filter->withQuery($this->query)->apply();
}
$this->query->closeOr();
return $this;
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Kanboard\Core\Filter;
use PicoDb\Table;
/**
* Class QueryBuilder
*
* @package filter
* @author Frederic Guillot
*/
class QueryBuilder
{
/**
* Query object
*
* @access protected
* @var Table
*/
protected $query;
/**
* Set the query
*
* @access public
* @param Table $query
* @return QueryBuilder
*/
public function withQuery(Table $query)
{
$this->query = $query;
return $this;
}
/**
* Set a filter
*
* @access public
* @param FilterInterface $filter
* @return QueryBuilder
*/
public function withFilter(FilterInterface $filter)
{
$filter->withQuery($this->query)->apply();
return $this;
}
/**
* Set a criteria
*
* @access public
* @param CriteriaInterface $criteria
* @return QueryBuilder
*/
public function withCriteria(CriteriaInterface $criteria)
{
$criteria->withQuery($this->query)->apply();
return $this;
}
/**
* Set a formatter
*
* @access public
* @param FormatterInterface $formatter
* @return string|array
*/
public function format(FormatterInterface $formatter)
{
return $formatter->withQuery($this->query)->format();
}
/**
* Get the query result as array
*
* @access public
* @return array
*/
public function toArray()
{
return $this->query->findAll();
}
/**
* Get Query object
*
* @access public
* @return Table
*/
public function getQuery()
{
return $this->query;
}
/**
* Clone object with deep copy
*/
public function __clone()
{
$this->query = clone $this->query;
}
}

View File

@@ -12,10 +12,12 @@ use Pimple\Container;
*
* @property \Kanboard\Helper\AppHelper $app
* @property \Kanboard\Helper\AssetHelper $asset
* @property \Kanboard\Helper\CalendarHelper $calendar
* @property \Kanboard\Helper\DateHelper $dt
* @property \Kanboard\Helper\FileHelper $file
* @property \Kanboard\Helper\FormHelper $form
* @property \Kanboard\Helper\HookHelper $hook
* @property \Kanboard\Helper\ICalHelper $ical
* @property \Kanboard\Helper\ModelHelper $model
* @property \Kanboard\Helper\SubtaskHelper $subtask
* @property \Kanboard\Helper\TaskHelper $task

View File

@@ -231,6 +231,20 @@ class Response extends Base
exit;
}
/**
* Send a iCal response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function ical($data, $status_code = 200)
{
$this->status($status_code);
$this->contentType('text/calendar; charset=utf-8');
echo $data;
}
/**
* Send the security header: Content-Security-Policy
*

View File

@@ -1,161 +0,0 @@
<?php
namespace Kanboard\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',
"/^(updated:)/" => 'T_UPDATED',
"/^(modified:)/" => 'T_UPDATED',
"/^(created:)/" => 'T_CREATED',
"/^(status:)/" => 'T_STATUS',
"/^(description:)/" => 'T_DESCRIPTION',
"/^(category:)/" => 'T_CATEGORY',
"/^(column:)/" => 'T_COLUMN',
"/^(project:)/" => 'T_PROJECT',
"/^(swimlane:)/" => 'T_SWIMLANE',
"/^(ref:)/" => 'T_REFERENCE',
"/^(reference:)/" => 'T_REFERENCE',
"/^(link:)/" => 'T_LINK',
"/^(\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',
"/^(#\d+)/" => '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':
case 'T_CATEGORY':
case 'T_COLUMN':
case 'T_PROJECT':
case 'T_SWIMLANE':
case 'T_LINK':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') {
$map[$token['token']][] = $next['match'];
}
break;
case 'T_STATUS':
case 'T_DUE':
case 'T_UPDATED':
case 'T_CREATED':
case 'T_DESCRIPTION':
case 'T_REFERENCE':
$next = next($tokens);
if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) {
$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;
}
}