-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
42 changed files
with
9,778 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Atk4\Data\Action; | ||
|
||
use atk4\data\Exception; | ||
use atk4\data\Field; | ||
use atk4\data\Model; | ||
|
||
/** | ||
* Class Array_ is returned by $model->action(). Compatible with DSQL to a certain point as it implements | ||
* specific actions such as getOne() or get(). | ||
*/ | ||
class Iterator | ||
{ | ||
/** | ||
* @var \ArrayIterator | ||
*/ | ||
public $generator; | ||
|
||
/** | ||
* Iterator constructor. | ||
*/ | ||
public function __construct(array $data) | ||
{ | ||
$this->generator = new \ArrayIterator($data); | ||
} | ||
|
||
/** | ||
* Applies FilterIterator making sure that values of $field equal to $value. | ||
* | ||
* @param string $field | ||
* @param string $value | ||
* | ||
* @return $this | ||
*/ | ||
public function filter(Model\Scope\AbstractScope $condition) | ||
{ | ||
if (!$condition->isEmpty()) { | ||
// bug in php, see: | ||
// https://github.com/atk4/data/pull/735 | ||
// https://bugs.php.net/bug.php?id=80125 | ||
// $this->generator = new \CallbackFilterIterator($this->generator, function ($row) use ($condition) { | ||
// return $this->match($row, $condition); | ||
// }); | ||
// remove code below once fixed | ||
$data = []; | ||
foreach ($this->generator as $k => $row) { | ||
if ($this->match($row, $condition)) { | ||
$data[$k] = $row; | ||
} | ||
} | ||
$this->generator = new \ArrayIterator($data); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Calculates SUM|AVG|MIN|MAX aggragate values for $field. | ||
* | ||
* @param string $fx | ||
* @param string $field | ||
* @param bool $coalesce | ||
* | ||
* @return \atk4\data\Action\Iterator | ||
*/ | ||
public function aggregate($fx, $field, $coalesce = false) | ||
{ | ||
$result = 0; | ||
$column = array_column($this->get(), $field); | ||
|
||
switch (strtoupper($fx)) { | ||
case 'SUM': | ||
$result = array_sum($column); | ||
|
||
break; | ||
case 'AVG': | ||
$column = $coalesce ? $column : array_filter($column, function ($value) { | ||
return $value !== null; | ||
}); | ||
|
||
$result = array_sum($column) / count($column); | ||
|
||
break; | ||
case 'MAX': | ||
$result = max($column); | ||
|
||
break; | ||
case 'MIN': | ||
$result = min($column); | ||
|
||
break; | ||
default: | ||
throw (new Exception('Persistence\Array_ driver action unsupported format')) | ||
->addMoreInfo('action', $fx); | ||
} | ||
|
||
$this->generator = new \ArrayIterator([[$result]]); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Checks if $row matches $condition. | ||
* | ||
* @return bool | ||
*/ | ||
protected function match(array $row, Model\Scope\AbstractScope $condition) | ||
{ | ||
$match = false; | ||
|
||
// simple condition | ||
if ($condition instanceof Model\Scope\Condition) { | ||
$args = $condition->toQueryArguments(); | ||
|
||
$field = $args[0]; | ||
$operator = $args[1] ?? null; | ||
$value = $args[2] ?? null; | ||
if (count($args) === 2) { | ||
$value = $operator; | ||
|
||
$operator = '='; | ||
} | ||
|
||
if (!is_a($field, Field::class)) { | ||
throw (new Exception('Persistence\Array_ driver condition unsupported format')) | ||
->addMoreInfo('reason', 'Unsupported object instance ' . get_class($field)) | ||
->addMoreInfo('condition', $condition); | ||
} | ||
|
||
$match = $this->evaluateIf($row[$field->short_name] ?? null, $operator, $value); | ||
} | ||
|
||
// nested conditions | ||
if ($condition instanceof Model\Scope) { | ||
$matches = []; | ||
|
||
foreach ($condition->getNestedConditions() as $nestedCondition) { | ||
$matches[] = $subMatch = (bool) $this->match($row, $nestedCondition); | ||
|
||
// do not check all conditions if any match required | ||
if ($condition->isOr() && $subMatch) { | ||
break; | ||
} | ||
} | ||
|
||
// any matches && all matches the same (if all required) | ||
$match = array_filter($matches) && ($condition->isAnd() ? count(array_unique($matches)) === 1 : true); | ||
} | ||
|
||
return $match; | ||
} | ||
|
||
protected function evaluateIf($v1, $operator, $v2): bool | ||
{ | ||
switch (strtoupper((string) $operator)) { | ||
case '=': | ||
$result = is_array($v2) ? $this->evaluateIf($v1, 'IN', $v2) : $v1 === $v2; | ||
|
||
break; | ||
case '>': | ||
$result = $v1 > $v2; | ||
|
||
break; | ||
case '>=': | ||
$result = $v1 >= $v2; | ||
|
||
break; | ||
case '<': | ||
$result = $v1 < $v2; | ||
|
||
break; | ||
case '<=': | ||
$result = $v1 <= $v2; | ||
|
||
break; | ||
case '!=': | ||
case '<>': | ||
$result = !$this->evaluateIf($v1, '=', $v2); | ||
|
||
break; | ||
case 'LIKE': | ||
$pattern = str_ireplace('%', '(.*?)', preg_quote($v2)); | ||
|
||
$result = (bool) preg_match('/^' . $pattern . '$/', (string) $v1); | ||
|
||
break; | ||
case 'NOT LIKE': | ||
$result = !$this->evaluateIf($v1, 'LIKE', $v2); | ||
|
||
break; | ||
case 'IN': | ||
$result = is_array($v2) ? in_array($v1, $v2, true) : $this->evaluateIf($v1, '=', $v2); | ||
|
||
break; | ||
case 'NOT IN': | ||
$result = !$this->evaluateIf($v1, 'IN', $v2); | ||
|
||
break; | ||
case 'REGEXP': | ||
$result = (bool) preg_match('/' . $v2 . '/', $v1); | ||
|
||
break; | ||
case 'NOT REGEXP': | ||
$result = !$this->evaluateIf($v1, 'REGEXP', $v2); | ||
|
||
break; | ||
default: | ||
throw (new Exception('Unsupported operator')) | ||
->addMoreInfo('operator', $operator); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Applies sorting on Iterator. | ||
* | ||
* @param array $fields | ||
* | ||
* @return $this | ||
*/ | ||
public function order($fields) | ||
{ | ||
$data = $this->get(); | ||
|
||
// prepare arguments for array_multisort() | ||
$args = []; | ||
foreach ($fields as [$field, $direction]) { | ||
$args[] = array_column($data, $field); | ||
$args[] = strtolower($direction) === 'desc' ? SORT_DESC : SORT_ASC; | ||
} | ||
$args[] = &$data; | ||
|
||
// call sorting | ||
array_multisort(...$args); | ||
|
||
// put data back in generator | ||
$this->generator = new \ArrayIterator(array_pop($args)); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Limit Iterator. | ||
* | ||
* @return $this | ||
*/ | ||
public function limit(int $limit = null, int $offset = 0) | ||
{ | ||
$data = array_slice($this->get(), $offset, $limit, true); | ||
|
||
// put data back in generator | ||
$this->generator = new \ArrayIterator($data); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Counts number of rows and replaces our generator with just a single number. | ||
* | ||
* @return $this | ||
*/ | ||
public function count() | ||
{ | ||
$this->generator = new \ArrayIterator([[iterator_count($this->generator)]]); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Checks if iterator has any rows. | ||
* | ||
* @return $this | ||
*/ | ||
public function exists() | ||
{ | ||
$this->generator = new \ArrayIterator([[$this->generator->valid() ? 1 : 0]]); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Return all data inside array. | ||
*/ | ||
public function get(): array | ||
{ | ||
return iterator_to_array($this->generator, true); | ||
} | ||
|
||
/** | ||
* Return one row of data. | ||
*/ | ||
public function getRow(): ?array | ||
{ | ||
$row = $this->generator->current(); | ||
$this->generator->next(); | ||
|
||
return $row; | ||
} | ||
|
||
/** | ||
* Return one value from one row of data. | ||
* | ||
* @return mixed | ||
*/ | ||
public function getOne() | ||
{ | ||
$data = $this->getRow(); | ||
|
||
return reset($data); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Atk4\Data; | ||
|
||
class Exception extends \atk4\core\Exception | ||
{ | ||
} |
Oops, something went wrong.