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

DatabaseSource + Filters - Add Joins, Group By, and BETWEEN filter #12

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
53eec08
Add BETWEEN as a filter operator
MouseEatsCat Feb 16, 2022
0524bb3
Adjust formatting
MouseEatsCat Feb 16, 2022
1825bef
Add NOT BETWEEN operator
MouseEatsCat Feb 16, 2022
35a854b
FilterCollection - hasActiveFilters() method
MouseEatsCat Mar 23, 2022
5217d04
DatabaseSource sqlFilters - Filter out inactive filters
MouseEatsCat Mar 23, 2022
1f1293f
Typo - Added missing asterisk in phpdoc
MouseEatsCat Mar 23, 2022
f635b6f
Apply suggested changes
MouseEatsCat Mar 23, 2022
c2a1e40
Added test for hasActiveFilters()
MouseEatsCat Mar 23, 2022
3387105
Merge branch 'feature/between-filter' into placeauxjeunes-local
MouseEatsCat Mar 23, 2022
892d418
Merge branch 'feature/has-active-filters' into placeauxjeunes-local
MouseEatsCat Mar 23, 2022
c4c68e3
BETWEEN - Add fallback for start + end dates
MouseEatsCat Mar 23, 2022
47d680e
Merge branch 'feature/between-filter' into placeauxjeunes-local
MouseEatsCat Mar 23, 2022
9f0e8c2
Revert "BETWEEN - Add fallback for start + end dates"
MouseEatsCat Mar 23, 2022
2b99ba4
BETWEEN database filter - Throw exception on empty value
MouseEatsCat Mar 24, 2022
4ea818e
Merge branch 'feature/between-filter' into placeauxjeunes-local
MouseEatsCat Mar 24, 2022
912f564
DatabaseSource - Add Joins + Group By functionality
MouseEatsCat May 9, 2022
76eb991
Arrays/Params - remove trailing slashes
MouseEatsCat Aug 26, 2024
4cc6593
DatabaseFilter - Code readability improvements
MouseEatsCat Aug 26, 2024
bd36c36
DatabaseSource - Add joins to sqlLoadCount
MouseEatsCat Dec 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/Charcoal/Source/Database/DatabaseFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,56 @@ protected function byPredicate()
$conditions[] = sprintf('%1$s %2$s (\'%3$s\')', $target, $operator, $value);
break;

case 'BETWEEN':
case 'NOT BETWEEN':
if (!is_array($value) || (is_array($value) && count($value) < 2)) {
throw new UnexpectedValueException(sprintf(
'Array is required as value on field "%s" for "%s"',
$target,
$operator
));
}

$fromValue = reset($value);
$toValue = end($value);

if ((empty($fromValue) && !is_numeric($fromValue)) ||
(empty($toValue) && !is_numeric($toValue))
) {
throw new UnexpectedValueException(sprintf(
'Two values are required on field "%s" for "%s"',
$target,
$operator
));
}

// Check if querying dates
try {
new \DateTimeImmutable($fromValue);
new \DateTimeImmutable($toValue);
$isDate = true;
} catch (\Exception $e) {
$isDate = false;
}

if ($isDate) {
$fromExpr = 'CAST(\''.$fromValue.'\' AS DATE)';
$toExpr = 'CAST(\''.$toValue.'\' AS DATE)';
} else {
$fromExpr = '\''.$fromValue.'\'';
$toExpr = '\''.$toValue.'\'';
}

$conditions[] = sprintf(
'%1$s %2$s %3$s AND %4$s',
$target,
$operator,
$fromExpr,
$toExpr
);

break;

default:
if ($value === null) {
throw new UnexpectedValueException(sprintf(
Expand Down
139 changes: 133 additions & 6 deletions src/Charcoal/Source/DatabaseSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ class DatabaseSource extends AbstractSource implements
*/
private $table;

/**
* Store the source query joins.
*
* @var array
*/
private array $joins = [];

/**
* Store the source GROUP BY statement
*
* @var string
*/
private string $groupBy = '';

/**
* Create a new database handler.
*
Expand Down Expand Up @@ -752,7 +766,7 @@ public function deleteItem(StorableInterface $item = null)
*
* If the query fails, this method will return false.
*
* @param string $query The SQL query to executed.
* @param string $query The SQL query to execute.
* @param array $binds Optional. Query parameter binds.
* @param array $types Optional. Types of parameter bindings.
* @throws PDOException If the SQL query fails.
Expand Down Expand Up @@ -788,7 +802,7 @@ public function dbQuery($query, array $binds = [], array $types = [])
*
* If the preparation fails, this method will return false.
*
* @param string $query The SQL query to executed.
* @param string $query The SQL query to execute.
* @param array $binds Optional. Query parameter binds.
* @param array $types Optional. Types of parameter bindings.
* @return \PDOStatement|false The PDOStatement, otherwise FALSE.
Expand Down Expand Up @@ -826,18 +840,20 @@ public function sqlLoad()
{
if (!$this->hasTable()) {
throw new UnexpectedValueException(sprintf(
'[%s] Can not get SQL SELECT clause; no databse table name defined',
'[%s] Can not get SQL SELECT clause; no database table name defined',
$this->getModelClassForException()
));
}

$selects = $this->sqlSelect();
$tables = $this->sqlFrom();
$joins = $this->sqlJoins();
$filters = $this->sqlFilters();
$groupBy = $this->sqlGroupBy();
$orders = $this->sqlOrders();
$limits = $this->sqlPagination();

$query = 'SELECT '.$selects.' FROM '.$tables.$filters.$orders.$limits;
$query = 'SELECT '.$selects.' FROM '.$tables.$joins.$filters.$groupBy.$orders.$limits;
return $query;
}

Expand All @@ -851,7 +867,7 @@ public function sqlLoadCount()
{
if (!$this->hasTable()) {
throw new UnexpectedValueException(sprintf(
'[%s] Can not get SQL count; no databse table name defined',
'[%s] Can not get SQL count; no database table name defined',
$this->getModelClassForException()
));
}
Expand Down Expand Up @@ -912,6 +928,86 @@ public function sqlFrom()
return '`'.$table.'` AS `'.self::DEFAULT_TABLE_ALIAS.'`';
}

/**
* Compile the Joins.
*
* @return string
*/
public function sqlJoins(): string
{
$joins = '';

if ($this->hasJoins()) {
$joins = implode(' ', $this->joins());
}

return $joins;
}

/**
* Get all join statements.
*
* @return array
*/
public function joins(): array
{
return ($this->joins ?? []);
}

/**
* Append a Join statement.
*
* @param string $join Join statement.
* @return DatabaseSource
*/
public function addJoin($join): self
{
if (!empty($join)) {
$this->joins[] = $join;
}
return $this;
}

/**
* Append multiple Join statements.
*
* @param array $joins Array of join statements.
* @return DatabaseSource
*/
public function addJoins(array $joins): self
{
if (!empty($joins)) {
foreach ($joins as $join) {
$this->addJoin($join);
}
}
return $this;
}

/**
* Determine if source contains join statements.
*
* @return boolean
*/
public function hasJoins(): bool
{
return !empty($this->joins);
}

/**
* Remove a join statement by index.
*
* @param mixed $index Index of the join statement.
* @return DatabaseSource
*/
public function removeJoin($index): self
{
if (isset($this->joins[$index])) {
array_splice($this->joins, $index, 1);
}
return $this;
}

/**
* Compile the WHERE clause.
*
Expand All @@ -925,7 +1021,13 @@ public function sqlFilters()
}

$criteria = $this->createFilter([
'filters' => $this->filters()
'filters' => array_filter($this->filters(), function ($filter) {
// Check for active subfilters
if ($filter->hasFilters()) {
return $filter->hasActiveFilters();
}
return true;
})
]);

$sql = $criteria->sql();
Expand All @@ -936,6 +1038,31 @@ public function sqlFilters()
return $sql;
}

/**
* Set Group By
*
* @param string $groupBy Group by statement.
* @return DatabaseSource
*/
public function setGroupBy($groupBy): self
{
$this->groupBy = sprintf(
' GROUP BY %s',
$groupBy
);
return $this;
}

/**
* Get SQL Group By
*
* @return string
*/
public function sqlGroupBy()
{
return ($this->groupBy ?? '');
}

/**
* Compile the ORDER BY clause.
*
Expand Down
1 change: 1 addition & 0 deletions src/Charcoal/Source/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ protected function validOperators()
'IS NOT TRUE', 'IS NOT FALSE', 'IS NOT UNKNOWN',
'%', 'MOD',
'IN', 'NOT IN',
'BETWEEN', 'NOT BETWEEN',
'REGEXP', 'NOT REGEXP'
];
}
Expand Down
24 changes: 24 additions & 0 deletions src/Charcoal/Source/FilterCollectionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,30 @@ public function hasFilters()
return !empty($this->filters);
}

/**
* Determine recursively if the object has any active subfilters.
*
* @return boolean
*/
public function hasActiveFilters()
{
$endFilters = [];

// Recusively find filters with no subfilters
$this->traverseFilters(function ($filter) use (&$endFilters) {
if (!$filter->hasFilters()) {
$endFilters[] = $filter;
}
});

// Exclude filters that aren't active
$endFilters = array_filter($endFilters, function ($filter) {
return $filter->active();
});

return !empty($endFilters);
}

/**
* Retrieve the query filters stored in this object.
*
Expand Down
41 changes: 41 additions & 0 deletions tests/Charcoal/Source/FilterCollectionTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,47 @@ public function testHasExpressions()
$this->assertTrue($obj->hasFilters());
}

/**
* Test collection for active filters.
*
* Assertions:
* 1. Empty; Default state
* 2. Populated; Mutated state
* 3. Populated; Added a inactive subfilter
*
* @covers \Charcoal\Source\FilterCollectionTrait::hasActiveFilters
*
* @return void
*/
public function testHasActiveExpressions()
{
$obj = $this->createCollector();

/** 1. Default state */
$this->assertFalse($obj->hasActiveFilters());

/** 2. Mutated state */
$obj->addFilter([
'condition' => '( 1 + 1 = 2 )',
'filters' => $this->dummyItems
]);

$this->assertTrue($obj->hasActiveFilters());

/** 3. Added a inactive subfilter */
$obj->setFilters([[
'condition' => '( 1 + 1 = 2 )',
'filters' => [
[
'condition' => '( 1 + 1 = 2 )',
'active' => false
]
]
]]);

$this->assertFalse($obj->hasActiveFilters());
}

/**
* Test the mass assignment of expressions.
*
Expand Down