diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index 8da5e37e23..45f7d42538 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -31,15 +31,9 @@ public function __construct(array $data = []) */ protected $lastInsertIds = []; - /** - * @deprecated TODO temporary for these: - * - https://github.com/atk4/data/blob/90ab68ac063b8fc2c72dcd66115f1bd3f70a3a92/src/Reference/ContainsOne.php#L119 - * - https://github.com/atk4/data/blob/90ab68ac063b8fc2c72dcd66115f1bd3f70a3a92/src/Reference/ContainsMany.php#L66 - * remove once fixed/no longer needed - */ - public function getRawDataByTable(string $table): array + public function getRawDataIterator($table): \Iterator { - return $this->data[$table]; + return new \ArrayIterator($this->data[$table]); } public function setRawData(Model $model, $data, $id = null) diff --git a/src/Persistence/Array_/Query.php b/src/Persistence/Array_/Query.php index 30670507cb..68f18b9715 100644 --- a/src/Persistence/Array_/Query.php +++ b/src/Persistence/Array_/Query.php @@ -4,384 +4,11 @@ namespace atk4\data\Persistence\Array_; -use atk4\data\Exception; -use atk4\data\Field; -use atk4\data\Model; -use atk4\data\Model\Scope\Condition; use atk4\data\Persistence; -use atk4\data\Persistence\AbstractQuery; /** * Class to perform queries on Array_ persistence. */ -class Query extends AbstractQuery +class Query extends Persistence\IteratorQuery { - /** @var \Iterator */ - private $iterator; - - private $data; - - private $fields; - - /** @var \Closure */ - private $fx; - - public function __construct(Model $model, Persistence\Array_ $persistence = null) - { - parent::__construct($model, $persistence); - - $this->data = $this->persistence->getRawDataByTable($model->table); - - $this->iterator = new \ArrayIterator($this->data); - - $this->fx = function (\Iterator $iterator) { - return $iterator; - }; - } - - protected function initSelect($fields = null): void - { - if ($fields) { - $this->fields = $fields; - - $keys = array_flip((array) $fields); - - $data = array_map(function ($row) use ($keys) { - return array_intersect_key($row, $keys); - }, $this->get()); - - $this->iterator = new \ArrayIterator($data); - } - } - - protected function initInsert(array $data): void - { - $this->fx = function (\Iterator $iterator) use ($data) { - return $this->persistence->setRawData($this->model, $data); - }; - } - - protected function initUpdate(array $data): void - { - $this->fx = function (\Iterator $iterator) use ($data) { - foreach ($iterator as $id => $row) { - $this->persistence->setRawData($this->model, array_merge($row, $data), $id); - } - }; - } - - protected function initDelete(): void - { - $this->fx = function (\Iterator $iterator) { - foreach ($iterator as $id => $row) { - $this->persistence->unsetRawData($this->model->table, $id); - } - }; - } - - /** - * Applies sorting on Iterator. - * - * @param array $fields - * - * @return $this - */ - protected function initOrder(): void - { - if ($this->order) { - $data = $this->get(); - - // prepare arguments for array_multisort() - $args = []; - foreach ($this->order as [$field, $desc]) { - $args[] = array_column($data, $field); - $args[] = $desc ? SORT_DESC : SORT_ASC; - } - $args[] = &$data; - - // call sorting - array_multisort(...$args); - - // put data back in generator - $this->iterator = new \ArrayIterator(array_pop($args)); - } - } - - protected function initLimit(): void - { - if ($args = $this->getLimitArgs()) { - [$limit, $offset] = $args; - - $this->iterator = new \LimitIterator($this->iterator, $offset, $limit); - } - } - - /** - * Counts number of rows and replaces our generator with just a single number. - * - * @return $this - */ - protected function initCount($alias = null): void - { - // @todo: kept for BC, inconstent results with SQL count! - $this->initLimit(); - - $alias = $alias ?? 'count'; - - $this->iterator = new \ArrayIterator([[$alias => iterator_count($this->iterator)]]); - } - - /** - * Checks if iterator has any rows. - * - * @return $this - */ - protected function initExists(): void - { - $this->iterator = new \ArrayIterator([[$this->iterator->valid() ? 1 : 0]]); - } - - protected function initField($fieldName, string $alias = null): void - { - if (!$fieldName) { - throw new Exception('Field query requires field name'); - } - - $field = $fieldName; - if (!is_string($fieldName)) { - $field = $this->model->getField($fieldName); - $fieldName = $field->short_name; - } - - $this->initSelect([$fieldName]); - - // get first record - if ($row = $this->getRow()) { - if ($alias && array_key_exists($field, $row)) { - $row[$alias] = $row[$field]; - unset($row[$field]); - } - } - - $this->iterator = new \ArrayIterator([[$row]]); - } - - protected function doExecute() - { - return ($this->fx)($this->iterator); - } - - /** - * Return all data inside array. - */ - protected function doGet(): array - { - return iterator_to_array($this->iterator, true); - } - - /** - * Return one row of data. - */ - protected function doGetRow(): ?array - { - $this->iterator->rewind(); - - $row = $this->iterator->current(); - - if ($row && $this->model->id_field && !isset($row[$this->model->id_field])) { - $row[$this->model->id_field] = $this->iterator->key(); - } - - return $row; - } - - /** - * Return one value from one row of data. - * - * @return mixed - */ - protected function doGetOne() - { - $data = $this->getRow(); - - return reset($data); - } - - /** - * Calculates SUM|AVG|MIN|MAX aggragate values for $field. - * - * @param string $fieldName - * @param string $alias - */ - protected function initAggregate(string $functionName, $field, string $alias = null, bool $coalesce = false): void - { - $field = is_string($field) ? $field : $field->short_name; - - $result = 0; - $column = array_column($this->get(), $field); - - switch (strtoupper($functionName)) { - 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_ query unsupported aggregate function')) - ->addMoreInfo('function', $functionName); - } - - $this->iterator = new \ArrayIterator([[$result]]); - } - - /** - * Applies FilterIterator. - */ - protected function initWhere(): void - { - if (!$this->scope->isEmpty()) { - $this->iterator = new \CallbackFilterIterator($this->iterator, function ($row, $id) { - // make sure we use the complete row with the filter - $row = $this->data[$id]; - - if ($this->model->id_field && !isset($row[$this->model->id_field])) { - $row[$this->model->id_field] = $id; - } - - return $this->match($row, $this->scope); - }); - } - } - - /** - * Checks if $row matches $condition. - */ - protected function match(array $row, Model\Scope\AbstractScope $condition): bool - { - $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 = Condition::OPERATOR_EQUALS; - } - - 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 Condition::OPERATOR_EQUALS: - $result = is_array($v2) ? $this->evaluateIf($v1, Condition::OPERATOR_IN, $v2) : $v1 == $v2; - - break; - case Condition::OPERATOR_GREATER: - $result = $v1 > $v2; - - break; - case Condition::OPERATOR_GREATER_EQUAL: - $result = $v1 >= $v2; - - break; - case Condition::OPERATOR_LESS: - $result = $v1 < $v2; - - break; - case Condition::OPERATOR_LESS_EQUAL: - $result = $v1 <= $v2; - - break; - case Condition::OPERATOR_DOESNOT_EQUAL: - $result = !$this->evaluateIf($v1, Condition::OPERATOR_EQUALS, $v2); - - break; - case Condition::OPERATOR_LIKE: - $pattern = str_ireplace('%', '(.*?)', preg_quote($v2)); - - $result = (bool) preg_match('/^' . $pattern . '$/', (string) $v1); - - break; - case Condition::OPERATOR_NOT_LIKE: - $result = !$this->evaluateIf($v1, Condition::OPERATOR_LIKE, $v2); - - break; - case Condition::OPERATOR_IN: - $result = is_array($v2) ? in_array($v1, $v2, true) : $this->evaluateIf($v1, '=', $v2); - - break; - case Condition::OPERATOR_NOT_IN: - $result = !$this->evaluateIf($v1, Condition::OPERATOR_IN, $v2); - - break; - case Condition::OPERATOR_REGEXP: - $result = (bool) preg_match('/' . $v2 . '/', $v1); - - break; - case Condition::OPERATOR_NOT_REGEXP: - $result = !$this->evaluateIf($v1, Condition::OPERATOR_REGEXP, $v2); - - break; - default: - throw (new Exception('Unsupported operator')) - ->addMoreInfo('operator', $operator); - } - - return $result; - } - - public function getDebug(): array - { - return array_merge([ - 'fields' => $this->fields, - ], parent::getDebug()); - } } diff --git a/src/Persistence/Csv.php b/src/Persistence/Csv.php index 3f0a37fe68..f50de8e417 100644 --- a/src/Persistence/Csv.php +++ b/src/Persistence/Csv.php @@ -5,7 +5,6 @@ namespace atk4\data\Persistence; use atk4\data\Exception; -use atk4\data\Field; use atk4\data\Model; use atk4\data\Persistence; @@ -86,266 +85,83 @@ class Csv extends Persistence */ public $header = []; - public function __construct(string $file, array $defaults = []) - { - $this->file = $file; - $this->setDefaults($defaults); - } - /** - * Destructor. close files correctly. - */ - public function __destruct() - { - $this->closeFile(); - } - - /** - * Open CSV file. - * - * Override this method and open handle yourself if you want to - * reposition or load some extra columns on the top. + * File access object. * - * @param string $mode 'r' or 'w' + * @var \SplFileObject */ - public function openFile($mode = 'r') - { - if (!$this->handle) { - $this->handle = fopen($this->file, $mode); - if ($this->handle === false) { - throw (new Exception('Can not open CSV file.')) - ->addMoreInfo('file', $this->file) - ->addMoreInfo('mode', $mode); - } - } - } - - /** - * Close CSV file. - */ - public function closeFile() - { - if ($this->handle) { - fclose($this->handle); - $this->handle = null; - } - } + protected $fileObject; - /** - * Returns one line of CSV file as array. - */ - public function getLine(): ?array - { - $data = fgetcsv($this->handle, 0, $this->delimiter, $this->enclosure, $this->escape_char); - if ($data === false || $data === null) { - return null; - } - - ++$this->line; - - return $data; - } + protected $lastInsertId; - /** - * Writes array as one record to CSV file. - * - * @param array - */ - public function putLine($data) + public function __construct(string $file, array $defaults = []) { - $ok = fputcsv($this->handle, $data, $this->delimiter, $this->enclosure, $this->escape_char); - if ($ok === false) { - throw new Exception('Can not write to CSV file.'); - } + $this->file = $file; + $this->setDefaults($defaults); } - /** - * When load operation starts, this will open file and read - * the first line. This line is then used to identify columns. - */ - public function loadHeader() + protected function initPersistence(Model $model) { - $this->openFile('r'); - - $header = $this->getLine(); - --$this->line; // because we don't want to count header line + parent::initPersistence($model); - $this->initializeHeader($header); + $this->initFileObject($model); } - /** - * When load operation starts, this will open file and read - * the first line. This line is then used to identify columns. - */ - public function saveHeader(Model $model) + public function getRawDataIterator($table): \Iterator { - $this->openFile('w'); - - $header = []; - foreach ($model->getFields() as $name => $field) { - if ($name === $model->id_field) { - continue; - } - - $header[] = $name; - } - - $this->putLine($header); - - $this->initializeHeader($header); + return new \LimitIterator($this->fileObject, 1); } - /** - * Remembers $this->header so that the data can be - * easier mapped. - * - * @param array - */ - public function initializeHeader($header) + public function setRawData(Model $model, $data, $id = null) { - // removes forbidden symbols from header (field names) - $this->header = array_map(function ($name) { - return preg_replace('/[^a-z0-9_-]+/i', '_', $name); - }, $header); - } + $emptyRow = array_flip($this->getHeader($model)); - /** - * Typecasting when load data row. - */ - public function typecastLoadRow(Model $model, array $row): array - { - $id = null; - if (isset($row[$model->id_field])) { - // temporary remove id field - $id = $row[$model->id_field]; - unset($row[$model->id_field]); - } else { - $id = null; - } - $row = array_combine($this->header, $row); - if (isset($id)) { - $row[$model->id_field] = $id; - } + $data = array_intersect_key(array_merge($emptyRow, $data), $emptyRow); - foreach ($row as $key => $value) { - if ($value === null) { - continue; + if ($id === null) { + while (!$this->fileObject->eof()) { + $this->fileObject->next(); } - if ($model->hasField($key)) { - $row[$key] = $this->typecastLoadField($model->getField($key), $value); - } - } - - return $row; - } + $id = $this->fileObject->key(); - /** - * Tries to load model and return data record. - * Doesn't throw exception if model can't be loaded. - */ - public function tryLoadAny(Model $model): ?array - { - if (!$this->mode) { - $this->mode = 'r'; - } elseif ($this->mode === 'w') { - throw new Exception('Currently writing records, so loading is not possible.'); - } - - if (!$this->handle) { - $this->loadHeader(); - } - - $data = $this->getLine(); - if ($data === null) { - return null; + $this->lastInsertId = $id; + } else { + $this->fileObject->seek($id); } - $data = $this->typecastLoadRow($model, $data); - $data['id'] = $this->line; + $this->fileObject->fputcsv($data); - return $data; + return $id; } - /** - * Prepare iterator. - */ - public function prepareIterator(Model $model): iterable + protected function initFileObject(Model $model) { - if (!$this->mode) { - $this->mode = 'r'; - } elseif ($this->mode === 'w') { - throw new Exception('Currently writing records, so loading is not possible.'); + if (!file_exists($this->file)) { + file_put_contents($this->file, ''); } - if (!$this->handle) { - $this->loadHeader(); - } + $this->fileObject = new \SplFileObject($this->file, 'r+'); + $this->fileObject->setFlags( + \SplFileObject::READ_CSV | + \SplFileObject::SKIP_EMPTY | + \SplFileObject::DROP_NEW_LINE | + \SplFileObject::READ_AHEAD + ); + $this->fileObject->setCsvControl($this->delimiter, $this->enclosure, $this->escape_char); - while (true) { - $data = $this->getLine(); - if ($data === null) { - break; - } - $data = $this->typecastLoadRow($model, $data); - $data[$model->id_field] = $this->line; - - yield $data; + if (!$this->getHeader($model)) { + $this->fileObject->fputcsv(array_keys($model->getFields('not system'))); } } - /** - * Loads any one record. - */ - public function loadAny(Model $model): array + public function getHeader(Model $model): array { - $data = $this->tryLoadAny($model); - - if (!$data) { - throw (new Exception('No more records', 404)) - ->addMoreInfo('model', $model); - } - - return $data; - } + $this->fileObject->seek(0); - /** - * Inserts record in data array and returns new record ID. - * - * @param array $data - * - * @return mixed - */ - public function insert(Model $model, $data) - { - if (!$this->mode) { - $this->mode = 'w'; - } elseif ($this->mode === 'r') { - throw new Exception('Currently reading records, so writing is not possible.'); - } - - if (!$this->handle) { - $this->saveHeader($model); - } - - $line = []; - - foreach ($this->header as $name) { - $line[] = $data[$name]; - } - - $this->putLine($line); - } - - /** - * Updates record in data array and returns record ID. - * - * @param mixed $id - * @param array $data - */ - public function update(Model $model, $id, $data, string $table = null) - { - throw new Exception('Updating records is not supported in CSV persistence.'); + return array_map(function ($name) { + return preg_replace('/[^a-z0-9_-]+/i', '_', $name); + }, $this->fileObject->current() ?: []); } /** @@ -358,46 +174,13 @@ public function delete(Model $model, $id, string $table = null) throw new Exception('Deleting records is not supported in CSV persistence.'); } - /** - * Generates new record ID. - * - * @return string - */ - public function generateNewId(Model $model, string $table = null) + public function lastInsertId(Model $model = null) { - if ($table === null) { - $table = $model->table; - } - - $ids = array_keys($this->data[$table]); - - $type = $model->getField($model->id_field)->type; - - switch ($type) { - case 'integer': - return count($ids) === 0 ? 1 : (max($ids) + 1); - case 'string': - return uniqid(); - default: - throw (new Exception('Unsupported id field type. Array supports type=integer or type=string only')) - ->addMoreInfo('type', $type); - } + return $this->lastInsertId; } - /** - * Export all DataSet. - */ - public function export(Model $model, array $fields = null, bool $typecast = true): array + public function query(Model $model): AbstractQuery { - $data = []; - - foreach ($model as $junk) { - $data[] = $fields !== null ? array_intersect_key($model->get(), array_flip($fields)) : $model->get(); - } - - // need to close file otherwise file pointer is at the end of file - $this->closeFile(); - - return $data; + return new Csv\Query($model, $this); } } diff --git a/src/Persistence/Csv/Query.php b/src/Persistence/Csv/Query.php new file mode 100644 index 0000000000..cbd4300ed3 --- /dev/null +++ b/src/Persistence/Csv/Query.php @@ -0,0 +1,46 @@ +fx = function (\Iterator $iterator) { + $keys = $this->fields ? array_flip((array) $this->fields) : []; + + $header = $this->persistence->getHeader($this->model); + + return new Persistence\ArrayCallbackIterator($iterator, function ($row, $id) use ($header, $keys) { + if (!$row) { + return []; + } + + $row = array_combine($header, $row); + + if ($this->model->id_field) { + $row[$this->model->id_field] = $id; + } + + return $keys ? array_intersect_key($row, $keys) : $row; + }); + }; + } + + protected function initSelect($fields = null): void + { + if ($fields) { + $this->fields = $fields; + } + } +} diff --git a/src/Persistence/IteratorQuery.php b/src/Persistence/IteratorQuery.php new file mode 100644 index 0000000000..abe359745c --- /dev/null +++ b/src/Persistence/IteratorQuery.php @@ -0,0 +1,393 @@ +iterator = $this->persistence->getRawDataIterator($model->table); + + $this->fx = function (\Iterator $iterator) { + return $iterator; + }; + } + + protected function initSelect($fields = null): void + { + if ($fields) { + $this->fields = $fields; + + $this->fx = function (\Iterator $iterator) { + $keys = array_flip((array) $this->fields); + + return new ArrayCallbackIterator($iterator, function ($row) use ($keys) { + return array_intersect_key($row, $keys); + }); + }; + } + } + + protected function initInsert(array $data): void + { + $this->fx = function (\Iterator $iterator) use ($data) { + return $this->persistence->setRawData($this->model, $data); + }; + } + + protected function initUpdate(array $data): void + { + $this->fx = function (\Iterator $iterator) use ($data) { + foreach ($iterator as $id => $row) { + $this->persistence->setRawData($this->model, array_merge($row, $data), $id); + } + }; + } + + protected function initDelete(): void + { + $this->fx = function (\Iterator $iterator) { + foreach ($iterator as $id => $row) { + $this->persistence->unsetRawData($this->model->table, $id); + } + }; + } + + /** + * Applies sorting on Iterator. + * + * @param array $fields + * + * @return $this + */ + protected function initOrder(): void + { + if ($this->order) { + $data = $this->get(); + + // prepare arguments for array_multisort() + $args = []; + foreach ($this->order as [$field, $desc]) { + $args[] = array_column($data, $field); + $args[] = $desc ? SORT_DESC : SORT_ASC; + } + $args[] = &$data; + + // call sorting + array_multisort(...$args); + + // put data back in generator + $this->iterator = new \ArrayIterator(array_pop($args)); + } + } + + protected function initLimit(): void + { + if ($args = $this->getLimitArgs()) { + [$limit, $offset] = $args; + + $this->iterator = new \LimitIterator($this->iterator, $offset, $limit); + } + } + + /** + * Counts number of rows and replaces our generator with just a single number. + * + * @return $this + */ + protected function initCount($alias = null): void + { + // @todo: kept for BC, inconstent results with SQL count! + $this->initLimit(); + + $alias = $alias ?? 'count'; + + $this->iterator = new \ArrayIterator([[$alias => iterator_count($this->iterator)]]); + } + + /** + * Checks if iterator has any rows. + * + * @return $this + */ + protected function initExists(): void + { + $this->iterator = new \ArrayIterator([[$this->iterator->valid() ? 1 : 0]]); + } + + protected function initField($fieldName, string $alias = null): void + { + if (!$fieldName) { + throw new Exception('Field query requires field name'); + } + + // get first record + if ($row = $this->persistence->query($this->model)->select([$fieldName])->getRow()) { + if ($alias && array_key_exists($fieldName, $row)) { + $row[$alias] = $row[$fieldName]; + unset($row[$fieldName]); + } + } + + $this->iterator = new \ArrayIterator([[$row]]); + } + + protected function doExecute() + { + return ($this->fx)($this->iterator); + } + + /** + * Return all data inside array. + */ + protected function doGet(): array + { + return iterator_to_array($this->doExecute(), true); + } + + /** + * Return one row of data. + */ + protected function doGetRow(): ?array + { + $iterator = $this->doExecute(); + + $iterator->rewind(); + + $row = $iterator->current(); + + if ($row && $this->model->id_field && !isset($row[$this->model->id_field])) { + $row[$this->model->id_field] = $this->iterator->key(); + } + + return $row; + } + + /** + * Return one value from one row of data. + * + * @return mixed + */ + protected function doGetOne() + { + $data = $this->getRow(); + + return reset($data); + } + + /** + * Calculates SUM|AVG|MIN|MAX aggragate values for $field. + * + * @param string $fieldName + * @param string $alias + */ + protected function initAggregate(string $functionName, $field, string $alias = null, bool $coalesce = false): void + { + $field = is_string($field) ? $field : $field->short_name; + + $result = 0; + $column = array_column($this->get(), $field); + + switch (strtoupper($functionName)) { + 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_ query unsupported aggregate function')) + ->addMoreInfo('function', $functionName); + } + + $this->iterator = new \ArrayIterator([[$result]]); + } + + /** + * Applies FilterIterator. + */ + protected function initWhere(): void + { + if (!$this->scope->isEmpty()) { + $this->iterator = new \CallbackFilterIterator($this->iterator, function ($row, $id) { + if ($this->model->id_field && !isset($row[$this->model->id_field])) { + $row[$this->model->id_field] = $id; + } + + return $this->match($row, $this->scope); + }); + } + } + + /** + * Checks if $row matches $condition. + */ + protected function match(array $row, Model\Scope\AbstractScope $condition): bool + { + $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 = Condition::OPERATOR_EQUALS; + } + + 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 Condition::OPERATOR_EQUALS: + $result = is_array($v2) ? $this->evaluateIf($v1, Condition::OPERATOR_IN, $v2) : $v1 == $v2; + + break; + case Condition::OPERATOR_GREATER: + $result = $v1 > $v2; + + break; + case Condition::OPERATOR_GREATER_EQUAL: + $result = $v1 >= $v2; + + break; + case Condition::OPERATOR_LESS: + $result = $v1 < $v2; + + break; + case Condition::OPERATOR_LESS_EQUAL: + $result = $v1 <= $v2; + + break; + case Condition::OPERATOR_DOESNOT_EQUAL: + $result = !$this->evaluateIf($v1, Condition::OPERATOR_EQUALS, $v2); + + break; + case Condition::OPERATOR_LIKE: + $pattern = str_ireplace('%', '(.*?)', preg_quote($v2)); + + $result = (bool) preg_match('/^' . $pattern . '$/', (string) $v1); + + break; + case Condition::OPERATOR_NOT_LIKE: + $result = !$this->evaluateIf($v1, Condition::OPERATOR_LIKE, $v2); + + break; + case Condition::OPERATOR_IN: + $result = is_array($v2) ? in_array($v1, $v2, true) : $this->evaluateIf($v1, '=', $v2); + + break; + case Condition::OPERATOR_NOT_IN: + $result = !$this->evaluateIf($v1, Condition::OPERATOR_IN, $v2); + + break; + case Condition::OPERATOR_REGEXP: + $result = (bool) preg_match('/' . $v2 . '/', $v1); + + break; + case Condition::OPERATOR_NOT_REGEXP: + $result = !$this->evaluateIf($v1, Condition::OPERATOR_REGEXP, $v2); + + break; + default: + throw (new Exception('Unsupported operator')) + ->addMoreInfo('operator', $operator); + } + + return $result; + } + + public function getDebug(): array + { + return array_merge([ + 'fields' => $this->fields, + ], parent::getDebug()); + } +} + +class ArrayCallbackIterator extends \IteratorIterator +{ + private $fx; + + public function __construct(\Traversable $iterator, $fx) + { + parent::__construct($iterator); + $this->fx = $fx; + } + + public function current() + { + return ($this->fx)(parent::current(), $this->key()); + } +} diff --git a/src/Reference/ContainsMany.php b/src/Reference/ContainsMany.php index 890860a54d..81ec6fae80 100644 --- a/src/Reference/ContainsMany.php +++ b/src/Reference/ContainsMany.php @@ -39,7 +39,7 @@ public function ref($defaults = []): Model // set some hooks for ref_model foreach ([Model::HOOK_AFTER_SAVE, Model::HOOK_AFTER_DELETE] as $spot) { $theirModel->onHook($spot, function ($theirModel) { - $rows = $theirModel->persistence->getRawDataByTable($this->table_alias); + $rows = iterator_to_array($theirModel->persistence->getRawDataIterator($this->table_alias)); $this->getOurModel()->save([ $this->getOurFieldName() => $rows ?: null, ]); diff --git a/src/Reference/ContainsOne.php b/src/Reference/ContainsOne.php index 6876b99546..d2347573e6 100644 --- a/src/Reference/ContainsOne.php +++ b/src/Reference/ContainsOne.php @@ -98,8 +98,8 @@ public function ref($defaults = []): Model // set some hooks for ref_model foreach ([Model::HOOK_AFTER_SAVE, Model::HOOK_AFTER_DELETE] as $spot) { $theirModel->onHook($spot, function ($theirModel) { - $row = $theirModel->persistence->getRawDataByTable($this->table_alias); - $row = $row ? array_shift($row) : null; // get first and only one record from array persistence + $rows = iterator_to_array($theirModel->persistence->getRawDataIterator($this->table_alias)); + $row = $rows ? array_shift($rows) : null; // get first and only one record from array persistence $this->getOurModel()->save([$this->getOurFieldName() => $row]); }); } diff --git a/tests/CsvTest.php b/tests/CsvTest.php index b1e3de01d3..8842be356b 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -113,13 +113,12 @@ public function testLoadAnyException() $m = new Model($p); $m->addField('name'); $m->addField('surname'); - $m->loadAny(); - $m->loadAny(); + $m->load(2); $this->assertSame('Sarah', $m->get('name')); $this->assertSame('Jones', $m->get('surname')); - $m->tryLoadAny(); + $m->tryLoad(3); $this->assertFalse($m->loaded()); } @@ -144,8 +143,8 @@ public function testPersistenceCopy() } $this->assertSame( - file_get_contents($this->file2), - file_get_contents($this->file) + file_get_contents($this->file), + file_get_contents($this->file2) ); } @@ -166,13 +165,13 @@ public function testExport() $m->addField('surname'); $this->assertSame([ - ['id' => 1, 'name' => 'John', 'surname' => 'Smith'], - ['id' => 2, 'name' => 'Sarah', 'surname' => 'Jones'], + 1 => ['name' => 'John', 'surname' => 'Smith', 'id' => 1], + 2 => ['name' => 'Sarah', 'surname' => 'Jones', 'id' => 2], ], $m->export()); $this->assertSame([ - ['surname' => 'Smith'], - ['surname' => 'Jones'], + 1 => ['surname' => 'Smith'], + 2 => ['surname' => 'Jones'], ], $m->export(['surname'])); } }