| 1: | <?php |
| 2: | |
| 3: | declare(strict_types=1); |
| 4: | |
| 5: | namespace Atk4\Data\Persistence\Array_; |
| 6: | |
| 7: | use Atk4\Data\Exception; |
| 8: | use Atk4\Data\Field; |
| 9: | use Atk4\Data\Model; |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | class Action |
| 16: | { |
| 17: | |
| 18: | public $generator; |
| 19: | |
| 20: | |
| 21: | private array $_filterFxs = []; |
| 22: | |
| 23: | |
| 24: | |
| 25: | |
| 26: | public function __construct(array $data) |
| 27: | { |
| 28: | $this->generator = new \ArrayIterator($data); |
| 29: | } |
| 30: | |
| 31: | |
| 32: | |
| 33: | |
| 34: | |
| 35: | |
| 36: | public function filter(Model\Scope\AbstractScope $condition) |
| 37: | { |
| 38: | if (!$condition->isEmpty()) { |
| 39: | |
| 40: | |
| 41: | |
| 42: | |
| 43: | $filterFx = function (array $row) use ($condition): bool { |
| 44: | return $this->match($row, $condition); |
| 45: | }; |
| 46: | if (\PHP_VERSION_ID < 80104 && count($this->_filterFxs) !== \PHP_INT_MAX) { |
| 47: | $this->_filterFxs[] = $filterFx; |
| 48: | $filterFxWeakRef = \WeakReference::create($filterFx); |
| 49: | $this->generator = new \CallbackFilterIterator($this->generator, static function (array $row) use ($filterFxWeakRef) { |
| 50: | return $filterFxWeakRef->get()($row); |
| 51: | }); |
| 52: | } else { |
| 53: | $this->generator = new \CallbackFilterIterator($this->generator, $filterFx); |
| 54: | } |
| 55: | |
| 56: | |
| 57: | $this->generator->rewind(); |
| 58: | } |
| 59: | |
| 60: | return $this; |
| 61: | } |
| 62: | |
| 63: | |
| 64: | |
| 65: | |
| 66: | |
| 67: | |
| 68: | public function aggregate(string $fx, string $field, bool $coalesce = false) |
| 69: | { |
| 70: | $result = 0; |
| 71: | $column = array_column($this->getRows(), $field); |
| 72: | |
| 73: | switch (strtoupper($fx)) { |
| 74: | case 'SUM': |
| 75: | $result = array_sum($column); |
| 76: | |
| 77: | break; |
| 78: | case 'AVG': |
| 79: | if (!$coalesce) { |
| 80: | $column = array_filter($column, static fn ($v) => $v !== null); |
| 81: | } |
| 82: | |
| 83: | $result = array_sum($column) / count($column); |
| 84: | |
| 85: | break; |
| 86: | case 'MAX': |
| 87: | $result = max($column); |
| 88: | |
| 89: | break; |
| 90: | case 'MIN': |
| 91: | $result = min($column); |
| 92: | |
| 93: | break; |
| 94: | default: |
| 95: | throw (new Exception('Array persistence driver action unsupported format')) |
| 96: | ->addMoreInfo('action', $fx); |
| 97: | } |
| 98: | |
| 99: | $this->generator = new \ArrayIterator([['v' => $result]]); |
| 100: | |
| 101: | return $this; |
| 102: | } |
| 103: | |
| 104: | |
| 105: | |
| 106: | |
| 107: | |
| 108: | |
| 109: | protected function match(array $row, Model\Scope\AbstractScope $condition): bool |
| 110: | { |
| 111: | if ($condition instanceof Model\Scope\Condition) { |
| 112: | $args = $condition->toQueryArguments(); |
| 113: | |
| 114: | $field = $args[0]; |
| 115: | $operator = $args[1] ?? null; |
| 116: | $value = $args[2] ?? null; |
| 117: | if (count($args) === 2) { |
| 118: | $value = $operator; |
| 119: | |
| 120: | $operator = '='; |
| 121: | } |
| 122: | |
| 123: | if (!is_a($field, Field::class)) { |
| 124: | throw (new Exception('Array persistence driver condition unsupported format')) |
| 125: | ->addMoreInfo('reason', 'Unsupported object instance ' . get_class($field)) |
| 126: | ->addMoreInfo('condition', $condition); |
| 127: | } |
| 128: | |
| 129: | return $this->evaluateIf($row[$field->shortName] ?? null, $operator, $value); |
| 130: | } elseif ($condition instanceof Model\Scope) { |
| 131: | $isOr = $condition->isOr(); |
| 132: | $res = true; |
| 133: | foreach ($condition->getNestedConditions() as $nestedCondition) { |
| 134: | $submatch = $this->match($row, $nestedCondition); |
| 135: | |
| 136: | if ($isOr) { |
| 137: | |
| 138: | if ($submatch) { |
| 139: | break; |
| 140: | } |
| 141: | } elseif (!$submatch) { |
| 142: | $res = false; |
| 143: | |
| 144: | break; |
| 145: | } |
| 146: | } |
| 147: | |
| 148: | return $res; |
| 149: | } |
| 150: | |
| 151: | throw (new Exception('Unexpected condition type')) |
| 152: | ->addMoreInfo('class', get_class($condition)); |
| 153: | } |
| 154: | |
| 155: | |
| 156: | |
| 157: | |
| 158: | |
| 159: | protected function evaluateIf($v1, string $operator, $v2): bool |
| 160: | { |
| 161: | if ($v2 instanceof self) { |
| 162: | $v2 = $v2->getRows(); |
| 163: | } |
| 164: | |
| 165: | if ($v2 instanceof \Traversable) { |
| 166: | throw (new Exception('Unexpected v2 type')) |
| 167: | ->addMoreInfo('class', get_class($v2)); |
| 168: | } |
| 169: | |
| 170: | switch (strtoupper($operator)) { |
| 171: | case '=': |
| 172: | $result = is_array($v2) ? $this->evaluateIf($v1, 'IN', $v2) : $v1 === $v2; |
| 173: | |
| 174: | break; |
| 175: | case '>': |
| 176: | $result = $v1 > $v2; |
| 177: | |
| 178: | break; |
| 179: | case '>=': |
| 180: | $result = $v1 >= $v2; |
| 181: | |
| 182: | break; |
| 183: | case '<': |
| 184: | $result = $v1 < $v2; |
| 185: | |
| 186: | break; |
| 187: | case '<=': |
| 188: | $result = $v1 <= $v2; |
| 189: | |
| 190: | break; |
| 191: | case '!=': |
| 192: | $result = !$this->evaluateIf($v1, '=', $v2); |
| 193: | |
| 194: | break; |
| 195: | case 'LIKE': |
| 196: | $pattern = str_ireplace('%', '(.*?)', preg_quote($v2, '~')); |
| 197: | |
| 198: | $result = (bool) preg_match('~^' . $pattern . '$~', (string) $v1); |
| 199: | |
| 200: | break; |
| 201: | case 'NOT LIKE': |
| 202: | $result = !$this->evaluateIf($v1, 'LIKE', $v2); |
| 203: | |
| 204: | break; |
| 205: | case 'IN': |
| 206: | $result = false; |
| 207: | foreach ($v2 as $v2Item) { |
| 208: | if ($this->evaluateIf($v1, '=', $v2Item)) { |
| 209: | $result = true; |
| 210: | |
| 211: | break; |
| 212: | } |
| 213: | } |
| 214: | |
| 215: | break; |
| 216: | case 'NOT IN': |
| 217: | $result = !$this->evaluateIf($v1, 'IN', $v2); |
| 218: | |
| 219: | break; |
| 220: | case 'REGEXP': |
| 221: | $result = (bool) preg_match('/' . $v2 . '/', $v1); |
| 222: | |
| 223: | break; |
| 224: | case 'NOT REGEXP': |
| 225: | $result = !$this->evaluateIf($v1, 'REGEXP', $v2); |
| 226: | |
| 227: | break; |
| 228: | default: |
| 229: | throw (new Exception('Unsupported operator')) |
| 230: | ->addMoreInfo('operator', $operator); |
| 231: | } |
| 232: | |
| 233: | return $result; |
| 234: | } |
| 235: | |
| 236: | |
| 237: | |
| 238: | |
| 239: | |
| 240: | |
| 241: | |
| 242: | |
| 243: | public function order(array $fields) |
| 244: | { |
| 245: | $data = $this->getRows(); |
| 246: | |
| 247: | |
| 248: | $args = []; |
| 249: | foreach ($fields as [$field, $direction]) { |
| 250: | $args[] = array_column($data, $field); |
| 251: | $args[] = strtolower($direction) === 'desc' ? \SORT_DESC : \SORT_ASC; |
| 252: | } |
| 253: | $args[] = &$data; |
| 254: | |
| 255: | |
| 256: | array_multisort(...$args); |
| 257: | |
| 258: | |
| 259: | $this->generator = new \ArrayIterator(array_pop($args)); |
| 260: | |
| 261: | return $this; |
| 262: | } |
| 263: | |
| 264: | |
| 265: | |
| 266: | |
| 267: | |
| 268: | |
| 269: | public function limit(?int $limit, int $offset = 0) |
| 270: | { |
| 271: | $this->generator = new \LimitIterator($this->generator, $offset, $limit ?? -1); |
| 272: | |
| 273: | return $this; |
| 274: | } |
| 275: | |
| 276: | |
| 277: | |
| 278: | |
| 279: | |
| 280: | |
| 281: | public function count() |
| 282: | { |
| 283: | $this->generator = new \ArrayIterator([['v' => iterator_count($this->generator)]]); |
| 284: | |
| 285: | return $this; |
| 286: | } |
| 287: | |
| 288: | |
| 289: | |
| 290: | |
| 291: | |
| 292: | |
| 293: | public function exists() |
| 294: | { |
| 295: | $this->generator->rewind(); |
| 296: | $this->generator = new \ArrayIterator([['v' => $this->generator->valid() ? 1 : 0]]); |
| 297: | |
| 298: | return $this; |
| 299: | } |
| 300: | |
| 301: | |
| 302: | |
| 303: | |
| 304: | |
| 305: | |
| 306: | public function getRows(): array |
| 307: | { |
| 308: | return iterator_to_array($this->generator, true); |
| 309: | } |
| 310: | |
| 311: | |
| 312: | |
| 313: | |
| 314: | |
| 315: | |
| 316: | public function getRow(): ?array |
| 317: | { |
| 318: | $this->generator->rewind(); |
| 319: | $row = $this->generator->current(); |
| 320: | $this->generator->next(); |
| 321: | |
| 322: | return $row; |
| 323: | } |
| 324: | |
| 325: | |
| 326: | |
| 327: | |
| 328: | |
| 329: | |
| 330: | public function getOne() |
| 331: | { |
| 332: | $data = $this->getRow(); |
| 333: | |
| 334: | return reset($data); |
| 335: | } |
| 336: | } |
| 337: | |