From e32fcf6f2c41a9322c04eb69e12023c8f51b78b2 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 10 Dec 2024 20:05:50 -0600 Subject: [PATCH 01/14] Major Change: Make the Row object thin. Added RowInterface. --- .github/workflows/phpunit.yml | 1 + README.md | 116 +++++------ composer.json | 5 +- docs/iteratorfilter.md | 52 +++++ docs/populate.md | 73 +++++++ docs/row.md | 115 ++++++++++ docs/rowoutput.md | 37 ++++ docs/rowvalidator.md | 27 +++ phpunit.xml.dist | 42 ++-- src/AnyDataset.php | 49 ++--- src/AnyIterator.php | 24 ++- src/Formatter/BaseFormatter.php | 13 +- src/GenericIterator.php | 44 ++-- src/IteratorFilter.php | 64 ++---- src/IteratorFilterFormatter.php | 86 ++++++-- src/IteratorInterface.php | 4 +- src/Row.php | 277 ++----------------------- src/RowArray.php | 90 ++++++++ src/RowInterface.php | 14 ++ src/RowObject.php | 83 ++++++++ src/RowOutput.php | 18 +- src/RowValidator.php | 9 +- tests/AnyDatasetTest.php | 81 +++++++- tests/IteratorFilterAnydatasetTest.php | 117 ++++++++++- tests/IteratorFilterXPathTest.php | 5 +- tests/ModelTest.php | 13 +- tests/RowOutputTest.php | 3 +- tests/RowTest.php | 188 ++++++----------- tests/RowValidatorTest.php | 5 +- tests/Sample/ModelPropertyPattern.php | 4 +- 30 files changed, 1032 insertions(+), 627 deletions(-) create mode 100644 docs/iteratorfilter.md create mode 100644 docs/populate.md create mode 100644 docs/row.md create mode 100644 docs/rowoutput.md create mode 100644 docs/rowvalidator.md create mode 100644 src/RowArray.php create mode 100644 src/RowInterface.php create mode 100644 src/RowObject.php diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index b65df91..c04291f 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -16,6 +16,7 @@ jobs: strategy: matrix: php-version: + - "8.4" - "8.3" - "8.2" - "8.1" diff --git a/README.md b/README.md index 13d306e..b7a8acc 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,68 @@ [![GitHub license](https://img.shields.io/github/license/byjg/anydataset.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/anydataset.svg)](https://github.com/byjg/php-anydataset/releases/) -Anydataset Core Module. Anydataset is an agnostic data source abstraction layer in PHP. +AnyDataset is a data source abstraction layer for PHP. It provides a simple and consistent interface +to access different data sources. -## Features +It is part of the [Anydataset project](https://packagist.org/providers/byjg/anydataset-implementation), +an agnostic data source abstraction layer for PHP. + +## Main Features - Access different data sources using the same interface. - Iterable results - Convert results to array +- Format output +- Validate fields + +## AnyDataset core + +The AnyDataset core provides the minimum classes and interfaces to allow you to use the AnyDataset project and create +your own implementation. + +### How it works? + +1. Create a database object +2. Get an iterator +3. Iterate over the results + +```php +getIterator(); +``` + +Iterating over the results: + +**for...each** +```php +foreach ($iterator as $row) { + print $row->toArray(); +} +``` + +**while** +```php +while ($iterator->hasNext()) { + $row = $iterator->moveNext(); + print $row->toArray(); +} +``` + +**toArray** +```php +print_r($iterator->toArray()); +``` ## Current Implementations | Object | Data Source | Read | Write | Reference | |------------------------|-----------------------|:----:|:-----:|-----------------------------------------------------| -| DbDriverInterface | Relational DB | yes | yes | [Github](https://github.com/byjg/anydataset-db) | | AnyDataSet | Anydataset | yes | yes | [Github](https://github.com/byjg/anydataset) | +| DbDriverInterface | Relational DB | yes | yes | [Github](https://github.com/byjg/anydataset-db) | | ArrayDataSet | Array | yes | no | [Github](https://github.com/byjg/anydataset-array) | | TextFileDataSet | Delimited Fields | yes | no | [Github](https://github.com/byjg/anydataset-text) | | FixedTextFileDataSet | Fixed Size fields | yes | no | [Github](https://github.com/byjg/anydataset-text) | @@ -30,19 +78,7 @@ Anydataset Core Module. Anydataset is an agnostic data source abstraction layer | NoSqlDocumentInterface | NoSql Document Based | yes | yes | [Github](https://github.com/byjg/anydataset-nosql) | | KeyValueInterface | NoSql Key/Value Based | yes | yes | [Github](https://github.com/byjg/anydataset-nosql) | -## Examples - -### Iterating with foreach -```php -getIterator(); -foreach ($iterator as $row) { - print $row->toArray(); -} -``` ### Filtering results @@ -81,52 +117,16 @@ foreach ($iterator as $row) { } ``` -## Additional Classes - -### RowOutpout - Format Field Output - -This class defines custom format for the field output. +## Topics -```php -addFormat("field1", "Test {field1}") - ->addFormat("field2", "Showing {} and {field3}"); - ->addCustomFormat("field3", function ($row, $field, $value) { - // return the formatted output. - // $row: The row object with all values - // $field: The field has been processed - // $value: The field value - }); - -// This will output the field1 formatted: -echo $output->print($row, "field1"); - -// This will apply the format defintion to all fields at once: -$ouput->apply($row); -``` +- [The Row object](docs/row.md) +- [Filtering the results](docs/iteratorfilter.md) +- [Format the output](docs/rowoutput.md) +- [Validate the fields](docs/rowvalidator.md) +- [Populate the fields](docs/populate.md) -Notes about the format pattern: - -- `{}` represents the current value -- `{.}` represents the field name -- `{field_name}` return the value of $row->get(field_name) - -### RowValidator - Validate Field contents +## Additional Classes -```php -requiredFields(["field1", "field2"]) - ->numericFields(['field1', 'field3']) - ->regexValidation("field4", '/\d{4}-\d{2}-\d{2}/') - ->customValidation("field3", function($value) { - // Return any string containing the error message if validation FAILS - // otherwise, just return null and the valition will pass. - }); - -$validator->validate($row) // Will return an array with the error messages. Empty array if not errors. -``` ## Formatters diff --git a/composer.json b/composer.json index 1edc6a4..b02dfdc 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "prefer-stable": true, "minimum-stability": "dev", "require": { - "php": ">=8.1 <8.4", + "php": ">=8.1 <8.5", "ext-dom": "*", "byjg/xmlutil": "^5.0", "byjg/serializer": "^5.0" @@ -25,5 +25,8 @@ "phpunit/phpunit": "^9.6", "vimeo/psalm": "^4.24" }, + "provide": { + "byjg/anydataset-implementation": "1.0" + }, "license": "MIT" } diff --git a/docs/iteratorfilter.md b/docs/iteratorfilter.md new file mode 100644 index 0000000..287b611 --- /dev/null +++ b/docs/iteratorfilter.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 2 +--- + +# Filtering Results + +You can filter the results of a query using the `IteratorFilter` class. This class simplifies creating filters +for use with an `Iterator`. It is a standard feature across all AnyDataset implementations. + +## Basic Usage + +```php +and('field', Relation::EQUAL, 10); + +// Create the Dataset +$dataset = new AnyDataset($file); + +// get the iterator +$iterator = $dataset->getIterator($filter); + +// This will return an iterator with only the rows where the field is equal to 10 +``` + +## And / Or Conditions + +For more complex queries, you can use the `and` and `or` methods to combine conditions. + +For example: + +If we want to filter the rows where the field is equal to 10 **or** 2, **and** the field2 is equal to 20: + +```text +(field = 10 OR field = 2) AND field2 = 20 +``` + +We can do this: + +```php +startGroup('field', Relation::EQUAL, 10); +$filter->or('field', Relation::EQUAL, 2); +$filter->endGroup(); +$filter->and('field2', Relation::EQUAL, 20); + +$iterator = $dataset->getIterator($filter); +``` + + diff --git a/docs/populate.md b/docs/populate.md new file mode 100644 index 0000000..99e3d55 --- /dev/null +++ b/docs/populate.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 5 +--- + +# Populate AnyDataSet + +You can populate an AnyDataSet with data from an array or a file. + +## Populate from an Array + +You can populate an AnyDataSet with data from an array. + +```php + 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Mary'], + ['id' => 3, 'name' => 'Paul'], +]; + +$dataset = new AnyDataset($data); +``` + +## Populate from a File + +You can populate an AnyDataSet with data from a file. + +```php + + + + 1 + John + + + 2 + Mary + + + 3 + Paul + + +``` + +## From scratch + +You can also create an empty dataset and populate it later. + +```php +appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 2, 'name' => 'Mary']); +$dataset->appendRow(['id' => 3, 'name' => 'Paul']); +``` + + diff --git a/docs/row.md b/docs/row.md new file mode 100644 index 0000000..745ad28 --- /dev/null +++ b/docs/row.md @@ -0,0 +1,115 @@ +--- +sidebar_position: 1 +--- + +# The Row object + +For each row returned by the dataset, you will receive a `Row` object. + +This object is a collection of key-value pairs, where the key is the field name and the value is the field value and +implements the `RowInterface` interface. + +This is particularly useful to allow you to access the fields in a row in a consistent way, +regardless of the dataset type. + +## Interface + +The `Row` object implements the following methods: + +| Method | Description | +|-----------------------------------------|-------------------------------------------------------------------------------------------| +| `get($field)` | Get the value of the field. | +| `set($field, $value)` | Set the value of the field. | +| `set($field, $value, $append)` | Set the value of the field. If append == true, it will add the value to the current field | +| `unset($field)` | Remove the field from the row. | +| `replace($field, $oldValue, $newValue)` | Replace the value of the field. If $oldValue is not set, nothing is changed. | +| `toArray($fields)` | Convert the row to an array. | +| `entity()` | Return the entity object used to store the row contents. | + +## Example + +```php +getIterator(); + +while ($iterator->hasNext()) { + $row = $iterator->moveNext(); + + echo $row->get("field1"); +} +``` + +## RowInterface implementations + +The `RowInterface` has two implementations: + +- `RowArray` - The default implementation +- `RowObject` - An implementation that uses an object to store the values + +When we iterate over a dataset, we receive a `Row` object. +The `Row` object decides how to store/get the values. + +### RowArray + +This is the default implementation. It uses an array to store the values. + +### RowObject + +This implementation uses an object to store the values. Some datasets, like the `AnyDatasetDb` dataset, can return a `RowObject` instead of a `RowArray`. +It doesn't change anything in the way you access the values, but it can be useful if you need to use the `entity()` method. + +```php +id = $id; + $this->name = $name; + } + + public function getId(): int + { + return $this->id; + } + + public function setId(int $id): void + { + $this->id = $id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } +} + +$model = new Model(id: 1, name: 'John'); + +$row = new RowObject($model); + +$row->get('id'); // 1 +$row->get('name'); // John +$row->entity(); // $model + +$row->set('name', 'Mary'); +$row->get('name'); // Mary +$row->entity()->getName(); // Mary + +$row->entity()->setId('20'); +$row->get('id'); // 20 +``` + + + + diff --git a/docs/rowoutput.md b/docs/rowoutput.md new file mode 100644 index 0000000..705e161 --- /dev/null +++ b/docs/rowoutput.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 3 +--- + +# RowOutput - Format a field + +This class allows you to format a field value based on a pattern. +The pattern can be a simple string or a custom function. + +```php +addFormat("field1", "Test {field1}") + ->addFormat("field2", "Showing {} and {field3}"); + ->addCustomFormat("field3", function ($row, $field, $value) { + // return the formatted output. + // $row: The row object with all values + // $field: The field has been processed + // $value: The field value + }); + +// This will output the field1 formatted: +echo $output->print($row, "field1"); + +// This will apply the format defintion to all fields at once: +$ouput->apply($row); +``` + +Format pattern: + +| Pattern | Description | +|----------------|------------------------------------| +| `{}` | The current value | +| `{.}` | The field name | +| `{field_name}` | The value of $row->get(field_name) | + + diff --git a/docs/rowvalidator.md b/docs/rowvalidator.md new file mode 100644 index 0000000..814701a --- /dev/null +++ b/docs/rowvalidator.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 4 +--- + + +# Row Validator - Validate Field contents + +The `RowValidator` class allows you to validate the contents of the fields in a `Row` object. +You can use the `RowValidator` class to ensure that the data in the fields meets your requirements. + +## Usage + +```php +requiredFields(["field1", "field2"]) + ->numericFields(['field1', 'field3']) + ->regexValidation("field4", '/\d{4}-\d{2}-\d{2}/') + ->customValidation("field3", function($value) { + // Return any string containing the error message if validation FAILS + // otherwise, just return null and the valition will pass. + }); + +$validator->validate($row) // Will return an array with the error messages. Empty array if not errors. +``` + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ebc7bf4..fc18251 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,33 +4,31 @@ To change this license header, choose License Headers in Project Properties. To change this template file, choose Tools | Templates and open the template in the editor. --> - - - - - - - - - - - - ./src - - + stopOnFailure="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + + + + + - - - ./tests - - + + + ./src + + + + + ./tests + + diff --git a/src/AnyDataset.php b/src/AnyDataset.php index 617b1dd..91bec10 100644 --- a/src/AnyDataset.php +++ b/src/AnyDataset.php @@ -52,7 +52,7 @@ class AnyDataset /** * Internal structure represent the current Row * - * @var Row[] + * @var RowInterface[] */ private array $collection; @@ -65,16 +65,20 @@ class AnyDataset private ?File $file; /** - * @param string|null $filename + * @param string|array|null $filename * @throws FileException * @throws XmlUtilException */ - public function __construct(?string $filename = null) + public function __construct(string|array|null $filename = null) { - $this->collection = array(); + $this->file = null; $this->currentRow = -1; + $this->collection = []; + if (is_array($filename)) { + $this->collection = array_map(fn($row) => Row::factory($row), $filename); + return; + } - $this->file = null; $this->defineSavePath($filename, function () { if (!is_null($this->file)) { $this->createFromFile(); @@ -114,7 +118,7 @@ private function defineSavePath(?string $filename, Closure $closure): void * Private method used to read and populate anydataset class from specified file * * @return void - * @throws XmlUtilException + * @throws XmlUtilException|FileException */ private function createFromFile(): void { @@ -124,16 +128,15 @@ private function createFromFile(): void $rows = $anyDataSet->selectNodes("row"); foreach ($rows as $row) { - $sr = new Row(); + $sr = Row::factory(); $fields = XmlNode::instance($row)->selectNodes("field"); /** @var DOMElement $field */ foreach ($fields as $field) { if (!$field->hasAttribute("name")) { throw new InvalidArgumentException('Malformed anydataset file ' . basename($this->getFilename())); } - $sr->addField($field->getAttribute("name"), $field->nodeValue); + $sr->set($field->getAttribute("name"), $field->nodeValue); } - $sr->acceptChanges(); $this->collection[] = $sr; } $this->currentRow = count($this->collection) - 1; @@ -170,15 +173,13 @@ public function save(?string $filename = null): void /** * Append one row to AnyDataset. * - * @param Row|array $singleRow + * @param array|object $singleRow * @return void */ - public function appendRow(Row|array $singleRow = []): void + public function appendRow(array|object $singleRow = []): void { - if (empty($singleRow)) { - $singleRow = new Row(); - } elseif (is_array($singleRow)) { - $singleRow = new Row($singleRow); + if (!($singleRow instanceof RowInterface)) { + $singleRow = Row::factory($singleRow); } $this->collection[] = $singleRow; @@ -202,16 +203,16 @@ public function import(GenericIterator $iterator): void * Insert one row before specified position. * * @param int $rowNumber - * @param Row|array $row + * @param array|object $row */ - public function insertRowBefore(int $rowNumber, Row|array $row): void + public function insertRowBefore(int $rowNumber, array|object $row): void { if ($rowNumber > count($this->collection)) { $this->appendRow($row); } else { $singleRow = $row; - if (!($row instanceof Row)) { - $singleRow = new Row($row); + if (!($row instanceof RowInterface)) { + $singleRow = Row::factory($row); } /** @@ -227,16 +228,16 @@ public function insertRowBefore(int $rowNumber, Row|array $row): void /** * - * @param int|Row|null $row + * @param int|RowInterface|null $row * @return void * @throws \ByJG\Serializer\Exception\InvalidArgumentException */ - public function removeRow(int|Row $row = null): void + public function removeRow(int|RowInterface $row = null): void { if (is_null($row)) { $row = $this->currentRow; } - if ($row instanceof Row) { + if ($row instanceof RowInterface) { $iPos = 0; $rowArr = $row->toArray(); foreach ($this->collection as $sr) { @@ -268,7 +269,7 @@ public function addField(string $name, mixed $value): void if ($this->currentRow < 0) { $this->appendRow(); } - $this->collection[$this->currentRow]->addField($name, $value); + $this->collection[$this->currentRow]->set($name, $value); } /** @@ -317,7 +318,7 @@ public function sort(string $field): void } /** - * @param Row[] $seq + * @param RowInterface[] $seq * @param string $field * @return array */ diff --git a/src/AnyIterator.php b/src/AnyIterator.php index b6180cc..957898f 100644 --- a/src/AnyIterator.php +++ b/src/AnyIterator.php @@ -2,6 +2,8 @@ namespace ByJG\AnyDataset\Core; +use ReturnTypeWillChange; + /** * Iterator class is a structure used to navigate forward in a AnyDataset structure. */ @@ -12,20 +14,20 @@ class AnyIterator extends GenericIterator * Row Elements * @var array */ - private $list; + private array $list; /** * Current row number * @var int */ - private $curRow; //int + private int $curRow; /** * Iterator constructor * * @param Row[] $list */ - public function __construct($list) + public function __construct(array $list) { $this->curRow = 0; $this->list = $list; @@ -50,7 +52,7 @@ public function hasNext(): bool /** * @inheritDoc */ - public function moveNext(): Row|null + public function moveNext(): RowInterface|null { if (!$this->hasNext()) { return null; @@ -61,16 +63,26 @@ public function moveNext(): Row|null /** * @inheritDoc */ - public function key() + #[ReturnTypeWillChange] + public function key(): mixed { return $this->curRow; } + /** + * @inheritDoc + */ + #[ReturnTypeWillChange] + public function current(): mixed + { + return $this->list[$this->curRow] ?? null; + } + /** * @param IteratorFilter $filter * @return AnyIterator */ - public function withFilter(IteratorFilter $filter) + public function withFilter(IteratorFilter $filter): AnyIterator { return new AnyIterator($filter->match($this->list)); } diff --git a/src/Formatter/BaseFormatter.php b/src/Formatter/BaseFormatter.php index 03613db..88b5376 100644 --- a/src/Formatter/BaseFormatter.php +++ b/src/Formatter/BaseFormatter.php @@ -3,15 +3,15 @@ namespace ByJG\AnyDataset\Core\Formatter; use ByJG\AnyDataset\Core\GenericIterator; -use ByJG\AnyDataset\Core\Row; +use ByJG\AnyDataset\Core\RowInterface; use InvalidArgumentException; abstract class BaseFormatter implements FormatterInterface { /** - * @var GenericIterator|Row + * @var GenericIterator|RowInterface */ - protected Row|GenericIterator $object; + protected RowInterface|GenericIterator $object; /** * @inheritDoc @@ -35,13 +35,10 @@ public function saveToFile(string $filename): void } /** - * @param GenericIterator|Row $object + * @param GenericIterator|RowInterface $object */ - public function __construct(GenericIterator|Row $object) + public function __construct(GenericIterator|RowInterface $object) { - if (!($object instanceof GenericIterator) && !($object instanceof Row)) { - throw new InvalidArgumentException("Constructor must have a GenericIterator or Row instance in the argument"); - } $this->object = $object; } } \ No newline at end of file diff --git a/src/GenericIterator.php b/src/GenericIterator.php index 63d97f3..5bcd4eb 100644 --- a/src/GenericIterator.php +++ b/src/GenericIterator.php @@ -3,6 +3,7 @@ namespace ByJG\AnyDataset\Core; use Iterator; +use ReturnTypeWillChange; abstract class GenericIterator implements IteratorInterface, Iterator { @@ -15,19 +16,13 @@ abstract public function hasNext(): bool; /** * @inheritDoc */ - abstract public function moveNext(): Row|null; + abstract public function moveNext(): RowInterface|null; /** * @inheritDoc */ abstract public function count(): int; - /** - * @inheritDoc - */ - #[\ReturnTypeWillChange] - abstract public function key(); - /** * @inheritDoc * @param array $fields @@ -44,42 +39,45 @@ public function toArray(array $fields = []): array return $retArray; } - /* ------------------------------------- */ - /* PHP 5 Specific functions for Iterator */ - /* ------------------------------------- */ + /* --------------------------------------------- */ + /* PHP Specific functions for Iterator interface */ + /* --------------------------------------------- */ + + /** + * @inheritDoc + */ + #[ReturnTypeWillChange] + abstract public function key(): mixed; /** * @return mixed */ - #[\ReturnTypeWillChange] - public function current() - { - return $this->moveNext(); - } + #[ReturnTypeWillChange] + abstract public function current(): mixed; /** * @inheritDoc */ - #[\ReturnTypeWillChange] - public function rewind() + #[ReturnTypeWillChange] + public function rewind(): void { - // There is no necessary + // Do nothing } /** * @inheritDoc */ - #[\ReturnTypeWillChange] - public function next() + #[ReturnTypeWillChange] + public function next(): void { - // There is no necessary + $this->moveNext(); } /** * @inheritDoc */ - #[\ReturnTypeWillChange] - public function valid() + #[ReturnTypeWillChange] + public function valid(): bool { return $this->hasNext(); } diff --git a/src/IteratorFilter.php b/src/IteratorFilter.php index 2cafb4d..0388df7 100644 --- a/src/IteratorFilter.php +++ b/src/IteratorFilter.php @@ -35,9 +35,21 @@ public static function getInstance(): IteratorFilter public function match(array $array): array { $returnArray = []; + $filterList = $this->format(new IteratorFilterFormatter()); + + if (empty($filterList)) { + return $array; + } foreach ($array as $sr) { - if ($this->evalString($sr)) { + $rowArr = $sr->toArray(); + $rowEval = []; + foreach ($rowArr as $key => $value) { + $rowEval["%$key"] = is_numeric($value) ? $value : "'$value'"; + } + + $result = eval("return " . strtr($filterList, $rowEval) . ";"); + if ($result) { $returnArray[] = $sr; } } @@ -59,52 +71,6 @@ public function format(IteratorFilterFormatter $formatter, string $tableName = n return $formatter->format($this->filters, $tableName, $params, $returnFields); } - - /** - * @param Row $singleRow - * @return bool - */ - private function evalString(Row $singleRow): bool - { - $result = []; - $finalResult = false; - $pos = 0; - - $result[0] = true; - - foreach ($this->filters as $filter) { - if (($filter[0] == ")") || ($filter[0] == " or ")) { - $finalResult = $finalResult || $result[$pos]; - $result[++$pos] = true; - } - - $name = $filter[1]; - $relation = $filter[2]; - $value = $filter[3]; - - $field = [$singleRow->get($name)]; - - foreach ($field as $valueparam) { - $result[$pos] = match ($relation) { - Relation::EQUAL => $result[$pos] && ($valueparam == $value), - Relation::GREATER_THAN => $result[$pos] && ($valueparam > $value), - Relation::LESS_THAN => $result[$pos] && ($valueparam < $value), - Relation::GREATER_OR_EQUAL_THAN => $result[$pos] && ($valueparam >= $value), - Relation::LESS_OR_EQUAL_THAN => $result[$pos] && ($valueparam <= $value), - Relation::NOT_EQUAL => $result[$pos] && ($valueparam != $value), - Relation::STARTS_WITH => $result[$pos] && (str_starts_with(is_null($valueparam) ? "" : $valueparam, $value)), - Relation::IN => $result[$pos] && in_array($valueparam, $value), - Relation::NOT_IN => $result[$pos] && !in_array($valueparam, $value), - default => $result[$pos] && (str_contains(is_null($valueparam) ? "" : $valueparam, $value)), - }; - } - } - - $finalResult = $finalResult || $result[$pos]; - - return $finalResult; - } - /** * @param string $name Field name * @param Relation $relation Relation enum @@ -161,9 +127,9 @@ public function or(string $name, Relation $relation, mixed $value): static * Add a "(" * @return static */ - public function startGroup(): static + public function startGroup(string $name, Relation $relation, mixed $value): static { - $this->filters[] = ["(", "", "", ""]; + $this->filters[] = ["(", $name, $relation, $value]; return $this; } diff --git a/src/IteratorFilterFormatter.php b/src/IteratorFilterFormatter.php index 064d4fa..0ffb1cb 100644 --- a/src/IteratorFilterFormatter.php +++ b/src/IteratorFilterFormatter.php @@ -4,7 +4,7 @@ use ByJG\AnyDataset\Core\Enum\Relation; -abstract class IteratorFilterFormatter +class IteratorFilterFormatter { /** @@ -16,7 +16,61 @@ abstract class IteratorFilterFormatter * @param array $param * @return string */ - abstract public function getRelation(string $name, Relation $relation, mixed $value, array &$param): string; + public function getRelation(string $name, Relation $relation, mixed $value, array &$param): string + { + if (is_array($value)) { + foreach ($value as $key => $val) { + $value[$key] = is_numeric($val) ? $val : "'$val'"; + } + $value = "[" . implode(",", $value) . "]"; + } else { + $value = is_numeric($value) ? $value : "'$value'"; + } + + switch ($relation) { + case Relation::EQUAL: + $return = "%$name == $value"; + break; + + case Relation::GREATER_THAN: + $return = "%$name > $value"; + break; + + case Relation::LESS_THAN: + $return = "%$name < $value"; + break; + + case Relation::GREATER_OR_EQUAL_THAN: + $return = "%$name >= $value"; + break; + + case Relation::LESS_OR_EQUAL_THAN: + $return = "%$name <= $value"; + break; + + case Relation::NOT_EQUAL: + $return = "%$name != $value"; + break; + + case Relation::STARTS_WITH: + $return = " str_starts_with(%$name, $value) "; + break; + + case Relation::IN: + $return = " in_array(%$name, $value) "; + break; + + case Relation::NOT_IN: + $return = " !in_array(%$name, $value) "; + break; + + default: // Relation::CONTAINS: + $return = " str_contains(%$name, $value) "; + break; + } + + return $return; + } /** * Get formatted field @@ -27,7 +81,10 @@ abstract public function getRelation(string $name, Relation $relation, mixed $va * @param string $returnFields * @return string */ - abstract public function format(array $filters, string $tableName = null, array &$params = [], string $returnFields = "*"): string; + public function format(array $filters, string $tableName = null, array &$params = [], string $returnFields = "*"): string + { + return $this->getFilter($filters, $params); + } /** * Get Filter @@ -41,23 +98,16 @@ public function getFilter(array $filters, array &$param): string $filter = ""; $param = array(); - $previousValue = null; + $first = true; foreach ($filters as $value) { - if ($value[0] == "(") { - if (!is_null($previousValue)) { - $filter .= " or ( "; - } else { - $filter .= " ( "; - } - } elseif ($value[0] == ")") { - $filter .= ")"; - } else { - if ((!is_null($previousValue)) && ($previousValue[0] != "(")) { - $filter .= $value[0]; - } - $filter .= $this->getRelation($value[1], $value[2], $value[3], $param); + if (!$first || $value[0] == "(") { + $filter .= $value[0]; + } + $first = false; + if ($value[0] == ")") { + continue; } - $previousValue = $value; + $filter .= $this->getRelation($value[1], $value[2], $value[3], $param); } return $filter; diff --git a/src/IteratorInterface.php b/src/IteratorInterface.php index 0e37137..327455f 100644 --- a/src/IteratorInterface.php +++ b/src/IteratorInterface.php @@ -15,9 +15,9 @@ public function hasNext(): bool; /** * Get the next record.Return a Row object * - * @return Row|null + * @return RowInterface|null */ - public function moveNext(): Row|null; + public function moveNext(): RowInterface|null; /** * Get the record count. Some implementations may have return -1. diff --git a/src/Row.php b/src/Row.php index 0ff52ac..8514f9f 100644 --- a/src/Row.php +++ b/src/Row.php @@ -2,294 +2,53 @@ namespace ByJG\AnyDataset\Core; -use ByJG\Serializer\Serialize; - -class Row +class Row implements RowInterface { - - /** - * @var array - */ - private array $row = []; - - /** - * @var array - */ - private array $originalRow = []; - /** - * @var boolean + * @var mixed */ - protected bool $fieldNameCaseSensitive = true; + private RowInterface $entity; - /** - * Row constructor - * - * @param object|array $instance - */ public function __construct(object|array $instance = []) { - if (is_array($instance)) { - $this->row = $instance; - } else { - $this->row = Serialize::from($instance)->toArray(); - } - - $this->acceptChanges(); + $this->entity = Row::factory($instance); } - /** - * Add a string field to row - * @param string $name - * @param array|string|null $value - * @return void - */ - public function addField(string $name, array|string|null $value): void + public static function factory(object|array $instance = []): RowInterface { - $name = $this->getHydratedFieldName($name); - - if (!array_key_exists($name, $this->row)) { - $this->row[$name] = $value; - } elseif (is_array($this->row[$name])) { - $this->row[$name][] = $value; - } else { - $this->row[$name] = array($this->row[$name], $value); + if (is_array($instance)) { + return new RowArray($instance); } + return new RowObject($instance); } - /** - * @param string $name - Field name - * @return mixed - * @desc et the string value from a field name - */ public function get(string $name): mixed { - $name = $this->getHydratedFieldName($name); - - if (!array_key_exists($name, $this->row)) { - return null; - } - - $result = $this->row[$name]; - if (is_array($result)) { - return array_shift($result); - } else { - return $result; - } + return $this->entity->get($name); } - /** - * Get array from a single field - * - * @param string $fieldName - * @return array - */ - public function getAsArray(string $fieldName): array + public function set(string $name, mixed $value, bool $append = false): void { - $fieldName = $this->getHydratedFieldName($fieldName); - - if (!array_key_exists($fieldName, $this->row)) { - return []; - } - - $result = $this->row[$fieldName]; - - if (empty($result)) { - return []; - } - - return (array)$result; - } - - /** - * Return all Field Names from current Row - * @return array - */ - public function getFieldNames(): array - { - return array_keys($this->row); - } - - /** - * Set a string value to existing field name - * @param string $name - * @param string $value - * @return void - */ - public function set(string $name, mixed $value): void - { - $name = $this->getHydratedFieldName($name); - - if (!array_key_exists($name, $this->row)) { - $this->addField($name, $value); - } else { - $this->row[$name] = $value; - } + $this->entity->set($name, $value, $append); } - /** - * Remove specified field name from row. - * - * @param string $fieldName - */ - public function removeField(string $fieldName): void + public function unset(string $name, mixed $value = null): void { - $fieldName = $this->getHydratedFieldName($fieldName); - - if (array_key_exists($fieldName, $this->row)) { - unset($this->row[$fieldName]); - } + $this->entity->unset($name, $value); } - /** - * Remove specified field name with specified value name from row. - * - * @param string $fieldName - * @param mixed $value - * @return void - */ - public function removeValue(string $fieldName, mixed $value): void + public function replace(string $name, mixed $oldValue, mixed $newValue): void { - $fieldName = $this->getHydratedFieldName($fieldName); - - $result = $this->row[$fieldName]; - if (!is_array($result)) { - if ($value == $result) { - unset($this->row[$fieldName]); - } - } else { - $qty = count($result); - for ($i = 0; $i < $qty; $i++) { - if ($result[$i] == $value) { - unset($result[$i]); - } - } - $this->row[$fieldName] = array_values($result); - } + $this->entity->replace($name, $oldValue, $newValue); } - /** - * Update a specific field and specific value with new value - * - * @param string $fieldName - * @param mixed $oldvalue - * @param mixed $newvalue - */ - public function replaceValue(string $fieldName, mixed $oldvalue, mixed $newvalue): void - { - $fieldName = $this->getHydratedFieldName($fieldName); - - $result = $this->row[$fieldName]; - if (!is_array($result)) { - if ($oldvalue == $result) { - $this->row[$fieldName] = $newvalue; - } - } else { - for ($i = count($result) - 1; $i >= 0; $i--) { - if ($result[$i] == $oldvalue) { - $this->row[$fieldName][$i] = $newvalue; - } - } - } - } - - /** - * @param array|null $fields - * @return array - */ public function toArray(?array $fields = []): array { - if (empty($fields)) { - return $this->row; - } - - $fieldAssoc = array_combine($fields, array_fill(0, count($fields), null)); - return array_intersect_key(array_merge($fieldAssoc, $this->row), $fieldAssoc); - } - - /** - * @return array - */ - public function getAsRaw(): array - { - return $this->originalRow; - } - - /** - * - * @return bool - */ - public function hasChanges(): bool - { - return ($this->row != $this->originalRow); - } - - /** - * @return void - */ - public function acceptChanges(): void - { - $this->originalRow = $this->row; - } - - /** - * @return void - */ - public function rejectChanges(): void - { - $this->row = $this->originalRow; - } - - /** - * Override Specific implementation of setPropValue to Row - * - * @param Row $obj - * @param string $propName - * @param mixed $value - * @return void - */ - protected function setPropValue(Row $obj, string $propName, mixed $value): void - { - $obj->set($propName, $value); - } - - /** - * @param string $name - * @return bool - */ - public function fieldExists(string $name): bool - { - return isset($this->row[$this->getHydratedFieldName($name)]); - } - - /** - * @return void - */ - public function enableFieldNameCaseInSensitive(): void - { - $this->row = array_change_key_case($this->row, CASE_LOWER); - $this->originalRow = array_change_key_case($this->originalRow, CASE_LOWER); - $this->fieldNameCaseSensitive = false; + return $this->entity->toArray($fields); } - /** - * @return bool - */ - public function isFieldNameCaseSensitive(): bool + public function entity(): mixed { - return $this->fieldNameCaseSensitive; - } - - /** - * @param string $name - * @return string - */ - protected function getHydratedFieldName(string $name): string - { - if (!$this->isFieldNameCaseSensitive()) { - return strtolower($name); - } - - return $name; + return $this->entity->entity(); } } diff --git a/src/RowArray.php b/src/RowArray.php new file mode 100644 index 0000000..cd04532 --- /dev/null +++ b/src/RowArray.php @@ -0,0 +1,90 @@ +entity = $instance; + } + + + public function get(string $name): mixed + { + return $this->entity[$name] ?? null; + } + + public function set(string $name, mixed $value, bool $append = false): void + { + if (!isset($this->entity[$name])) { + $this->entity[$name] = $value; + return; + } + + if ($append) { + if (!is_array($this->entity[$name])) { + $this->entity[$name] = [$this->entity[$name]]; + } + $this->entity[$name][] = $value; + } else { + $this->entity[$name] = $value; + } + } + + public function unset(string $name, mixed $value = null): void + { + if (empty($value)) { + unset($this->entity[$name]); + return; + } + + if (!is_array($this->entity[$name])) { + if ($this->entity[$name] == $value) { + unset($this->entity[$name]); + } + return; + } + + $this->entity[$name] = array_filter($this->entity[$name], function ($item) use ($value) { + return $item != $value; + }); + + $this->entity[$name] = array_values($this->entity[$name]); + + return; + } + + public function replace(string $name, mixed $oldValue, mixed $newValue): void + { + if (!is_array($this->entity[$name])) { + $this->entity[$name] = $this->entity[$name] == $oldValue ? $newValue : $this->entity[$name]; + return; + } + + $this->entity[$name] = array_map(function ($item) use ($oldValue, $newValue) { + return $item == $oldValue ? $newValue : $item; + }, $this->entity[$name]); + } + + public function toArray(?array $fields = []): array + { + if (empty($fields)) { + return $this->entity; + } + + // return an array with only the fields in $fields + return array_intersect_key($this->entity, array_flip($fields)); + + } + + public function entity(): mixed + { + return $this->entity; + } +} diff --git a/src/RowInterface.php b/src/RowInterface.php new file mode 100644 index 0000000..79a9543 --- /dev/null +++ b/src/RowInterface.php @@ -0,0 +1,14 @@ +entity = $instance; + } + + + public function get(string $name): mixed + { + if (property_exists($this->entity, $name)) { + return $this->entity->$name; + } + + if (method_exists($this->entity, "get$name")) { + $name = "get$name"; + return $this->entity->$name(); + } + + return null; + } + + public function set(string $name, mixed $value, bool $append = false): void + { + if ($append) { + throw new \InvalidArgumentException("Append is not supported for object"); + } + + if (property_exists($this->entity, $name)) { + $this->entity->$name = $value; + return; + } + + if (method_exists($this->entity, "set$name")) { + $name = "set$name"; + $this->entity->$name($value); + return; + } + + throw new \InvalidArgumentException("Field '$name' not found"); + } + + public function unset(string $name, mixed $value = null): void + { + throw new \InvalidArgumentException("Unset is not supported for object"); + } + + public function replace(string $name, mixed $oldValue, mixed $newValue): void + { + throw new \InvalidArgumentException("Replace is not supported for object"); + } + + public function toArray(?array $fields = []): array + { + $result = Serialize::from($this->entity)->toArray(); + + if (empty($fields)) { + return $result; + } + + $retArray = []; + foreach ($fields as $field) { + $retArray[$field] = $result[$field] ?? null; + } + return $retArray; + } + + public function entity(): mixed + { + return $this->entity; + } +} diff --git a/src/RowOutput.php b/src/RowOutput.php index bbe3cee..c1fc1ba 100644 --- a/src/RowOutput.php +++ b/src/RowOutput.php @@ -22,11 +22,11 @@ public static function getInstance(): RowOutput } /** - * @param Row $row + * @param RowInterface $row * @param string $field * @return mixed */ - public function print(Row $row, string $field): mixed + public function print(RowInterface $row, string $field): mixed { if (!isset($this->fieldList[$field])) { return $row->get($field); @@ -43,10 +43,10 @@ public function print(Row $row, string $field): mixed } /** - * @param Row $row - * @return Row + * @param RowInterface $row + * @return RowInterface */ - public function apply(Row $row): Row + public function apply(RowInterface $row): RowInterface { $newRow = new Row(); @@ -61,12 +61,12 @@ public function apply(Row $row): Row } /** - * @param Row $row + * @param RowInterface $row * @param string $field * @param string $pattern * @return string */ - protected function formatPattern(Row $row, string $field, string $pattern): string + protected function formatPattern(RowInterface $row, string $field, string $pattern): string { $rowParsed = $row->toArray(); foreach ($rowParsed as $key => $value) { @@ -80,12 +80,12 @@ protected function formatPattern(Row $row, string $field, string $pattern): stri } /** - * @param Row $row + * @param RowInterface $row * @param string $field * @param mixed $closure * @return string */ - protected function formatCustom(Row $row, string $field, Closure $closure): string + protected function formatCustom(RowInterface $row, string $field, Closure $closure): string { return $closure($row, $field, $row->get($field)); } diff --git a/src/RowValidator.php b/src/RowValidator.php index 0f27e8f..90046c3 100644 --- a/src/RowValidator.php +++ b/src/RowValidator.php @@ -8,7 +8,7 @@ class RowValidator /** * @var array */ - protected $fieldValidator = []; + protected array $fieldValidator = []; const REQUIRED="required"; const NUMBER="number"; @@ -16,17 +16,14 @@ class RowValidator const CUSTOM="custom"; /** - * @param string|array $fieldList + * @param array|string $fieldList * @param string $property * @param mixed $value * @return void */ - protected function setProperty($fieldList, $property, $value) + protected function setProperty(array|string $fieldList, string $property, mixed $value) { foreach ((array)$fieldList as $field) { - if (!isset($this->fieldValidator[$field])) { - $this->fieldValidator[$field] = []; - } $this->fieldValidator[$field] = [ $property => $value ]; } } diff --git a/tests/AnyDatasetTest.php b/tests/AnyDatasetTest.php index 996a618..b8a3ddd 100644 --- a/tests/AnyDatasetTest.php +++ b/tests/AnyDatasetTest.php @@ -7,8 +7,11 @@ use ByJG\AnyDataset\Core\Formatter\JsonFormatter; use ByJG\AnyDataset\Core\Formatter\XmlFormatter; use ByJG\AnyDataset\Core\IteratorFilter; +use ByJG\XmlUtil\Exception\FileException; +use ByJG\XmlUtil\Exception\XmlUtilException; use ByJG\XmlUtil\XmlDocument; use PHPUnit\Framework\TestCase; +use Tests\Sample\ModelPublic; class AnyDatasetTest extends TestCase { @@ -57,8 +60,7 @@ public function testConstructorString() ], ], $anydata->getIterator()->toArray()); - $anydataMem = new AnyDataset("php://memory"); - $anydataMem->import($anydata->getIterator()); + $anydataMem = new AnyDataset(self::SAMPLE_DIR . 'sample.anydata.xml'); $this->assertEquals(2, count($anydataMem->getIterator()->toArray())); $this->assertEquals([ [ @@ -70,7 +72,17 @@ public function testConstructorString() "field2" => "othervalue2", ], ], $anydata->getIterator()->toArray()); - $anydataMem->save(); + + try { + $anydataMem->save("/tmp/sample"); + $this->assertFileExists("/tmp/sample.anydata.xml"); + $this->assertEquals( + preg_replace("/(\n|\\s\\s)/", "", file_get_contents(self::SAMPLE_DIR . 'sample.anydata.xml')), + str_replace("\n", "", file_get_contents("/tmp/sample.anydata.xml")) + ); + } finally { + unlink("/tmp/sample.anydata.xml"); + } } public function testXML() @@ -343,8 +355,69 @@ public function testToArrayFields() ], [ "field1" => "1", - "field3" => "" ], ], $iterator); } + + /** + * @throws FileException + * @throws XmlUtilException + */ + public function testFromArray() + { + $array = [ + [ + "field1" => "value1", + "field2" => "value2", + "field3" => "value3", + "field4" => "value4", + ], + [ + "field1" => "1", + "field2" => "2", + "field4" => "4", + ], + ]; + + $anydataset = new AnyDataset($array); + + $iterator = $anydataset->getIterator()->toArray(); + $this->assertEquals($array, $iterator); + } + + /** + * @throws FileException + * @throws XmlUtilException + */ + public function testFromArray2() + { + $array = [ + [ + "field1" => "value1", + "field2" => "value2", + ], + new ModelPublic("value1", "value2"), + ]; + + $anydataset = new AnyDataset($array); + + $expected = [ + [ + "field1" => "value1", + "field2" => "value2", + ], + [ + "Id" => "value1", + "Name" => "value2", + ], + ]; + + $iterator = $anydataset->getIterator()->toArray(); + $this->assertEquals($expected, $iterator); + + $iterator = $anydataset->getIterator(); + $this->assertIsArray($iterator->moveNext()->entity()); + $this->assertInstanceOf(ModelPublic::class, $iterator->moveNext()->entity()); + $this->assertFalse($iterator->hasNext()); + } } diff --git a/tests/IteratorFilterAnydatasetTest.php b/tests/IteratorFilterAnydatasetTest.php index 57e72a2..67e7d33 100644 --- a/tests/IteratorFilterAnydatasetTest.php +++ b/tests/IteratorFilterAnydatasetTest.php @@ -1,7 +1,8 @@ object = new IteratorFilter(); $this->object->and('val', Relation::NOT_IN, [10, 30, 50]); $this->assertEquals([$row2], $this->object->match($collection)); + + // Test Group + $this->object = new IteratorFilter(); + $this->object->startGroup('id', Relation::EQUAL, 1); + $this->object->or('id', Relation::EQUAL, 3); + $this->object->endGroup(); + $this->assertEquals([$row1, $row3], $this->object->match($collection)); } + public function testGetIterator() + { + + $row1 = [ + 'id' => 1, + 'field' => 'value1', + 'field2' => 'value2', + 'val' => 50, + ]; + $row2 = [ + 'id' => 2, + 'field' => 'other1', + 'field2' => 'other2', + 'val' => 80, + ]; + $row3 = [ + 'id' => 3, + 'field' => 'last1', + 'field2' => 'last2', + 'val' => 30, + ]; + $row4 = [ + 'id' => 4, + 'field' => 'xy', + 'field2' => 'zy', + 'val' => 10, + ]; + + $anydataset = new AnyDataset(); + $anydataset->appendRow($row1); + $anydataset->appendRow($row2); + $anydataset->appendRow($row3); + $anydataset->appendRow($row4); + + $this->assertEquals([$row1, $row2, $row3, $row4], $anydataset->getIterator()->toArray()); + + $this->object->and('field2', Relation::EQUAL, 'other2'); + $this->assertEquals([$row2], $anydataset->getIterator($this->object)->toArray()); + + $this->object->or('field', Relation::EQUAL, 'last1'); + $this->assertEquals([$row2, $row3], $anydataset->getIterator($this->object)->toArray()); + + + //------------------------ + + $this->object = new IteratorFilter(); + $this->object->and('field', Relation::EQUAL, 'last1'); + $this->object->and('field2', Relation::EQUAL, 'last2'); + $this->assertEquals([$row3], $anydataset->getIterator($this->object)->toArray()); + + // Test Greater Than + $this->object = new IteratorFilter(); + $this->object->and('val', Relation::GREATER_THAN, 50); + $this->assertEquals([$row2], $anydataset->getIterator($this->object)->toArray()); + + // Test Less Than + $this->object = new IteratorFilter(); + $this->object->and('val', Relation::LESS_THAN, 50); + $this->assertEquals([$row3, $row4], $anydataset->getIterator($this->object)->toArray()); + + // Test Greater or Equal Than + $this->object = new IteratorFilter(); + $this->object->and('val', Relation::GREATER_OR_EQUAL_THAN, 50); + $this->assertEquals([$row1, $row2], $anydataset->getIterator($this->object)->toArray()); + + // Test Less or Equal Than + $this->object = new IteratorFilter(); + $this->object->and('val', Relation::LESS_OR_EQUAL_THAN, 50); + $this->assertEquals([$row1, $row3, $row4], $anydataset->getIterator($this->object)->toArray()); + + // Test Not Equal + $this->object = new IteratorFilter(); + $this->object->and('val', Relation::NOT_EQUAL, 50); + $this->assertEquals([$row2, $row3, $row4], $anydataset->getIterator($this->object)->toArray()); + + // Test Starts With + $this->object = new IteratorFilter(); + $this->object->and('field', Relation::STARTS_WITH, 'la'); + $this->assertEquals([$row3], $anydataset->getIterator($this->object)->toArray()); + + // Test Contains + $this->object = new IteratorFilter(); + $this->object->and('field', Relation::CONTAINS, '1'); + $this->assertEquals([$row1, $row2, $row3], $anydataset->getIterator($this->object)->toArray()); + + // Test In + $this->object = new IteratorFilter(); + $this->object->and('val', Relation::IN, [10, 30, 50]); + $this->assertEquals([$row1, $row3, $row4], $anydataset->getIterator($this->object)->toArray()); + + // Test Not In + $this->object = new IteratorFilter(); + $this->object->and('val', Relation::NOT_IN, [10, 30, 50]); + $this->assertEquals([$row2], $anydataset->getIterator($this->object)->toArray()); + + // Test Group + $this->object = new IteratorFilter(); + $this->object->startGroup('id', Relation::EQUAL, 1); + $this->object->or('id', Relation::EQUAL, 3); + $this->object->endGroup(); + $this->assertEquals([$row1, $row3], $anydataset->getIterator($this->object)->toArray()); + + + } } diff --git a/tests/IteratorFilterXPathTest.php b/tests/IteratorFilterXPathTest.php index 67d8014..9e1ed60 100644 --- a/tests/IteratorFilterXPathTest.php +++ b/tests/IteratorFilterXPathTest.php @@ -62,13 +62,12 @@ public function testAddRelationOr() public function testGroup() { - $this->object->startGroup(); - $this->object->and('field', Relation::EQUAL, 'test'); + $this->object->startGroup('field', Relation::EQUAL, 'test'); $this->object->and('field2', Relation::EQUAL, 'test2'); $this->object->endGroup(); $this->object->or('field3', Relation::EQUAL, 'test3'); $this->assertEquals( - "/anydataset/row[ ( field[@name='field'] = 'test' and field[@name='field2'] = 'test2' ) or field[@name='field3'] = 'test3' ]", + "/anydataset/row[(field[@name='field'] = 'test' and field[@name='field2'] = 'test2' ) or field[@name='field3'] = 'test3' ]", $this->object->format(new IteratorFilterXPathFormatter()) ); } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index efad06c..ad11a93 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -3,6 +3,7 @@ namespace Tests; use ByJG\AnyDataset\Core\AnyDataset; +use ByJG\AnyDataset\Core\Row; use ByJG\Serializer\Serialize; use PHPUnit\Framework\TestCase; use Tests\Sample\SampleModel; @@ -11,9 +12,9 @@ class ModelTest extends TestCase { public function testBindSingleRow() { - $sr = new \ByJG\AnyDataset\Core\Row(); - $sr->addField("id", 10); - $sr->addField("name", "Testing"); + $sr = new Row(); + $sr->set("id", 10); + $sr->set("name", "Testing"); $object = new SampleModel($sr->toArray()); @@ -25,9 +26,9 @@ public function testBindIterator() { $anydata = new AnyDataset(); - $sr = new \ByJG\AnyDataset\Core\Row(); - $sr->addField("id", 10); - $sr->addField("name", "Testing"); + $sr = new Row(); + $sr->set("id", 10); + $sr->set("name", "Testing"); $anydata->appendRow($sr); $object = new SampleModel($anydata->getIterator()->moveNext()->toArray()); diff --git a/tests/RowOutputTest.php b/tests/RowOutputTest.php index a03e397..b82fa55 100644 --- a/tests/RowOutputTest.php +++ b/tests/RowOutputTest.php @@ -3,9 +3,8 @@ namespace Tests; use ByJG\AnyDataset\Core\Row; -use ByJG\AnyDataset\Core\RowValidator; -use PHPUnit\Framework\TestCase; use ByJG\AnyDataset\Core\RowOutput; +use PHPUnit\Framework\TestCase; class RowOutputTest extends TestCase { diff --git a/tests/RowTest.php b/tests/RowTest.php index b7a605d..0461235 100644 --- a/tests/RowTest.php +++ b/tests/RowTest.php @@ -18,7 +18,7 @@ class RowTest extends TestCase /** * @var Row */ - protected $object; + protected Row $object; /** * Sets up the fixture, for example, opens a network connection. @@ -26,21 +26,20 @@ class RowTest extends TestCase */ protected function setUp(): void { - $this->object = new Row; + $this->object = new Row(); } protected function fill() { - $this->object->addField('field1', '10'); - $this->object->addField('field1', '20'); - $this->object->addField('field1', '30'); - $this->object->addField('field2', '40'); - $this->object->acceptChanges(); + $this->object->set('field1', '10', append: true); + $this->object->set('field1', '20', append: true); + $this->object->set('field1', '30', append: true); + $this->object->set('field2', '40'); } - public function testAddField() + public function testAppend() { - $this->object->addField('field1', '10'); + $this->object->set('field1', '10'); $this->assertEquals( array( 'field1' => 10 @@ -48,7 +47,7 @@ public function testAddField() $this->object->toArray() ); - $this->object->addField('field1', '20'); + $this->object->set('field1', '20', append: true); $this->assertEquals( array( 'field1' => array(10, 20) @@ -56,7 +55,7 @@ public function testAddField() $this->object->toArray() ); - $this->object->addField('field1', '30'); + $this->object->set('field1', '30', append: true); $this->assertEquals( array( 'field1' => array(10, 20, 30) @@ -64,7 +63,7 @@ public function testAddField() $this->object->toArray() ); - $this->object->addField('field2', '40'); + $this->object->set('field2', '40', append: true); $this->assertEquals( array( 'field1' => array(10, 20, 30), @@ -73,7 +72,7 @@ public function testAddField() $this->object->toArray() ); - $this->object->addField('field1', '20'); + $this->object->set('field1', '20', append: true); $this->assertEquals( array( 'field1' => array(10, 20, 30, 20), @@ -83,34 +82,24 @@ public function testAddField() ); } - public function testGetField() - { - $this->fill(); - - $this->assertEquals(10, $this->object->get('field1')); - $this->assertEquals(10, $this->object->get('field1')); // Test it again, because is an array - $this->assertEquals(40, $this->object->get('field2')); - $this->assertEquals(null, $this->object->get('not-exists')); - } - public function testGetFieldArray() { $this->fill(); - $this->assertEquals(array(10, 20, 30), $this->object->getAsArray('field1')); - $this->assertEquals(array(40), $this->object->getAsArray('field2')); + $this->assertEquals(array(10, 20, 30), $this->object->get('field1')); + $this->assertEquals(40, $this->object->get('field2')); - $this->object->addField('field3', ''); - $this->object->acceptChanges(); + $this->object->set('field3', ''); - $this->assertEquals(array(), $this->object->getAsArray('field3')); + $this->assertEquals('', $this->object->get('field3')); + $this->assertNull($this->object->get('field4')); } public function testGetFieldNames() { $this->fill(); - $this->assertEquals(array('field1', 'field2'), $this->object->getFieldNames()); + $this->assertEquals(array('field1', 'field2'), array_keys($this->object->toArray())); } public function testSetField() @@ -133,7 +122,7 @@ public function testRemoveFieldName() $this->assertEquals(["field1" => [10, 20, 30], "field2" => 40], $this->object->toArray()); - $this->object->removeField('field1'); + $this->object->unset('field1'); $this->assertEquals(null, $this->object->get('field1')); $this->assertEquals(40, $this->object->get('field2')); @@ -146,8 +135,8 @@ public function testRemoveFieldName2() $this->assertEquals(["field1" => [10, 20, 30], "field2" => 40], $this->object->toArray()); - $this->object->removeField('field2'); - $this->assertEquals(10, $this->object->get('field1')); + $this->object->unset('field2'); + $this->assertEquals([10, 20, 30], $this->object->get('field1')); $this->assertEquals(null, $this->object->get('field2')); $this->assertEquals(["field1" => [10, 20, 30]], $this->object->toArray()); @@ -157,13 +146,13 @@ public function testRemoveFieldNameValue() { $this->fill(); - $this->object->removeValue('field1', 20); - $this->assertEquals(array(10, 30), $this->object->getAsArray('field1')); + $this->object->unset('field1', 20); + $this->assertEquals(array(10, 30), $this->object->get('field1')); - $this->object->removeValue('field2', 100); + $this->object->unset('field2', 100); $this->assertEquals(40, $this->object->get('field2')); // Element was not removed - $this->object->removeValue('field2', 40); + $this->object->unset('field2', 40); $this->assertEquals(null, $this->object->get('field2')); } @@ -171,17 +160,17 @@ public function testSetFieldValue() { $this->fill(); - $this->object->replaceValue('field2', 100, 200); + $this->object->replace('field2', 100, 200); $this->assertEquals(40, $this->object->get('field2')); // Element was not changed - $this->object->replaceValue('field2', 40, 200); + $this->object->replace('field2', 40, 200); $this->assertEquals(200, $this->object->get('field2')); - $this->object->replaceValue('field1', 500, 190); - $this->assertEquals(array(10, 20, 30), $this->object->getAsArray('field1')); // Element was not changed + $this->object->replace('field1', 500, 190); + $this->assertEquals(array(10, 20, 30), $this->object->get('field1')); // Element was not changed - $this->object->replaceValue('field1', 20, 190); - $this->assertEquals(array(10, 190, 30), $this->object->getAsArray('field1')); + $this->object->replace('field1', 20, 190); + $this->assertEquals(array(10, 190, 30), $this->object->get('field1')); } public function testGetDomObject() @@ -223,42 +212,11 @@ public function testGetOriginalRawFormat() $this->object->set('field2', 150); $this->assertEquals( - array('field1' => array(10, 20, 30), 'field2' => 40), - $this->object->getAsRaw() + array('field1' => array(10, 20, 30), 'field2' => 150), + $this->object->entity() ); } - public function testHasChanges() - { - $this->fill(); - - $this->assertFalse($this->object->hasChanges()); - $this->object->set('field2', 150); - $this->assertTrue($this->object->hasChanges()); - } - - public function testAcceptChanges() - { - $this->fill(); - - $this->object->set('field2', 150); - $this->assertEquals(array('field1' => array(10, 20, 30), 'field2' => 40), $this->object->getAsRaw()); - $this->object->acceptChanges(); - $this->assertEquals(array('field1' => array(10, 20, 30), 'field2' => 150), $this->object->getAsRaw()); - } - - public function testRejectChanges() - { - $this->fill(); - - $this->object->set('field2', 150); - $this->assertEquals(array('field1' => array(10, 20, 30), 'field2' => 150), $this->object->toArray()); - $this->assertEquals(150, $this->object->get('field2')); - $this->object->rejectChanges(); - $this->assertEquals(array('field1' => array(10, 20, 30), 'field2' => 40), $this->object->toArray()); - $this->assertEquals(40, $this->object->get('field2')); - } - public function testConstructor_ModelPublic() { $model = new ModelPublic(10, 'Testing'); @@ -268,6 +226,14 @@ public function testConstructor_ModelPublic() $this->assertEquals(10, $sr->get("Id")); $this->assertEquals("Testing", $sr->get("Name")); $this->assertEquals(['Id' => 10, 'Name' => 'Testing'], $sr->toArray()); + + $sr->set("Id", 20); + $sr->set("Name", "New Name"); + + $this->assertEquals(20, $sr->get("Id")); + $this->assertEquals("New Name", $sr->get("Name")); + $this->assertEquals(['Id' => 20, 'Name' => 'New Name'], $sr->toArray()); + $this->assertEquals(new ModelPublic(20, "New Name"), $sr->entity()); } public function testConstructor_ModelGetter() @@ -279,6 +245,14 @@ public function testConstructor_ModelGetter() $this->assertEquals(10, $sr->get("Id")); $this->assertEquals("Testing", $sr->get("Name")); $this->assertEquals(['Id' => 10, 'Name' => 'Testing'], $sr->toArray()); + + $sr->set("Id", 20); + $sr->set("Name", "New Name"); + + $this->assertEquals(20, $sr->get("Id")); + $this->assertEquals("New Name", $sr->get("Name")); + $this->assertEquals(['Id' => 20, 'Name' => 'New Name'], $sr->toArray()); + $this->assertEquals(new ModelGetter(20, "New Name"), $sr->entity()); } public function testConstructor_stdClass() @@ -292,6 +266,14 @@ public function testConstructor_stdClass() $this->assertEquals(10, $sr->get("Id")); $this->assertEquals("Testing", $sr->get("Name")); $this->assertEquals(['Id' => 10, 'Name' => 'Testing'], $sr->toArray()); + + $sr->set("Id", 20); + $sr->set("Name", "New Name"); + + $this->assertEquals(20, $sr->get("Id")); + $this->assertEquals("New Name", $sr->get("Name")); + $this->assertEquals(['Id' => 20, 'Name' => 'New Name'], $sr->toArray()); + $this->assertEquals((object) ['Id' => 20, 'Name' => 'New Name'], $sr->entity()); } public function testConstructor_Array() @@ -318,55 +300,19 @@ public function testConstructor_PropertyPattern() // Because this, the field is Id_Model instead IdModel $this->assertEquals(10, $sr->get("IdModel")); $this->assertEquals("Testing", $sr->get("ClientName")); - } - - public function testCaseSensitive_1() - { - $row = new Row([ - "fieldA" => "test", - "fieldB" => "new test" - ]); - - $this->assertTrue($row->isFieldNameCaseSensitive()); - - $this->assertEquals("test", $row->get("fieldA")); - $this->assertEquals("new test", $row->get("fieldB")); - - $this->assertNull($row->get("fielda")); - $this->assertNull($row->get("fieldb")); - - $row->enableFieldNameCaseInSensitive(); - - $this->assertFalse($row->isFieldNameCaseSensitive()); - - $this->assertEquals("test", $row->get("fielda")); - $this->assertEquals("new test", $row->get("FiEldb")); - } - - public function testCaseSensitive_2() - { - $row = new Row([ - "fieldA" => "test", - "fieldB" => "new test" - ]); - - $row->set("FIELDA", "a"); - $this->assertEquals("test", $row->get("fieldA")); - $this->assertEquals("a", $row->get("FIELDA")); + $this->assertEquals(['IdModel' => 10, 'ClientName' => 'Testing'], $sr->toArray()); - $row->enableFieldNameCaseInSensitive(); - // When enable case insentive, the last field name overwrite the value - $this->assertEquals("a", $row->get("FieLda")); - - $row->set("FIELDB", "new value"); - $this->assertEquals("new value", $row->get("FieLdB")); + $sr->set("IdModel", 20); + $sr->set("ClientName", "New Name"); - $this->assertFalse($row->fieldExists("DelEteME")); - $row->addField("DELETEME", "true"); - $this->assertTrue($row->fieldExists("DelEteME")); + $this->assertEquals(20, $sr->get("IdModel")); + $this->assertEquals("New Name", $sr->get("ClientName")); + $this->assertEquals(['IdModel' => 20, 'ClientName' => 'New Name'], $sr->toArray()); - $row->removeField("dELeTEme"); - $this->assertFalse($row->fieldExists("DelEteME")); + $expected = new ModelPropertyPattern(); + $expected->setIdModel(20); + $expected->setClientName("New Name"); + $this->assertEquals($expected, $sr->entity()); } public function testToArrayFields() diff --git a/tests/RowValidatorTest.php b/tests/RowValidatorTest.php index 4925a18..64d0f0b 100644 --- a/tests/RowValidatorTest.php +++ b/tests/RowValidatorTest.php @@ -12,8 +12,8 @@ class RowValidatorTest extends TestCase /** * @var Row */ - protected $row1; - protected $row2; + protected Row $row1; + protected Row $row2; /** * Sets up the fixture, for example, opens a network connection. @@ -68,6 +68,7 @@ public function testCustom() if ($value != 10) { return "Value should be 10, but $value was found."; } + return null; }); $this->assertSame([], $validator->validate($this->row1)); diff --git a/tests/Sample/ModelPropertyPattern.php b/tests/Sample/ModelPropertyPattern.php index 2708c9e..6536624 100644 --- a/tests/Sample/ModelPropertyPattern.php +++ b/tests/Sample/ModelPropertyPattern.php @@ -2,10 +2,12 @@ namespace Tests\Sample; +use ByJG\Serializer\BaseModel; + /** * @Xmlnuke:NodeName ModelPropertyPattern */ -class ModelPropertyPattern extends \ByJG\Serializer\BaseModel +class ModelPropertyPattern extends BaseModel { protected $_Id_Model = ""; From 6462167c96c5113a74cf9643383c4af215dad7a4 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 11 Dec 2024 10:05:54 -0600 Subject: [PATCH 02/14] Major Change: Make the Row object thin. Added RowInterface. --- src/IteratorFilter.php | 100 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 11 deletions(-) diff --git a/src/IteratorFilter.php b/src/IteratorFilter.php index 0388df7..5278586 100644 --- a/src/IteratorFilter.php +++ b/src/IteratorFilter.php @@ -34,21 +34,13 @@ public static function getInstance(): IteratorFilter */ public function match(array $array): array { - $returnArray = []; - $filterList = $this->format(new IteratorFilterFormatter()); - - if (empty($filterList)) { + if (count($this->filters) === 0) { return $array; } + $returnArray = []; foreach ($array as $sr) { - $rowArr = $sr->toArray(); - $rowEval = []; - foreach ($rowArr as $key => $value) { - $rowEval["%$key"] = is_numeric($value) ? $value : "'$value'"; - } - - $result = eval("return " . strtr($filterList, $rowEval) . ";"); + $result = $this->evaluateFilter($sr, $this->filters); if ($result) { $returnArray[] = $sr; } @@ -57,6 +49,92 @@ public function match(array $array): array return $returnArray; } + protected function evaluateFilter(RowInterface $row, array $filterList): bool + { + $result = true; + $position = 0; + $subList = []; + foreach ($filterList as $filter) { + $operator = $filter[0]; + $field = $filter[1]; + $relation = $filter[2]; + $value = $filter[3]; + + + if ($operator == ")") { + $result = $this->evaluateFilter($row, $subList); + $subList = []; + continue; + } elseif ($operator == "(") { + $filter[0] = " and "; + $subList[] = $filter; + continue; + } elseif (count($subList) > 0) { + $subList[] = $filter; + continue; + } + + switch ($relation) { + case Relation::EQUAL: + $localEval = $row->get($field) == $value; + break; + + case Relation::GREATER_THAN: + $localEval = $row->get($field) > $value; + break; + + case Relation::LESS_THAN: + $localEval = $row->get($field) < $value; + break; + + case Relation::GREATER_OR_EQUAL_THAN: + $localEval = $row->get($field) >= $value; + break; + + case Relation::LESS_OR_EQUAL_THAN: + $localEval = $row->get($field) <= $value; + break; + + case Relation::NOT_EQUAL: + $localEval = $row->get($field) != $value; + break; + + case Relation::STARTS_WITH: + $localEval = str_starts_with($row->get($field), $value); + break; + + case Relation::IN: + $localEval = in_array($row->get($field), $value); + break; + + case Relation::NOT_IN: + $localEval = !in_array($row->get($field), $value); + break; + + default: // Relation::CONTAINS: + $localEval = str_contains($row->get($field), $value); + break; + } + + if ($position == 0) { + $result = $localEval; + } elseif ($operator == " and ") { + $result = $result && $localEval; + if (!$result) { + break; + } + } elseif ($operator == " or ") { + $result = $result || $localEval; + } else { + throw new \InvalidArgumentException("Invalid operator: $operator"); + } + + $position++; + } + + return $result; + } + /** * Get the filter * From 68eb9c94ef7dd08845f49680e8b82595e621ad9e Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 11 Dec 2024 10:21:08 -0600 Subject: [PATCH 03/14] Major Change: Make the Row object thin. Added RowInterface. --- src/IteratorFilter.php | 11 +++++-- tests/IteratorFilterAnydatasetTest.php | 42 ++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/IteratorFilter.php b/src/IteratorFilter.php index 5278586..c1de912 100644 --- a/src/IteratorFilter.php +++ b/src/IteratorFilter.php @@ -49,7 +49,7 @@ public function match(array $array): array return $returnArray; } - protected function evaluateFilter(RowInterface $row, array $filterList): bool + protected function evaluateFilter(RowInterface $row, array $filterList, ?string $previousOperator = null): bool { $result = true; $position = 0; @@ -62,11 +62,15 @@ protected function evaluateFilter(RowInterface $row, array $filterList): bool if ($operator == ")") { - $result = $this->evaluateFilter($row, $subList); + $result = $this->evaluateFilter($row, $subList, $previousOperator); $subList = []; continue; } elseif ($operator == "(") { - $filter[0] = " and "; + $filter[0] = $previousOperator ?? " and "; + $previousOperator = $filter[0]; + if ($previousOperator == " and " && $result === false) { + return false; + } $subList[] = $filter; continue; } elseif (count($subList) > 0) { @@ -129,6 +133,7 @@ protected function evaluateFilter(RowInterface $row, array $filterList): bool throw new \InvalidArgumentException("Invalid operator: $operator"); } + $previousOperator = $operator; $position++; } diff --git a/tests/IteratorFilterAnydatasetTest.php b/tests/IteratorFilterAnydatasetTest.php index 67e7d33..caae261 100644 --- a/tests/IteratorFilterAnydatasetTest.php +++ b/tests/IteratorFilterAnydatasetTest.php @@ -28,7 +28,7 @@ protected function setUp(): void public function testMatch() { $collection = [ - $row1 = new Row( + $row1 = Row::factory( [ 'id' => 1, 'field' => 'value1', @@ -36,7 +36,7 @@ public function testMatch() 'val' => 50, ] ), - $row2 = new Row( + $row2 = Row::factory( [ 'id' => 2, 'field' => 'other1', @@ -44,7 +44,7 @@ public function testMatch() 'val' => 80, ] ), - $row3 = new Row( + $row3 = Row::factory( [ 'id' => 3, 'field' => 'last1', @@ -52,7 +52,7 @@ public function testMatch() 'val' => 30, ] ), - $row4 = new Row( + $row4 = Row::factory( [ 'id' => 4, 'field' => 'xy', @@ -123,12 +123,28 @@ public function testMatch() $this->object->and('val', Relation::NOT_IN, [10, 30, 50]); $this->assertEquals([$row2], $this->object->match($collection)); - // Test Group + // Test Group 1 $this->object = new IteratorFilter(); $this->object->startGroup('id', Relation::EQUAL, 1); $this->object->or('id', Relation::EQUAL, 3); $this->object->endGroup(); $this->assertEquals([$row1, $row3], $this->object->match($collection)); + + // Test Group 2 + $this->object = new IteratorFilter(); + $this->object->startGroup('id', Relation::EQUAL, 1); + $this->object->or('id', Relation::EQUAL, 3); + $this->object->endGroup(); + $this->object->and('field2', Relation::EQUAL, 'last2'); + $this->assertEquals([$row3], $this->object->match($collection)); + + // Test Group 3 + $this->object = new IteratorFilter(); + $this->object->and('field2', Relation::EQUAL, 'last2'); + $this->object->startGroup('id', Relation::EQUAL, 1); + $this->object->or('id', Relation::EQUAL, 3); + $this->object->endGroup(); + $this->assertEquals([$row3], $this->object->match($collection)); } public function testGetIterator() @@ -226,14 +242,28 @@ public function testGetIterator() $this->object->and('val', Relation::NOT_IN, [10, 30, 50]); $this->assertEquals([$row2], $anydataset->getIterator($this->object)->toArray()); - // Test Group + // Test Group 1 $this->object = new IteratorFilter(); $this->object->startGroup('id', Relation::EQUAL, 1); $this->object->or('id', Relation::EQUAL, 3); $this->object->endGroup(); $this->assertEquals([$row1, $row3], $anydataset->getIterator($this->object)->toArray()); + // Test Group 2 + $this->object = new IteratorFilter(); + $this->object->startGroup('id', Relation::EQUAL, 1); + $this->object->or('id', Relation::EQUAL, 3); + $this->object->endGroup(); + $this->object->and('field2', Relation::EQUAL, 'last2'); + $this->assertEquals([$row3], $anydataset->getIterator($this->object)->toArray()); + // Test Group 3 + $this->object = new IteratorFilter(); + $this->object->and('field2', Relation::EQUAL, 'last2'); + $this->object->startGroup('id', Relation::EQUAL, 1); + $this->object->or('id', Relation::EQUAL, 3); + $this->object->endGroup(); + $this->assertEquals([$row3], $anydataset->getIterator($this->object)->toArray()); } } From d7a364a737e84babb918f869ab479910d25d1e09 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 11 Dec 2024 10:52:05 -0600 Subject: [PATCH 04/14] Major Change: Make the Row object thin. Added RowInterface. --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index b02dfdc..6343dde 100644 --- a/composer.json +++ b/composer.json @@ -16,14 +16,14 @@ "require": { "php": ">=8.1 <8.5", "ext-dom": "*", - "byjg/xmlutil": "^5.0", - "byjg/serializer": "^5.0" + "byjg/xmlutil": "^5.1", + "byjg/serializer": "^5.1" }, "suggest": { }, "require-dev": { - "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^4.24" + "phpunit/phpunit": "^9.6|^10.5|^11.5", + "vimeo/psalm": "^6.0" }, "provide": { "byjg/anydataset-implementation": "1.0" From fcdbd8560d6716f265a05d348f33d481a28cd763 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 11 Dec 2024 11:19:31 -0600 Subject: [PATCH 05/14] Major Change: Make the Row object thin. Added RowInterface. --- psalm.xml.dist => psalm.xml | 5 ++++- src/AnyDataset.php | 6 +++--- src/AnyIterator.php | 9 --------- src/Formatter/BaseFormatter.php | 2 +- src/Formatter/FormatterInterface.php | 4 ++-- src/Formatter/JsonFormatter.php | 5 +---- src/Formatter/XmlFormatter.php | 2 +- src/GenericIterator.php | 3 +++ tests/AnyDatasetTest.php | 2 +- tests/ModelTest.php | 2 +- 10 files changed, 17 insertions(+), 23 deletions(-) rename psalm.xml.dist => psalm.xml (80%) diff --git a/psalm.xml.dist b/psalm.xml similarity index 80% rename from psalm.xml.dist rename to psalm.xml index 3e4e3d0..ebabb1a 100644 --- a/psalm.xml.dist +++ b/psalm.xml @@ -2,14 +2,17 @@ + - + \ No newline at end of file diff --git a/src/AnyDataset.php b/src/AnyDataset.php index 91bec10..a4e7419 100644 --- a/src/AnyDataset.php +++ b/src/AnyDataset.php @@ -146,9 +146,9 @@ private function createFromFile(): void /** * Returns the AnyDataset XML representative structure. * - * @return string XML String + * @return string|false XML String */ - public function xml(): string + public function xml(): string|false { return (new XmlFormatter($this->getIterator()))->toText(); } @@ -261,7 +261,7 @@ public function removeRow(int|RowInterface $row = null): void * Add a single string field to an existing row * * @param string $name - Field name - * @param string $value - Field value + * @param mixed $value - Field value * @return void */ public function addField(string $name, mixed $value): void diff --git a/src/AnyIterator.php b/src/AnyIterator.php index 957898f..cddad94 100644 --- a/src/AnyIterator.php +++ b/src/AnyIterator.php @@ -77,13 +77,4 @@ public function current(): mixed { return $this->list[$this->curRow] ?? null; } - - /** - * @param IteratorFilter $filter - * @return AnyIterator - */ - public function withFilter(IteratorFilter $filter): AnyIterator - { - return new AnyIterator($filter->match($this->list)); - } } diff --git a/src/Formatter/BaseFormatter.php b/src/Formatter/BaseFormatter.php index 88b5376..7fcaada 100644 --- a/src/Formatter/BaseFormatter.php +++ b/src/Formatter/BaseFormatter.php @@ -21,7 +21,7 @@ abstract public function raw(): mixed; /** * @inheritDoc */ - abstract public function toText(): string; + abstract public function toText(): string|false; /** * @inheritDoc diff --git a/src/Formatter/FormatterInterface.php b/src/Formatter/FormatterInterface.php index 6ff6994..1f765cb 100644 --- a/src/Formatter/FormatterInterface.php +++ b/src/Formatter/FormatterInterface.php @@ -14,9 +14,9 @@ public function raw(): mixed; /** * Return the object transformed to string. * - * @return string + * @return string|false */ - public function toText(): string; + public function toText(): string|false; /** * Save the contents to a file diff --git a/src/Formatter/JsonFormatter.php b/src/Formatter/JsonFormatter.php index b74fd9b..9368896 100644 --- a/src/Formatter/JsonFormatter.php +++ b/src/Formatter/JsonFormatter.php @@ -3,13 +3,11 @@ namespace ByJG\AnyDataset\Core\Formatter; use ByJG\AnyDataset\Core\GenericIterator; -use ByJG\Serializer\Exception\InvalidArgumentException; class JsonFormatter extends BaseFormatter { /** * @inheritDoc - * @throws InvalidArgumentException */ public function raw(): mixed { @@ -18,9 +16,8 @@ public function raw(): mixed /** * @inheritDoc - * @throws InvalidArgumentException */ - public function toText(): string + public function toText(): string|false { if ($this->object instanceof GenericIterator) { return json_encode($this->object->toArray()); diff --git a/src/Formatter/XmlFormatter.php b/src/Formatter/XmlFormatter.php index d552a19..f529de1 100644 --- a/src/Formatter/XmlFormatter.php +++ b/src/Formatter/XmlFormatter.php @@ -59,7 +59,7 @@ public function raw(): mixed /** * @inheritDoc */ - public function toText(): string + public function toText(): string|false { return $this->raw()->saveXML(); } diff --git a/src/GenericIterator.php b/src/GenericIterator.php index 5bcd4eb..534bbbe 100644 --- a/src/GenericIterator.php +++ b/src/GenericIterator.php @@ -5,6 +5,9 @@ use Iterator; use ReturnTypeWillChange; +/** + * @psalm-suppress MissingTemplateParam + */ abstract class GenericIterator implements IteratorInterface, Iterator { diff --git a/tests/AnyDatasetTest.php b/tests/AnyDatasetTest.php index b8a3ddd..08ec908 100644 --- a/tests/AnyDatasetTest.php +++ b/tests/AnyDatasetTest.php @@ -327,7 +327,7 @@ public function testIteratorFilter() $this->assertEquals([ ['name' => 'jf', 'age' => 15], ['name' => 'jg jr', 'age' => 4] - ], $this->object->getIterator()->withFilter($filter)->toArray()); + ], $this->object->getIterator($filter)->toArray()); } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index ad11a93..dfc603e 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -47,7 +47,7 @@ public function testBind_Iterator2() $anydata->addField('Name', 'Gilberto'); $object1 = new SampleModel(); - $object1->copyFrom( $anydata->getIterator()->moveNext()->toArray() ); + $object1->copyFrom($anydata->getIterator()->moveNext()->toArray()); $this->assertEquals(10, $object1->Id); $this->assertEquals('Joao', $object1->getName()); } From 0c3b9be6ed0e019fd83d184bb7bafd2afcce8f6d Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 11 Dec 2024 11:50:29 -0600 Subject: [PATCH 06/14] Major Change: Make the Row object thin. Added RowInterface. --- composer.json | 2 +- phpunit.xml.dist | 20 +++++++++++--------- src/AnyDataset.php | 6 +++--- src/Formatter/XmlFormatter.php | 2 +- src/IteratorFilter.php | 2 +- src/IteratorFilterFormatter.php | 2 +- src/IteratorFilterXPathFormatter.php | 2 +- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 6343dde..9517f39 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "suggest": { }, "require-dev": { - "phpunit/phpunit": "^9.6|^10.5|^11.5", + "phpunit/phpunit": "^10.5|^11.5", "vimeo/psalm": "^6.0" }, "provide": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fc18251..584610f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,11 +7,13 @@ and open the template in the editor. @@ -20,15 +22,15 @@ and open the template in the editor. - + - ./src + ./src/ - + - ./tests + ./tests/ diff --git a/src/AnyDataset.php b/src/AnyDataset.php index a4e7419..3eb1092 100644 --- a/src/AnyDataset.php +++ b/src/AnyDataset.php @@ -232,7 +232,7 @@ public function insertRowBefore(int $rowNumber, array|object $row): void * @return void * @throws \ByJG\Serializer\Exception\InvalidArgumentException */ - public function removeRow(int|RowInterface $row = null): void + public function removeRow(int|RowInterface|null $row = null): void { if (is_null($row)) { $row = $this->currentRow; @@ -277,7 +277,7 @@ public function addField(string $name, mixed $value): void * @param IteratorFilter|null $itf * @return GenericIterator|AnyIterator */ - public function getIterator(IteratorFilter $itf = null): GenericIterator|AnyIterator + public function getIterator(?IteratorFilter $itf = null): GenericIterator|AnyIterator { if (is_null($itf)) { return new AnyIterator($this->collection); @@ -293,7 +293,7 @@ public function getIterator(IteratorFilter $itf = null): GenericIterator|AnyIter * @param IteratorFilter|null $itf * @return array */ - public function getArray(string $fieldName, IteratorFilter $itf = null): array + public function getArray(string $fieldName, ?IteratorFilter $itf = null): array { $iterator = $this->getIterator($itf); $result = array(); diff --git a/src/Formatter/XmlFormatter.php b/src/Formatter/XmlFormatter.php index f529de1..3996173 100644 --- a/src/Formatter/XmlFormatter.php +++ b/src/Formatter/XmlFormatter.php @@ -28,7 +28,7 @@ protected function anydatasetXml(array $collection): XmlNode * @param array $row * @return XmlNode */ - protected function rowXml(array $row, XmlDocument $parentDocument = null): XmlNode + protected function rowXml(array $row, ?XmlDocument $parentDocument = null): XmlNode { if (!empty($parentDocument)) { $node = $parentDocument->appendChild('row'); diff --git a/src/IteratorFilter.php b/src/IteratorFilter.php index c1de912..6e7e627 100644 --- a/src/IteratorFilter.php +++ b/src/IteratorFilter.php @@ -149,7 +149,7 @@ protected function evaluateFilter(RowInterface $row, array $filterList, ?string * @param string $returnFields * @return string */ - public function format(IteratorFilterFormatter $formatter, string $tableName = null, array &$params = [], string $returnFields = "*"): string + public function format(IteratorFilterFormatter $formatter, ?string $tableName = null, array &$params = [], string $returnFields = "*"): string { return $formatter->format($this->filters, $tableName, $params, $returnFields); } diff --git a/src/IteratorFilterFormatter.php b/src/IteratorFilterFormatter.php index 0ffb1cb..d3e3073 100644 --- a/src/IteratorFilterFormatter.php +++ b/src/IteratorFilterFormatter.php @@ -81,7 +81,7 @@ public function getRelation(string $name, Relation $relation, mixed $value, arra * @param string $returnFields * @return string */ - public function format(array $filters, string $tableName = null, array &$params = [], string $returnFields = "*"): string + public function format(array $filters, ?string $tableName = null, array &$params = [], string $returnFields = "*"): string { return $this->getFilter($filters, $params); } diff --git a/src/IteratorFilterXPathFormatter.php b/src/IteratorFilterXPathFormatter.php index aaf77f0..0713fba 100644 --- a/src/IteratorFilterXPathFormatter.php +++ b/src/IteratorFilterXPathFormatter.php @@ -9,7 +9,7 @@ class IteratorFilterXPathFormatter extends IteratorFilterFormatter /** * @inheritDoc */ - public function format(array $filters, string $tableName = null, array &$params = [], string $returnFields = "*"): string + public function format(array $filters, ?string $tableName = null, array &$params = [], string $returnFields = "*"): string { $param = []; $xpathFilter = $this->getFilter($filters, $param); From c909242cffeb59a95bf821835c531b70afa50786 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 11 Dec 2024 16:34:30 -0600 Subject: [PATCH 07/14] Remove `count()`; --- src/AnyIterator.php | 10 +--------- src/GenericIterator.php | 5 ----- src/IteratorInterface.php | 7 ------- tests/AnyDatasetTest.php | 29 ++++++++++++++++------------- 4 files changed, 17 insertions(+), 34 deletions(-) diff --git a/src/AnyIterator.php b/src/AnyIterator.php index cddad94..921f53e 100644 --- a/src/AnyIterator.php +++ b/src/AnyIterator.php @@ -33,20 +33,12 @@ public function __construct(array $list) $this->list = $list; } - /** - * @inheritDoc - */ - public function count(): int - { - return count($this->list); - } - /** * @inheritDoc */ public function hasNext(): bool { - return ($this->curRow < $this->count()); + return ($this->curRow < count($this->list)); } /** diff --git a/src/GenericIterator.php b/src/GenericIterator.php index 534bbbe..0ee9131 100644 --- a/src/GenericIterator.php +++ b/src/GenericIterator.php @@ -21,11 +21,6 @@ abstract public function hasNext(): bool; */ abstract public function moveNext(): RowInterface|null; - /** - * @inheritDoc - */ - abstract public function count(): int; - /** * @inheritDoc * @param array $fields diff --git a/src/IteratorInterface.php b/src/IteratorInterface.php index 327455f..e62e2a9 100644 --- a/src/IteratorInterface.php +++ b/src/IteratorInterface.php @@ -19,13 +19,6 @@ public function hasNext(): bool; */ public function moveNext(): RowInterface|null; - /** - * Get the record count. Some implementations may have return -1. - * - * @return int - */ - public function count(): int; - /** * Get an array of the iterator * diff --git a/tests/AnyDatasetTest.php b/tests/AnyDatasetTest.php index 08ec908..d1d015e 100644 --- a/tests/AnyDatasetTest.php +++ b/tests/AnyDatasetTest.php @@ -164,16 +164,16 @@ public function testSave() public function testAppendRow() { - $qtd = $this->object->getIterator()->count(); - $this->assertEquals(0, $qtd); + $qtd = $this->object->getIterator(); + $this->assertEquals([], $qtd->toArray()); $this->object->appendRow(); - $qtd = $this->object->getIterator()->count(); - $this->assertEquals(1, $qtd); + $qtd = $this->object->getIterator(); + $this->assertEquals([[]], $qtd->toArray()); $this->object->appendRow(); - $qtd = $this->object->getIterator()->count(); - $this->assertEquals(2, $qtd); + $qtd = $this->object->getIterator(); + $this->assertEquals([[], []], $qtd->toArray()); } public function testImport() @@ -256,20 +256,23 @@ public function testRemoveRow_1() public function testAddField() { - $qtd = $this->object->getIterator()->count(); - $this->assertEquals(0, $qtd); + $qtd = $this->object->getIterator(); + $this->assertEquals([], $qtd->toArray()); $this->object->appendRow(); - $qtd = $this->object->getIterator()->count(); - $this->assertEquals(1, $qtd); + $qtd = $this->object->getIterator(); + $this->assertEquals([[]], $qtd->toArray()); $this->object->addField('newfield', 'value'); - $this->assertEquals([ + $this->assertEquals( [ - "newfield" => "value", + [ + "newfield" => "value", + ], ], - ], $this->object->getIterator()->toArray()); + $this->object->getIterator()->toArray() + ); } public function testGetArray() From a8bcb00a7b2039cea334111842d63c00b92b913a Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Fri, 13 Dec 2024 11:56:46 -0600 Subject: [PATCH 08/14] Fix hasNext() and moveNext() --- README.md | 20 ++++++++++++++++---- docs/row.md | 5 +++-- src/AnyIterator.php | 27 ++++++++++----------------- src/GenericIterator.php | 29 ++++++++++++----------------- tests/AnyDatasetTest.php | 40 ++++++++++++++++++++++++++++++++++++++++ tests/ModelTest.php | 4 ++-- 6 files changed, 83 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index b7a8acc..bfca453 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,20 @@ foreach ($iterator as $row) { **while** ```php -while ($iterator->hasNext()) { - $row = $iterator->moveNext(); +// This one uses the PHP original implementation +while ($iterator->valid()) { + $row = $iterator->current(); print $row->toArray(); + $iterator->next(); +} +``` + +**while (implementation 2)** + +```php +while ($iterator->hasNext()) { + // This returns the current row and move the pointer to the next + print $iterator->moveNext()->toArray(); } ``` @@ -102,10 +113,11 @@ print_r($iterator->toArray()); ```php getIterator(); -while ($iterator->hasNext()) { - $row = $iterator->moveNext(); +while ($iterator->valid()) { + $row = $iterator->current(); print_r($row->get("field1")); + $iterator->next(); } ``` diff --git a/docs/row.md b/docs/row.md index 745ad28..6ffbf13 100644 --- a/docs/row.md +++ b/docs/row.md @@ -33,10 +33,11 @@ The `Row` object implements the following methods: $dataset = new AnyDataset($data); $iterator = $dataset->getIterator(); -while ($iterator->hasNext()) { - $row = $iterator->moveNext(); +while ($iterator->valid()) { + $row = $iterator->current(); echo $row->get("field1"); + $iterator->next(); } ``` diff --git a/src/AnyIterator.php b/src/AnyIterator.php index 921f53e..92fd4b7 100644 --- a/src/AnyIterator.php +++ b/src/AnyIterator.php @@ -36,37 +36,30 @@ public function __construct(array $list) /** * @inheritDoc */ - public function hasNext(): bool + #[ReturnTypeWillChange] + public function key(): mixed { - return ($this->curRow < count($this->list)); + return $this->curRow; } /** * @inheritDoc */ - public function moveNext(): RowInterface|null + #[ReturnTypeWillChange] + public function current(): mixed { - if (!$this->hasNext()) { - return null; - } - return $this->list[$this->curRow++]; + return $this->list[$this->curRow] ?? null; } - /** - * @inheritDoc - */ #[ReturnTypeWillChange] - public function key(): mixed + public function next(): void { - return $this->curRow; + $this->curRow++; } - /** - * @inheritDoc - */ #[ReturnTypeWillChange] - public function current(): mixed + public function valid(): bool { - return $this->list[$this->curRow] ?? null; + return ($this->curRow < count($this->list)); } } diff --git a/src/GenericIterator.php b/src/GenericIterator.php index 0ee9131..789ad4c 100644 --- a/src/GenericIterator.php +++ b/src/GenericIterator.php @@ -10,16 +10,17 @@ */ abstract class GenericIterator implements IteratorInterface, Iterator { + public function hasNext(): bool + { + return $this->valid(); + } - /** - * @inheritDoc - */ - abstract public function hasNext(): bool; - - /** - * @inheritDoc - */ - abstract public function moveNext(): RowInterface|null; + public function moveNext(): RowInterface|null + { + $row = $this->current(); + $this->next(); + return $row; + } /** * @inheritDoc @@ -66,17 +67,11 @@ public function rewind(): void * @inheritDoc */ #[ReturnTypeWillChange] - public function next(): void - { - $this->moveNext(); - } + abstract public function next(): void; /** * @inheritDoc */ #[ReturnTypeWillChange] - public function valid(): bool - { - return $this->hasNext(); - } + abstract public function valid(): bool; } diff --git a/tests/AnyDatasetTest.php b/tests/AnyDatasetTest.php index d1d015e..8fe3767 100644 --- a/tests/AnyDatasetTest.php +++ b/tests/AnyDatasetTest.php @@ -423,4 +423,44 @@ public function testFromArray2() $this->assertInstanceOf(ModelPublic::class, $iterator->moveNext()->entity()); $this->assertFalse($iterator->hasNext()); } + + public function testIterator() + { + $anydata = new AnyDataset(self::SAMPLE_DIR . 'sample'); + $expected = [ + [ + "field1" => "value1", + "field2" => "value2", + ], + [ + "field1" => "othervalue1", + "field2" => "othervalue2", + ], + ]; + + // Iterator PHP + $iterator = $anydata->getIterator(); + $result = []; + while ($iterator->valid()) + { + $result[] = $iterator->current()->toArray(); + $iterator->next(); + } + $this->assertEquals($expected, $result); + + // Iterator foreach + $result = []; + foreach ($anydata->getIterator() as $row) { + $result[] = $row->toArray(); + } + $this->assertEquals($expected, $result); + + // Iterator GenericIterator + $result = []; + $iterator = $anydata->getIterator(); + while ($iterator->hasNext()) { + $result[] = $iterator->moveNext()->toArray(); + } + $this->assertEquals($expected, $result); + } } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index dfc603e..afe50f1 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -31,7 +31,7 @@ public function testBindIterator() $sr->set("name", "Testing"); $anydata->appendRow($sr); - $object = new SampleModel($anydata->getIterator()->moveNext()->toArray()); + $object = new SampleModel($anydata->getIterator()->current()->toArray()); $this->assertEquals(10, $object->Id); $this->assertEquals("Testing", $object->getName()); @@ -47,7 +47,7 @@ public function testBind_Iterator2() $anydata->addField('Name', 'Gilberto'); $object1 = new SampleModel(); - $object1->copyFrom($anydata->getIterator()->moveNext()->toArray()); + $object1->copyFrom($anydata->getIterator()->current()->toArray()); $this->assertEquals(10, $object1->Id); $this->assertEquals('Joao', $object1->getName()); } From 053939236927bf33a23f085f6fad2784e106d89d Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Thu, 13 Mar 2025 10:37:55 -0500 Subject: [PATCH 09/14] Add more documentation --- README.md | 201 +++++++++++------------------------------ docs/anydataset.md | 200 ++++++++++++++++++++++++++++++++++++++++ docs/iteratorfilter.md | 82 ++++++++++++++++- docs/iterators.md | 143 +++++++++++++++++++++++++++++ docs/populate.md | 111 +++++++++++++++++++++-- docs/row.md | 62 ++++++++++--- docs/rowoutput.md | 86 ++++++++++++++++-- docs/rowvalidator.md | 65 ++++++++++++- 8 files changed, 763 insertions(+), 187 deletions(-) create mode 100644 docs/anydataset.md create mode 100644 docs/iterators.md diff --git a/README.md b/README.md index bfca453..89eeaaa 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,35 @@ [![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) [![Build Status](https://github.com/byjg/php-anydataset/actions/workflows/phpunit.yml/badge.svg?branch=master)](https://github.com/byjg/php-anydataset/actions/workflows/phpunit.yml) -[![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) [![GitHub source](https://img.shields.io/badge/Github-source-informational?logo=github)](https://github.com/byjg/php-anydataset/) [![GitHub license](https://img.shields.io/github/license/byjg/anydataset.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/anydataset.svg)](https://github.com/byjg/php-anydataset/releases/) -AnyDataset is a data source abstraction layer for PHP. It provides a simple and consistent interface -to access different data sources. +## Overview -It is part of the [Anydataset project](https://packagist.org/providers/byjg/anydataset-implementation), -an agnostic data source abstraction layer for PHP. +AnyDataset is a powerful data source abstraction layer for PHP that provides a **simple and consistent interface** to access different data sources. +With AnyDataset, you can work with various data formats and storage systems using the same programming interface. -## Main Features +It is the core component of the [Anydataset project](https://packagist.org/providers/byjg/anydataset-implementation), an agnostic data source abstraction layer for PHP. -- Access different data sources using the same interface. -- Iterable results -- Convert results to array -- Format output -- Validate fields +## Key Features -## AnyDataset core +- **Unified Interface**: Access different data sources (databases, arrays, XML, JSON, etc.) using the same interface +- **Flexible Iteration**: Multiple ways to iterate through your data +- **Powerful Filtering**: Filter your data using a SQL-like syntax +- **Data Transformation**: Convert between different formats (JSON, XML, arrays) +- **Validation**: Validate your data against rules +- **Extensible**: Create your own data source implementations -The AnyDataset core provides the minimum classes and interfaces to allow you to use the AnyDataset project and create -your own implementation. +## Quick Start -### How it works? +### Installation -1. Create a database object -2. Get an iterator -3. Iterate over the results +```bash +composer require "byjg/anydataset" +``` + +### Basic Usage ```php getIterator(); -``` - -Iterating over the results: -**for...each** -```php +# Iterate over the results foreach ($iterator as $row) { - print $row->toArray(); + print_r($row->toArray()); } ``` -**while** -```php -// This one uses the PHP original implementation -while ($iterator->valid()) { - $row = $iterator->current(); - print $row->toArray(); - $iterator->next(); -} -``` +## Documentation -**while (implementation 2)** - -```php -while ($iterator->hasNext()) { - // This returns the current row and move the pointer to the next - print $iterator->moveNext()->toArray(); -} -``` +### Core Concepts +- [AnyDataset Overview](docs/anydataset.md) - Core component overview and usage +- [The Row Object](docs/row.md) - Working with data rows +- [Iterators](docs/iterators.md) - Different ways to iterate through data -**toArray** -```php -print_r($iterator->toArray()); -``` +### Advanced Features +- [Filtering Results](docs/iteratorfilter.md) - How to filter your data +- [Formatting Output](docs/rowoutput.md) - Transform your data into different formats +- [Field Validation](docs/rowvalidator.md) - Validate your data against rules +- [Data Population](docs/populate.md) - Populate objects with data -## Current Implementations +## Available Implementations -| Object | Data Source | Read | Write | Reference | -|------------------------|-----------------------|:----:|:-----:|-----------------------------------------------------| -| AnyDataSet | Anydataset | yes | yes | [Github](https://github.com/byjg/anydataset) | -| DbDriverInterface | Relational DB | yes | yes | [Github](https://github.com/byjg/anydataset-db) | -| ArrayDataSet | Array | yes | no | [Github](https://github.com/byjg/anydataset-array) | -| TextFileDataSet | Delimited Fields | yes | no | [Github](https://github.com/byjg/anydataset-text) | -| FixedTextFileDataSet | Fixed Size fields | yes | no | [Github](https://github.com/byjg/anydataset-text) | -| XmlDataSet | Xml | yes | no | [Github](https://github.com/byjg/anydataset-xml) | -| JSONDataSet | Json | yes | no | [Github](https://github.com/byjg/anydataset-json) | -| SparQLDataSet | SparQl Repositories | yes | no | [Github](https://github.com/byjg/anydataset-sparql) | -| NoSqlDocumentInterface | NoSql Document Based | yes | yes | [Github](https://github.com/byjg/anydataset-nosql) | -| KeyValueInterface | NoSql Key/Value Based | yes | yes | [Github](https://github.com/byjg/anydataset-nosql) | +| Data Source Type | Implementation | Read/Write | Repository | +|-----------------------|------------------------|:----------:|-----------------------------------------------------| +| **Core** | AnyDataSet | R/W | [Github](https://github.com/byjg/anydataset) | +| **Databases** | DbDriverInterface | R/W | [Github](https://github.com/byjg/anydataset-db) | +| **Arrays** | ArrayDataSet | R | [Github](https://github.com/byjg/anydataset-array) | +| **Text Files** | TextFileDataSet | R | [Github](https://github.com/byjg/anydataset-text) | +| **Fixed Width Files** | FixedTextFileDataSet | R | [Github](https://github.com/byjg/anydataset-text) | +| **XML** | XmlDataSet | R | [Github](https://github.com/byjg/anydataset-xml) | +| **JSON** | JSONDataSet | R | [Github](https://github.com/byjg/anydataset-json) | +| **SparQL** | SparQLDataSet | R | [Github](https://github.com/byjg/anydataset-sparql) | +| **NoSQL Document** | NoSqlDocumentInterface | R/W | [Github](https://github.com/byjg/anydataset-nosql) | +| **NoSQL Key/Value** | KeyValueInterface | R/W | [Github](https://github.com/byjg/anydataset-nosql) | +## Example Code Snippets - -### Filtering results +### Filtering Data ```php addRelation("field1", \ByJG\AnyDataset\Core\Enum\Relation::EQUAL, 10); -$iterator2 = $dataset->getIterator($filter); -``` - -### Converting to Array - -```php -getIterator(); -print_r($iterator->toArray()); -``` - -### Iterating with While - -```php -getIterator(); -while ($iterator->valid()) { - $row = $iterator->current(); - - print_r($row->get("field1")); - $iterator->next(); -} +$iterator = $dataset->getIterator($filter); ``` -or - -```php -foreach ($iterator as $row) { - print_r($row->get("field1")); -} -``` - -## Topics - -- [The Row object](docs/row.md) -- [Filtering the results](docs/iteratorfilter.md) -- [Format the output](docs/rowoutput.md) -- [Validate the fields](docs/rowvalidator.md) -- [Populate the fields](docs/populate.md) - -## Additional Classes - - -## Formatters - -AnyDataset comes with an extensible set to format the AnyDataset. The interface is: - -```php -namespace ByJG\AnyDataset\Core\Formatter; - -interface FormatterInterface -{ - /** - * Return the object in your original format, normally as object - * - * @return mixed - */ - public function raw(); - - /** - * Return the object transformed to string. - * - * @return string - */ - public function toText(); - - /** - * Save the contents to a file - * - * @param string $filename - * @return void - */ - public function saveToFile($filename); -} -``` - -AnyDataset implements two formatters: - -- JsonFormatter -- XmlFormatter - -Example: +### Using Formatters ```php getIterator()); -$formatter->raw(); // Return a DOM object -$formatter->toText(); // Return the XML as a text -$formatter->saveToFile("/path/to/file.xml"); // Save the XML Text to a file. -``` - -## Install - -```bash -composer require "byjg/anydataset" +$formatter->raw(); // Return a DOM object +$formatter->toText(); // Return the XML as a text +$formatter->saveToFile("/path/to/file.xml"); // Save the XML Text to a file ``` -## Running Unit tests +## Running Unit Tests ```bash vendor/bin/phpunit diff --git a/docs/anydataset.md b/docs/anydataset.md new file mode 100644 index 0000000..716bf74 --- /dev/null +++ b/docs/anydataset.md @@ -0,0 +1,200 @@ +--- +sidebar_position: 0 +--- + +# AnyDataset Overview + +The `AnyDataset` class is the core component of the AnyDataset library. It provides a simple way to store, manipulate, and retrieve data using a consistent interface. + +## Introduction + +AnyDataset is designed to be a flexible data container that can be populated from various sources and manipulated in a consistent way. It stores data as rows, where each row contains fields with values. + +## Creating an AnyDataset + +You can create an AnyDataset in several ways: + +```php + 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Mary'] +]; +$dataset = new AnyDataset($data); + +// From an XML file +$dataset = new AnyDataset('data.anydata.xml'); +``` + +## Basic Operations + +### Adding Rows + +```php +appendRow(['id' => 1, 'name' => 'John']); + +// Insert a row at a specific position +$dataset->insertRowBefore(0, ['id' => 2, 'name' => 'Mary']); +``` + +### Removing Rows + +```php +appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 2, 'name' => 'Mary']); + +// Remove by index +$dataset->removeRow(0); // Removes John + +// Remove by Row object +$row = new Row(['id' => 2, 'name' => 'Mary']); +$dataset->removeRow($row); // Removes Mary +``` + +### Sorting + +```php +appendRow(['id' => 3, 'name' => 'Paul']); +$dataset->appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 2, 'name' => 'Mary']); + +// Sort by id +$dataset->sort('id'); +``` + +### Saving to a File + +```php +appendRow(['id' => 1, 'name' => 'John']); + +// Save to a file +$dataset->save('data.anydata.xml'); +``` + +## Iterating Through Data + +The most common way to access data in an AnyDataset is through iterators: + +```php +getIterator(); + +foreach ($iterator as $row) { + echo $row->get('name') . "\n"; +} +``` + +### Filtered Iteration + +You can use `IteratorFilter` to filter the results: + +```php +and('age', Relation::GREATER_THAN, 30); + +$iterator = $dataset->getIterator($filter); + +foreach ($iterator as $row) { + // This will only iterate over rows where age > 30 + echo $row->get('name') . " is " . $row->get('age') . " years old\n"; +} +``` + +## Getting Data as Arrays + +You can extract data from the dataset as arrays: + +```php +getIterator()->toArray(); + +// Get filtered data +$filter = new IteratorFilter(); +$filter->and('age', Relation::GREATER_THAN, 30); +$filteredData = $dataset->getIterator($filter)->toArray(); + +// Get only specific fields +$namesOnly = $dataset->getIterator()->toArray(['name']); + +// Get an array of values for a specific field +$allNames = $dataset->getArray('name'); + +// Get an array of filtered values for a specific field +$adultNames = $dataset->getArray('name', $filter); +``` + +## XML Representation + +You can get the XML representation of the dataset: + +```php +appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 2, 'name' => 'Mary']); + +$xml = $dataset->xml(); +echo $xml; +``` + +This will output: + +```xml + + + + 1 + John + + + 2 + Mary + + +``` \ No newline at end of file diff --git a/docs/iteratorfilter.md b/docs/iteratorfilter.md index 287b611..2d9355b 100644 --- a/docs/iteratorfilter.md +++ b/docs/iteratorfilter.md @@ -24,11 +24,54 @@ $iterator = $dataset->getIterator($filter); // This will return an iterator with only the rows where the field is equal to 10 ``` -## And / Or Conditions +## Available Relations -For more complex queries, you can use the `and` and `or` methods to combine conditions. +The `IteratorFilter` class supports the following relations from the `Relation` enum: -For example: +| Relation | Description | +|-----------------------------------|---------------------------------------------| +| `Relation::EQUAL` | Field equals the value | +| `Relation::NOT_EQUAL` | Field does not equal the value | +| `Relation::GREATER_THAN` | Field is greater than the value | +| `Relation::LESS_THAN` | Field is less than the value | +| `Relation::GREATER_OR_EQUAL_THAN` | Field is greater than or equal to the value | +| `Relation::LESS_OR_EQUAL_THAN` | Field is less than or equal to the value | +| `Relation::CONTAINS` | Field contains the value (substring match) | +| `Relation::STARTS_WITH` | Field starts with the value | +| `Relation::IN` | Field value is in the provided array | +| `Relation::NOT_IN` | Field value is not in the provided array | + +## Filter Methods + +The `IteratorFilter` class provides the following methods: + +| Method | Description | +|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------| +| `and(string $name, Relation $relation, mixed $value)` | Adds an AND condition to the filter | +| `or(string $name, Relation $relation, mixed $value)` | Adds an OR condition to the filter | +| `startGroup(string $name, Relation $relation, mixed $value)` | Starts a group of conditions with the first condition | +| `endGroup()` | Ends a group of conditions | +| `match(array $array)` | Applies the filter to an array of rows and returns matching rows | +| `format(IteratorFilterFormatter $formatter, ?string $tableName, array &$params, string $returnFields)` | Formats the filter for use with a specific formatter (e.g., SQL) | + +## Complex Conditions with AND / OR + +For more complex queries, you can use the `and`, `or`, `startGroup`, and `endGroup` methods to combine conditions. + +### Example: Simple AND/OR Conditions + +```php +and('field1', Relation::EQUAL, 10); +$filter->and('field2', Relation::GREATER_THAN, 20); +$filter->or('field3', Relation::CONTAINS, 'test'); + +// This will match rows where: +// (field1 = 10 AND field2 > 20) OR field3 contains 'test' +``` + +### Example: Grouped Conditions If we want to filter the rows where the field is equal to 10 **or** 2, **and** the field2 is equal to 20: @@ -49,4 +92,37 @@ $filter->and('field2', Relation::EQUAL, 20); $iterator = $dataset->getIterator($filter); ``` +## Using with Different Formatters + +The `IteratorFilter` can be used with different formatters to generate SQL, XPath, or other query formats: + +```php +and('name', Relation::CONTAINS, 'John'); +$filter->and('age', Relation::GREATER_THAN, 30); + +// Format for SQL +$params = []; +$sql = $filter->format(new SqlFormatter(), 'users', $params, 'id, name, age'); +// Result: "SELECT id, name, age FROM users WHERE name LIKE ? AND age > ?" +// $params will contain ['%John%', 30] + +// Format for XPath +$xpath = $filter->format(new XPathFormatter(), null, $params); +// Result: "//row[contains(field[@name='name'], 'John') and field[@name='age'] > '30']" +``` + +## Static Factory Method + +You can also use the static factory method to create a new instance: + +```php +and('field1', Relation::EQUAL, 10) + ->and('field2', Relation::GREATER_THAN, 20); +``` + diff --git a/docs/iterators.md b/docs/iterators.md new file mode 100644 index 0000000..c0b9279 --- /dev/null +++ b/docs/iterators.md @@ -0,0 +1,143 @@ +--- +sidebar_position: 6 +--- + +# Iterators + +AnyDataset provides several iterator classes and interfaces to help you navigate through your data. These iterators follow the standard PHP `Iterator` interface, making them compatible with PHP's foreach loops and other iterator functions. + +## Iterator Interfaces + +### IteratorInterface + +The base interface for all iterators in AnyDataset: + +```php +interface IteratorInterface +{ + public function hasNext(): bool; + public function moveNext(): RowInterface|null; + public function toArray(array $fields = []): array; +} +``` + +## Iterator Classes + +### GenericIterator + +An abstract base class that implements both `IteratorInterface` and PHP's `Iterator` interface: + +```php +abstract class GenericIterator implements IteratorInterface, Iterator +{ + public function hasNext(): bool; + public function moveNext(): RowInterface|null; + public function toArray(array $fields = []): array; + + // Abstract methods that must be implemented by subclasses + abstract public function key(): mixed; + abstract public function current(): mixed; + abstract public function next(): void; + abstract public function valid(): bool; + + // Implemented method + public function rewind(): void; +} +``` + +### AnyIterator + +A concrete implementation of `GenericIterator` that works with arrays of `Row` objects: + +```php +class AnyIterator extends GenericIterator +{ + public function __construct(array $list); + public function key(): mixed; + public function current(): mixed; + public function next(): void; + public function valid(): bool; +} +``` + +## Using Iterators + +### Basic Iteration + +```php +getIterator(); + +// Using while loop +while ($iterator->valid()) { + $row = $iterator->current(); + echo $row->get('name') . "\n"; + $iterator->next(); +} + +// Or using foreach (preferred) +foreach ($iterator as $row) { + echo $row->get('name') . "\n"; +} +``` + +### Filtered Iteration + +You can use `IteratorFilter` to filter the results: + +```php +and('age', Relation::GREATER_THAN, 30); + +$iterator = $dataset->getIterator($filter); + +foreach ($iterator as $row) { + // This will only iterate over rows where age > 30 + echo $row->get('name') . " is " . $row->get('age') . " years old\n"; +} +``` + +### Converting to Array + +You can convert an iterator to an array: + +```php +getIterator(); + +// Get all data as an array of associative arrays +$allData = $iterator->toArray(); + +// Get only specific fields +$namesAndAges = $iterator->toArray(['name', 'age']); +``` + +### Using hasNext() and moveNext() + +For more control over iteration: + +```php +getIterator(); + +while ($iterator->hasNext()) { + $row = $iterator->moveNext(); + echo $row->get('name') . "\n"; +} +``` \ No newline at end of file diff --git a/docs/populate.md b/docs/populate.md index 99e3d55..2fee1a2 100644 --- a/docs/populate.md +++ b/docs/populate.md @@ -4,11 +4,11 @@ sidebar_position: 5 # Populate AnyDataSet -You can populate an AnyDataSet with data from an array or a file. +You can populate an AnyDataSet with data from various sources including arrays, files, or by building it incrementally. ## Populate from an Array -You can populate an AnyDataSet with data from an array. +You can populate an AnyDataSet with data from an array. Each element in the array should be an associative array representing a row. ```php @@ -56,9 +60,76 @@ It is a XML file with the following structure: ``` -## From scratch +## Building from Scratch + +You can also create an empty dataset and populate it incrementally. + +```php +appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 2, 'name' => 'Mary']); +$dataset->appendRow(['id' => 3, 'name' => 'Paul']); +``` + +## Importing from Another Iterator + +You can import data from any iterator that implements the `GenericIterator` interface: + +```php +import($otherIterator); +``` + +## Inserting Rows at Specific Positions + +You can insert a row at a specific position in the dataset: + +```php +appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 3, 'name' => 'Paul']); + +// Insert a row at position 1 (between John and Paul) +$dataset->insertRowBefore(1, ['id' => 2, 'name' => 'Mary']); +``` + +## Removing Rows + +You can remove rows from the dataset: + +```php +appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 2, 'name' => 'Mary']); +$dataset->appendRow(['id' => 3, 'name' => 'Paul']); + +// Remove by index +$dataset->removeRow(1); // Removes Mary + +// Remove by Row object +$row = new Row(['id' => 3, 'name' => 'Paul']); +$dataset->removeRow($row); // Removes Paul +``` + +## Saving to a File -You can also create an empty dataset and populate it later. +After populating your dataset, you can save it to an XML file: ```php appendRow(['id' => 1, 'name' => 'John']); $dataset->appendRow(['id' => 2, 'name' => 'Mary']); + +// Save to the original file (if loaded from a file) +$dataset->save(); + +// Or save to a new file +$dataset->save('new_data.anydata.xml'); +``` + +## Sorting the Dataset + +You can sort the dataset by a specific field: + +```php +appendRow(['id' => 3, 'name' => 'Paul']); +$dataset->appendRow(['id' => 1, 'name' => 'John']); +$dataset->appendRow(['id' => 2, 'name' => 'Mary']); + +// Sort by id +$dataset->sort('id'); + +// Now the dataset is ordered by id: John, Mary, Paul ``` diff --git a/docs/row.md b/docs/row.md index 6ffbf13..6bca1b5 100644 --- a/docs/row.md +++ b/docs/row.md @@ -23,7 +23,7 @@ The `Row` object implements the following methods: | `set($field, $value, $append)` | Set the value of the field. If append == true, it will add the value to the current field | | `unset($field)` | Remove the field from the row. | | `replace($field, $oldValue, $newValue)` | Replace the value of the field. If $oldValue is not set, nothing is changed. | -| `toArray($fields)` | Convert the row to an array. | +| `toArray($fields = null)` | Convert the row to an array. If $fields is provided, only those fields will be returned. | | `entity()` | Return the entity object used to store the row contents. | ## Example @@ -45,24 +45,43 @@ while ($iterator->valid()) { The `RowInterface` has two implementations: -- `RowArray` - The default implementation -- `RowObject` - An implementation that uses an object to store the values +- `RowArray` - Uses an array to store the values +- `RowObject` - Uses an object to store the values -When we iterate over a dataset, we receive a `Row` object. -The `Row` object decides how to store/get the values. +The `Row` class acts as a factory and wrapper for these implementations. When you create a `Row` object, it internally decides which implementation to use based on the data type provided. ### RowArray -This is the default implementation. It uses an array to store the values. +This is the default implementation when you provide an array. It uses an array to store the values. + +Key features: +- Supports appending values to existing fields (turning them into arrays) +- Supports unsetting specific values from array fields +- Supports replacing specific values in array fields + +```php + 1, 'name' => 'John']); +// or +$row = new RowArray(['id' => 1, 'name' => 'John']); + +$row->get('id'); // 1 +$row->set('name', 'Mary'); // Changes name to Mary +$row->set('tags', 'php', true); // Creates tags field with 'php' +$row->set('tags', 'database', true); // Appends 'database' to tags, making it an array +``` ### RowObject -This implementation uses an object to store the values. Some datasets, like the `AnyDatasetDb` dataset, can return a `RowObject` instead of a `RowArray`. -It doesn't change anything in the way you access the values, but it can be useful if you need to use the `entity()` method. +This implementation is used when you provide an object. It uses the object's properties and getter/setter methods to access values. + +Key features: +- Automatically uses getter/setter methods if they exist (e.g., `getName()`, `setName()`) +- Falls back to direct property access if methods don't exist +- Does not support appending, unsetting specific values, or replacing specific values ```php get('id'); // 1 -$row->get('name'); // John -$row->entity(); // $model +$row->get('id'); // 1 (calls getId() method) +$row->get('name'); // John (calls getName() method) +$row->entity(); // Returns the original $model object -$row->set('name', 'Mary'); +$row->set('name', 'Mary'); // Calls setName('Mary') $row->get('name'); // Mary $row->entity()->getName(); // Mary -$row->entity()->setId('20'); +$row->entity()->setId(20); $row->get('id'); // 20 ``` +## Factory Method + +The `Row` class provides a static factory method to create the appropriate implementation: + +```php + 1, 'name' => 'John']); + +// Creates a RowObject +$rowObject = Row::factory($model); +``` + diff --git a/docs/rowoutput.md b/docs/rowoutput.md index 705e161..3e9c8aa 100644 --- a/docs/rowoutput.md +++ b/docs/rowoutput.md @@ -2,36 +2,102 @@ sidebar_position: 3 --- -# RowOutput - Format a field +# RowOutput - Format Field Values -This class allows you to format a field value based on a pattern. -The pattern can be a simple string or a custom function. +The `RowOutput` class allows you to format field values based on patterns or custom functions. +This is useful for transforming raw data into display-friendly formats without modifying the original data. + +## Basic Usage ```php addFormat("field1", "Test {field1}") - ->addFormat("field2", "Showing {} and {field3}"); + ->addFormat("field2", "Showing {} and {field3}") ->addCustomFormat("field3", function ($row, $field, $value) { // return the formatted output. // $row: The row object with all values - // $field: The field has been processed + // $field: The field being processed // $value: The field value + return strtoupper($value); }); // This will output the field1 formatted: echo $output->print($row, "field1"); -// This will apply the format defintion to all fields at once: -$ouput->apply($row); +// This will apply the format definition to all fields at once: +$formattedRow = $output->apply($row); ``` -Format pattern: +## Format Patterns + +When using the `addFormat` method, you can use the following placeholders in your pattern: | Pattern | Description | |----------------|------------------------------------| -| `{}` | The current value | -| `{.}` | The field name | +| `{}` | The current field's value | +| `{.}` | The current field's name | | `{field_name}` | The value of $row->get(field_name) | +## Available Methods + +The `RowOutput` class provides the following methods: + +| Method | Description | +|----------------------------------------------------|-----------------------------------------------------------------------------------| +| `getInstance()` | Static factory method to create a new RowOutput instance. | +| `print(RowInterface $row, string $field)` | Returns the formatted value for a specific field. | +| `apply(RowInterface $row)` | Returns a new Row object with all fields formatted according to defined patterns. | +| `addFormat(string $field, string $pattern)` | Adds a pattern-based format for a specific field. | +| `addCustomFormat(string $field, Closure $closure)` | Adds a custom formatting function for a specific field. | + +## Examples + +### Pattern-based Formatting + +```php + 123, + 'name' => 'John Doe', + 'price' => 29.99 +]); + +$output = RowOutput::getInstance() + ->addFormat('id', 'ID: {}') + ->addFormat('name', 'Name: {name}') + ->addFormat('price', '${} ({id})'); +echo $output->print($row, 'id'); // "ID: 123" +echo $output->print($row, 'name'); // "Name: John Doe" +echo $output->print($row, 'price'); // "$29.99 (123)" +``` + +### Custom Formatting + +```php + '2023-12-15', + 'price' => 29.99, + 'status' => 'active' +]); + +$output = RowOutput::getInstance() + ->addCustomFormat('date', function ($row, $field, $value) { + // Convert YYYY-MM-DD to MM/DD/YYYY + $parts = explode('-', $value); + return $parts[1] . '/' . $parts[2] . '/' . $parts[0]; + }) + ->addCustomFormat('price', function ($row, $field, $value) { + // Format as currency + return '$' . number_format($value, 2); + }) + ->addCustomFormat('status', function ($row, $field, $value) { + // Convert status to badge style + return '' . ucfirst($value) . ''; + }); + +$formattedRow = $output->apply($row); +// $formattedRow now contains formatted values for all fields +``` diff --git a/docs/rowvalidator.md b/docs/rowvalidator.md index 814701a..6d2230f 100644 --- a/docs/rowvalidator.md +++ b/docs/rowvalidator.md @@ -18,10 +18,71 @@ $validator = RowValidator::getInstance() ->regexValidation("field4", '/\d{4}-\d{2}-\d{2}/') ->customValidation("field3", function($value) { // Return any string containing the error message if validation FAILS - // otherwise, just return null and the valition will pass. + // otherwise, just return null and the validation will pass. }); -$validator->validate($row) // Will return an array with the error messages. Empty array if not errors. +$validator->validate($row); // Will return an array with the error messages. Empty array if no errors. +``` + +## Available Validation Methods + +The `RowValidator` class provides the following validation methods: + +| Method | Description | +|---------------------------------------|------------------------------------------------------------------------------------------------------------| +| `requiredField($field)` | Validates that the specified field is not empty. | +| `requiredFields($fieldList)` | Validates that all fields in the array are not empty. | +| `numericFields($fieldList)` | Validates that all fields in the array contain numeric values. | +| `regexValidation($field, $regex)` | Validates that the field value matches the specified regular expression. | +| `customValidation($field, $closure)` | Applies a custom validation function to the field. The function should return an error message or null. | + +## Validation Process + +When you call the `validate()` method, the validator checks all the defined validations against the provided `Row` object: + +1. For each field with validations defined, it runs all applicable validation checks +2. If a validation fails, an error message is added to the result array +3. The method returns an array of error messages (empty if all validations pass) + +## Example with Multiple Validations + +```php +requiredField("name"); +$validator->requiredFields(["email", "phone"]); + +// Add numeric validations +$validator->numericFields(["age", "zipcode"]); + +// Add regex validation for email format +$validator->regexValidation("email", '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'); + +// Add custom validation for age (must be between 18 and 120) +$validator->customValidation("age", function($value) { + if ($value < 18) { + return "Age must be at least 18"; + } + if ($value > 120) { + return "Age must be less than 120"; + } + return null; // Validation passes +}); + +// Validate a row +$errors = $validator->validate($row); + +// Check if there are any errors +if (empty($errors)) { + echo "Validation passed!"; +} else { + echo "Validation failed with the following errors:"; + foreach ($errors as $error) { + echo "- $error\n"; + } +} ``` From 4eae37453fc2beeeda0e94f07a824597fe34a1d4 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Fri, 21 Mar 2025 15:21:51 -0500 Subject: [PATCH 10/14] Some updates --- .github/workflows/phpunit.yml | 2 +- .vscode/launch.json | 35 +++++++++++++++++++++++++++++++++++ README.md | 10 +++++----- composer.json | 2 +- phpunit.xml.dist | 2 +- 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index c04291f..2743e1c 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -24,8 +24,8 @@ jobs: steps: - uses: actions/checkout@v4 - run: composer install - - run: ./vendor/bin/phpunit - run: ./vendor/bin/psalm + - run: ./vendor/bin/phpunit Documentation: if: github.ref == 'refs/heads/master' diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4531b08 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug current Script in Console", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9003, + "runtimeArgs": [ + "-dxdebug.start_with_request=yes" + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "client_port=${port}" + } + }, + { + "name": "PHPUnit Debug", + "type": "php", + "request": "launch", + "program": "${workspaceFolder}/vendor/bin/phpunit", + "cwd": "${workspaceFolder}", + "port": 9003, + "runtimeArgs": [ + "-dxdebug.start_with_request=yes" + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "client_port=${port}" + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 89eeaaa..a1614c0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# AnyDataset - -[![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) [![Build Status](https://github.com/byjg/php-anydataset/actions/workflows/phpunit.yml/badge.svg?branch=master)](https://github.com/byjg/php-anydataset/actions/workflows/phpunit.yml) +[![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) [![GitHub source](https://img.shields.io/badge/Github-source-informational?logo=github)](https://github.com/byjg/php-anydataset/) -[![GitHub license](https://img.shields.io/github/license/byjg/anydataset.svg)](https://opensource.byjg.com/opensource/licensing.html) -[![GitHub release](https://img.shields.io/github/release/byjg/anydataset.svg)](https://github.com/byjg/php-anydataset/releases/) +[![GitHub license](https://img.shields.io/github/license/byjg/php-anydataset.svg)](https://opensource.byjg.com/opensource/licensing.html) +[![GitHub release](https://img.shields.io/github/release/byjg/php-anydataset.svg)](https://github.com/byjg/php-anydataset/releases/) + +# AnyDataset ## Overview diff --git a/composer.json b/composer.json index 9517f39..1e4ec4f 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ }, "require-dev": { "phpunit/phpunit": "^10.5|^11.5", - "vimeo/psalm": "^6.0" + "vimeo/psalm": "^5.9|^6.2" }, "provide": { "byjg/anydataset-implementation": "1.0" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 584610f..711a9b0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,7 +15,7 @@ and open the template in the editor. displayDetailsOnTestsThatTriggerWarnings="true" displayDetailsOnPhpunitDeprecations="true" stopOnFailure="false" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"> From cd7f43906e0f803448769437b86fff1f41a64863 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Fri, 21 Mar 2025 15:23:58 -0500 Subject: [PATCH 11/14] Psalm Fixes --- src/AnyIterator.php | 4 ++++ src/Formatter/BaseFormatter.php | 3 +++ src/Formatter/JsonFormatter.php | 2 ++ src/Formatter/XmlFormatter.php | 2 ++ src/GenericIterator.php | 8 ++++++++ src/IteratorFilterXPathFormatter.php | 2 ++ src/Row.php | 6 ++++++ src/RowArray.php | 6 ++++++ src/RowObject.php | 6 ++++++ tests/AnyDatasetTest.php | 1 + tests/IteratorFilterAnydatasetTest.php | 1 + tests/IteratorFilterXPathTest.php | 1 + tests/RowTest.php | 1 + tests/RowValidatorTest.php | 1 + 14 files changed, 44 insertions(+) diff --git a/src/AnyIterator.php b/src/AnyIterator.php index 92fd4b7..97c156a 100644 --- a/src/AnyIterator.php +++ b/src/AnyIterator.php @@ -36,6 +36,7 @@ public function __construct(array $list) /** * @inheritDoc */ + #[\Override] #[ReturnTypeWillChange] public function key(): mixed { @@ -45,18 +46,21 @@ public function key(): mixed /** * @inheritDoc */ + #[\Override] #[ReturnTypeWillChange] public function current(): mixed { return $this->list[$this->curRow] ?? null; } + #[\Override] #[ReturnTypeWillChange] public function next(): void { $this->curRow++; } + #[\Override] #[ReturnTypeWillChange] public function valid(): bool { diff --git a/src/Formatter/BaseFormatter.php b/src/Formatter/BaseFormatter.php index 7fcaada..631ca8e 100644 --- a/src/Formatter/BaseFormatter.php +++ b/src/Formatter/BaseFormatter.php @@ -16,16 +16,19 @@ abstract class BaseFormatter implements FormatterInterface /** * @inheritDoc */ + #[\Override] abstract public function raw(): mixed; /** * @inheritDoc */ + #[\Override] abstract public function toText(): string|false; /** * @inheritDoc */ + #[\Override] public function saveToFile(string $filename): void { if (empty($filename)) { diff --git a/src/Formatter/JsonFormatter.php b/src/Formatter/JsonFormatter.php index 9368896..5d5b462 100644 --- a/src/Formatter/JsonFormatter.php +++ b/src/Formatter/JsonFormatter.php @@ -9,6 +9,7 @@ class JsonFormatter extends BaseFormatter /** * @inheritDoc */ + #[\Override] public function raw(): mixed { return json_decode($this->toText()); @@ -17,6 +18,7 @@ public function raw(): mixed /** * @inheritDoc */ + #[\Override] public function toText(): string|false { if ($this->object instanceof GenericIterator) { diff --git a/src/Formatter/XmlFormatter.php b/src/Formatter/XmlFormatter.php index 3996173..5bffa56 100644 --- a/src/Formatter/XmlFormatter.php +++ b/src/Formatter/XmlFormatter.php @@ -48,6 +48,7 @@ protected function rowXml(array $row, ?XmlDocument $parentDocument = null): XmlN /** * @inheritDoc */ + #[\Override] public function raw(): mixed { if ($this->object instanceof GenericIterator) { @@ -59,6 +60,7 @@ public function raw(): mixed /** * @inheritDoc */ + #[\Override] public function toText(): string|false { return $this->raw()->saveXML(); diff --git a/src/GenericIterator.php b/src/GenericIterator.php index 789ad4c..f451fd9 100644 --- a/src/GenericIterator.php +++ b/src/GenericIterator.php @@ -10,11 +10,13 @@ */ abstract class GenericIterator implements IteratorInterface, Iterator { + #[\Override] public function hasNext(): bool { return $this->valid(); } + #[\Override] public function moveNext(): RowInterface|null { $row = $this->current(); @@ -27,6 +29,7 @@ public function moveNext(): RowInterface|null * @param array $fields * @return array */ + #[\Override] public function toArray(array $fields = []): array { $retArray = []; @@ -45,18 +48,21 @@ public function toArray(array $fields = []): array /** * @inheritDoc */ + #[\Override] #[ReturnTypeWillChange] abstract public function key(): mixed; /** * @return mixed */ + #[\Override] #[ReturnTypeWillChange] abstract public function current(): mixed; /** * @inheritDoc */ + #[\Override] #[ReturnTypeWillChange] public function rewind(): void { @@ -66,12 +72,14 @@ public function rewind(): void /** * @inheritDoc */ + #[\Override] #[ReturnTypeWillChange] abstract public function next(): void; /** * @inheritDoc */ + #[\Override] #[ReturnTypeWillChange] abstract public function valid(): bool; } diff --git a/src/IteratorFilterXPathFormatter.php b/src/IteratorFilterXPathFormatter.php index 0713fba..d1501eb 100644 --- a/src/IteratorFilterXPathFormatter.php +++ b/src/IteratorFilterXPathFormatter.php @@ -9,6 +9,7 @@ class IteratorFilterXPathFormatter extends IteratorFilterFormatter /** * @inheritDoc */ + #[\Override] public function format(array $filters, ?string $tableName = null, array &$params = [], string $returnFields = "*"): string { $param = []; @@ -24,6 +25,7 @@ public function format(array $filters, ?string $tableName = null, array &$params /** * @inheritDoc */ + #[\Override] public function getRelation(string $name, Relation $relation, mixed $value, array &$param): string { $str = is_numeric($value) ? "" : "'"; diff --git a/src/Row.php b/src/Row.php index 8514f9f..19b0567 100644 --- a/src/Row.php +++ b/src/Row.php @@ -22,31 +22,37 @@ public static function factory(object|array $instance = []): RowInterface return new RowObject($instance); } + #[\Override] public function get(string $name): mixed { return $this->entity->get($name); } + #[\Override] public function set(string $name, mixed $value, bool $append = false): void { $this->entity->set($name, $value, $append); } + #[\Override] public function unset(string $name, mixed $value = null): void { $this->entity->unset($name, $value); } + #[\Override] public function replace(string $name, mixed $oldValue, mixed $newValue): void { $this->entity->replace($name, $oldValue, $newValue); } + #[\Override] public function toArray(?array $fields = []): array { return $this->entity->toArray($fields); } + #[\Override] public function entity(): mixed { return $this->entity->entity(); diff --git a/src/RowArray.php b/src/RowArray.php index cd04532..f7496d8 100644 --- a/src/RowArray.php +++ b/src/RowArray.php @@ -15,11 +15,13 @@ public function __construct(array $instance = []) } + #[\Override] public function get(string $name): mixed { return $this->entity[$name] ?? null; } + #[\Override] public function set(string $name, mixed $value, bool $append = false): void { if (!isset($this->entity[$name])) { @@ -37,6 +39,7 @@ public function set(string $name, mixed $value, bool $append = false): void } } + #[\Override] public function unset(string $name, mixed $value = null): void { if (empty($value)) { @@ -60,6 +63,7 @@ public function unset(string $name, mixed $value = null): void return; } + #[\Override] public function replace(string $name, mixed $oldValue, mixed $newValue): void { if (!is_array($this->entity[$name])) { @@ -72,6 +76,7 @@ public function replace(string $name, mixed $oldValue, mixed $newValue): void }, $this->entity[$name]); } + #[\Override] public function toArray(?array $fields = []): array { if (empty($fields)) { @@ -83,6 +88,7 @@ public function toArray(?array $fields = []): array } + #[\Override] public function entity(): mixed { return $this->entity; diff --git a/src/RowObject.php b/src/RowObject.php index ec199ea..7944384 100644 --- a/src/RowObject.php +++ b/src/RowObject.php @@ -17,6 +17,7 @@ public function __construct(object $instance) } + #[\Override] public function get(string $name): mixed { if (property_exists($this->entity, $name)) { @@ -31,6 +32,7 @@ public function get(string $name): mixed return null; } + #[\Override] public function set(string $name, mixed $value, bool $append = false): void { if ($append) { @@ -51,16 +53,19 @@ public function set(string $name, mixed $value, bool $append = false): void throw new \InvalidArgumentException("Field '$name' not found"); } + #[\Override] public function unset(string $name, mixed $value = null): void { throw new \InvalidArgumentException("Unset is not supported for object"); } + #[\Override] public function replace(string $name, mixed $oldValue, mixed $newValue): void { throw new \InvalidArgumentException("Replace is not supported for object"); } + #[\Override] public function toArray(?array $fields = []): array { $result = Serialize::from($this->entity)->toArray(); @@ -76,6 +81,7 @@ public function toArray(?array $fields = []): array return $retArray; } + #[\Override] public function entity(): mixed { return $this->entity; diff --git a/tests/AnyDatasetTest.php b/tests/AnyDatasetTest.php index 8fe3767..e68a34c 100644 --- a/tests/AnyDatasetTest.php +++ b/tests/AnyDatasetTest.php @@ -27,6 +27,7 @@ class AnyDatasetTest extends TestCase * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ + #[\Override] protected function setUp(): void { $this->object = new AnyDataset(); diff --git a/tests/IteratorFilterAnydatasetTest.php b/tests/IteratorFilterAnydatasetTest.php index 46444da..46cd349 100644 --- a/tests/IteratorFilterAnydatasetTest.php +++ b/tests/IteratorFilterAnydatasetTest.php @@ -20,6 +20,7 @@ class IteratorFilterAnydatasetTest extends TestCase * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ + #[\Override] protected function setUp(): void { $this->object = new IteratorFilter(); diff --git a/tests/IteratorFilterXPathTest.php b/tests/IteratorFilterXPathTest.php index 9e1ed60..65a8d02 100644 --- a/tests/IteratorFilterXPathTest.php +++ b/tests/IteratorFilterXPathTest.php @@ -19,6 +19,7 @@ class IteratorFilterXPathTest extends TestCase * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ + #[\Override] protected function setUp(): void { $this->object = new IteratorFilter(); diff --git a/tests/RowTest.php b/tests/RowTest.php index 0461235..1ff393b 100644 --- a/tests/RowTest.php +++ b/tests/RowTest.php @@ -24,6 +24,7 @@ class RowTest extends TestCase * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ + #[\Override] protected function setUp(): void { $this->object = new Row(); diff --git a/tests/RowValidatorTest.php b/tests/RowValidatorTest.php index 64d0f0b..ddf730b 100644 --- a/tests/RowValidatorTest.php +++ b/tests/RowValidatorTest.php @@ -19,6 +19,7 @@ class RowValidatorTest extends TestCase * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ + #[\Override] protected function setUp(): void { $this->row1 = new Row([ From 75da489d12d764a7f760c96e666166b59810e17b Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Fri, 21 Mar 2025 15:35:17 -0500 Subject: [PATCH 12/14] Update method signatures and documentation details Updated method return types for consistency across `RowOutput`, `RowValidator`, and `IteratorFilter` classes. Adjusted documentation with clearer examples and added new features such as NULL filtering, row importing, and field additions to datasets. Also reorganized sidebar positions to improve navigation. --- docs/anydataset.md | 33 +++++++++++++++++++++++++++++---- docs/iteratorfilter.md | 22 ++++++++++++++++++++-- docs/iterators.md | 13 ++++++++++++- docs/populate.md | 2 +- docs/rowoutput.md | 16 ++++++++-------- docs/rowvalidator.md | 17 ++++++++--------- 6 files changed, 78 insertions(+), 25 deletions(-) diff --git a/docs/anydataset.md b/docs/anydataset.md index 716bf74..c142954 100644 --- a/docs/anydataset.md +++ b/docs/anydataset.md @@ -21,14 +21,16 @@ use ByJG\AnyDataset\Core\AnyDataset; // Empty dataset $dataset = new AnyDataset(); -// From an array +// From an array of associative arrays $data = [ ['id' => 1, 'name' => 'John'], ['id' => 2, 'name' => 'Mary'] ]; $dataset = new AnyDataset($data); -// From an XML file +// From an XML file (with .anydata.xml extension automatically added if not specified) +$dataset = new AnyDataset('data'); +// Or with full extension $dataset = new AnyDataset('data.anydata.xml'); ``` @@ -47,6 +49,10 @@ $dataset->appendRow(['id' => 1, 'name' => 'John']); // Insert a row at a specific position $dataset->insertRowBefore(0, ['id' => 2, 'name' => 'Mary']); + +// Import rows from an iterator +$otherDataset = new AnyDataset($someData); +$dataset->import($otherDataset->getIterator()); ``` ### Removing Rows @@ -66,6 +72,22 @@ $dataset->removeRow(0); // Removes John // Remove by Row object $row = new Row(['id' => 2, 'name' => 'Mary']); $dataset->removeRow($row); // Removes Mary + +// Remove current row (last one added or accessed) +$dataset->removeRow(); +``` + +### Adding Fields to Current Row + +```php +appendRow(['id' => 1, 'name' => 'John']); + +// Add a field to the current row +$dataset->addField('email', 'john@example.com'); ``` ### Sorting @@ -92,8 +114,11 @@ use ByJG\AnyDataset\Core\AnyDataset; $dataset = new AnyDataset(); $dataset->appendRow(['id' => 1, 'name' => 'John']); -// Save to a file -$dataset->save('data.anydata.xml'); +// Save to the file specified in constructor +$dataset->save(); + +// Or save to a different file +$dataset->save('new_data.anydata.xml'); ``` ## Iterating Through Data diff --git a/docs/iteratorfilter.md b/docs/iteratorfilter.md index 2d9355b..6c646b1 100644 --- a/docs/iteratorfilter.md +++ b/docs/iteratorfilter.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 --- # Filtering Results @@ -40,6 +40,8 @@ The `IteratorFilter` class supports the following relations from the `Relation` | `Relation::STARTS_WITH` | Field starts with the value | | `Relation::IN` | Field value is in the provided array | | `Relation::NOT_IN` | Field value is not in the provided array | +| `Relation::IS_NULL` | Field value is null | +| `Relation::IS_NOT_NULL` | Field value is not null | ## Filter Methods @@ -47,12 +49,14 @@ The `IteratorFilter` class provides the following methods: | Method | Description | |--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------| -| `and(string $name, Relation $relation, mixed $value)` | Adds an AND condition to the filter | +| `and(string $name, Relation $relation, mixed $value = null)` | Adds an AND condition to the filter | | `or(string $name, Relation $relation, mixed $value)` | Adds an OR condition to the filter | | `startGroup(string $name, Relation $relation, mixed $value)` | Starts a group of conditions with the first condition | | `endGroup()` | Ends a group of conditions | | `match(array $array)` | Applies the filter to an array of rows and returns matching rows | | `format(IteratorFilterFormatter $formatter, ?string $tableName, array &$params, string $returnFields)` | Formats the filter for use with a specific formatter (e.g., SQL) | +| `addRelation(string $name, Relation $relation, mixed $value)` | Alias for `and()` (deprecated) | +| `addRelationOr(string $name, Relation $relation, mixed $value)` | Alias for `or()` (deprecated) | ## Complex Conditions with AND / OR @@ -92,6 +96,20 @@ $filter->and('field2', Relation::EQUAL, 20); $iterator = $dataset->getIterator($filter); ``` +### Example: NULL Checking + +To filter for rows where a field is null or not null: + +```php +and('email', Relation::IS_NULL); + +// Or find rows where email is not null +$filter->and('email', Relation::IS_NOT_NULL); +``` + ## Using with Different Formatters The `IteratorFilter` can be used with different formatters to generate SQL, XPath, or other query formats: diff --git a/docs/iterators.md b/docs/iterators.md index c0b9279..9268314 100644 --- a/docs/iterators.md +++ b/docs/iterators.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 2 --- # Iterators @@ -15,8 +15,19 @@ The base interface for all iterators in AnyDataset: ```php interface IteratorInterface { + /** + * Check if exists more records. + */ public function hasNext(): bool; + + /** + * Get the next record. Return a Row object. + */ public function moveNext(): RowInterface|null; + + /** + * Get an array representation of the iterator. + */ public function toArray(array $fields = []): array; } ``` diff --git a/docs/populate.md b/docs/populate.md index 2fee1a2..df60704 100644 --- a/docs/populate.md +++ b/docs/populate.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 --- # Populate AnyDataSet diff --git a/docs/rowoutput.md b/docs/rowoutput.md index 3e9c8aa..89bfca3 100644 --- a/docs/rowoutput.md +++ b/docs/rowoutput.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 --- # RowOutput - Format Field Values @@ -43,13 +43,13 @@ When using the `addFormat` method, you can use the following placeholders in you The `RowOutput` class provides the following methods: -| Method | Description | -|----------------------------------------------------|-----------------------------------------------------------------------------------| -| `getInstance()` | Static factory method to create a new RowOutput instance. | -| `print(RowInterface $row, string $field)` | Returns the formatted value for a specific field. | -| `apply(RowInterface $row)` | Returns a new Row object with all fields formatted according to defined patterns. | -| `addFormat(string $field, string $pattern)` | Adds a pattern-based format for a specific field. | -| `addCustomFormat(string $field, Closure $closure)` | Adds a custom formatting function for a specific field. | +| Method | Description | +|------------------------------------------------------------|-----------------------------------------------------------------------------------| +| `getInstance()` | Static factory method to create a new RowOutput instance. | +| `print(RowInterface $row, string $field): mixed` | Returns the formatted value for a specific field. | +| `apply(RowInterface $row): RowInterface` | Returns a new Row object with all fields formatted according to defined patterns. | +| `addFormat(string $field, string $pattern): static` | Adds a pattern-based format for a specific field. | +| `addCustomFormat(string $field, Closure $closure): static` | Adds a custom formatting function for a specific field. | ## Examples diff --git a/docs/rowvalidator.md b/docs/rowvalidator.md index 6d2230f..1ce04e4 100644 --- a/docs/rowvalidator.md +++ b/docs/rowvalidator.md @@ -1,8 +1,7 @@ --- -sidebar_position: 4 +sidebar_position: 5 --- - # Row Validator - Validate Field contents The `RowValidator` class allows you to validate the contents of the fields in a `Row` object. @@ -28,13 +27,13 @@ $validator->validate($row); // Will return an array with the error messages. Emp The `RowValidator` class provides the following validation methods: -| Method | Description | -|---------------------------------------|------------------------------------------------------------------------------------------------------------| -| `requiredField($field)` | Validates that the specified field is not empty. | -| `requiredFields($fieldList)` | Validates that all fields in the array are not empty. | -| `numericFields($fieldList)` | Validates that all fields in the array contain numeric values. | -| `regexValidation($field, $regex)` | Validates that the field value matches the specified regular expression. | -| `customValidation($field, $closure)` | Applies a custom validation function to the field. The function should return an error message or null. | +| Method | Description | +|--------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| +| `requiredField(string $field): RowValidator` | Validates that the specified field is not empty. | +| `requiredFields(array $fieldList): RowValidator` | Validates that all fields in the array are not empty. | +| `numericFields(array $fieldList): RowValidator` | Validates that all fields in the array contain numeric values. | +| `regexValidation(string\|array $field, string $regex): RowValidator` | Validates that the field value matches the specified regular expression. | +| `customValidation(string\|array $field, Closure $closure): RowValidator` | Applies a custom validation function to the field. The function should return an error message or null. | ## Validation Process From 81eb4807ae6b6be9de77d08c856eb135dd10b169 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 22 Mar 2025 19:59:00 -0500 Subject: [PATCH 13/14] Remove hasNext and moveNext --- composer.json | 4 ++-- docs/iterators.md | 33 ++------------------------------- src/GenericIterator.php | 17 +---------------- src/IteratorInterface.php | 19 +++---------------- tests/AnyDatasetTest.php | 18 +++++++++++++----- 5 files changed, 21 insertions(+), 70 deletions(-) diff --git a/composer.json b/composer.json index 1e4ec4f..dc28729 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,8 @@ "require": { "php": ">=8.1 <8.5", "ext-dom": "*", - "byjg/xmlutil": "^5.1", - "byjg/serializer": "^5.1" + "byjg/xmlutil": "^6.0", + "byjg/serializer": "^6.0" }, "suggest": { }, diff --git a/docs/iterators.md b/docs/iterators.md index 9268314..4dfdb25 100644 --- a/docs/iterators.md +++ b/docs/iterators.md @@ -13,18 +13,8 @@ AnyDataset provides several iterator classes and interfaces to help you navigate The base interface for all iterators in AnyDataset: ```php -interface IteratorInterface +interface IteratorInterface extends Iterator { - /** - * Check if exists more records. - */ - public function hasNext(): bool; - - /** - * Get the next record. Return a Row object. - */ - public function moveNext(): RowInterface|null; - /** * Get an array representation of the iterator. */ @@ -39,10 +29,8 @@ interface IteratorInterface An abstract base class that implements both `IteratorInterface` and PHP's `Iterator` interface: ```php -abstract class GenericIterator implements IteratorInterface, Iterator +abstract class GenericIterator implements IteratorInterface { - public function hasNext(): bool; - public function moveNext(): RowInterface|null; public function toArray(array $fields = []): array; // Abstract methods that must be implemented by subclasses @@ -135,20 +123,3 @@ $allData = $iterator->toArray(); // Get only specific fields $namesAndAges = $iterator->toArray(['name', 'age']); ``` - -### Using hasNext() and moveNext() - -For more control over iteration: - -```php -getIterator(); - -while ($iterator->hasNext()) { - $row = $iterator->moveNext(); - echo $row->get('name') . "\n"; -} -``` \ No newline at end of file diff --git a/src/GenericIterator.php b/src/GenericIterator.php index f451fd9..3639a2d 100644 --- a/src/GenericIterator.php +++ b/src/GenericIterator.php @@ -2,28 +2,13 @@ namespace ByJG\AnyDataset\Core; -use Iterator; use ReturnTypeWillChange; /** * @psalm-suppress MissingTemplateParam */ -abstract class GenericIterator implements IteratorInterface, Iterator +abstract class GenericIterator implements IteratorInterface { - #[\Override] - public function hasNext(): bool - { - return $this->valid(); - } - - #[\Override] - public function moveNext(): RowInterface|null - { - $row = $this->current(); - $this->next(); - return $row; - } - /** * @inheritDoc * @param array $fields diff --git a/src/IteratorInterface.php b/src/IteratorInterface.php index e62e2a9..c170ac5 100644 --- a/src/IteratorInterface.php +++ b/src/IteratorInterface.php @@ -2,23 +2,10 @@ namespace ByJG\AnyDataset\Core; -interface IteratorInterface -{ - - /** - * Check if exists more records. - * - * @return bool Return True if is possible get one or more records. - */ - public function hasNext(): bool; - - /** - * Get the next record.Return a Row object - * - * @return RowInterface|null - */ - public function moveNext(): RowInterface|null; +use Iterator; +interface IteratorInterface extends Iterator +{ /** * Get an array of the iterator * diff --git a/tests/AnyDatasetTest.php b/tests/AnyDatasetTest.php index e68a34c..9545528 100644 --- a/tests/AnyDatasetTest.php +++ b/tests/AnyDatasetTest.php @@ -420,9 +420,16 @@ public function testFromArray2() $this->assertEquals($expected, $iterator); $iterator = $anydataset->getIterator(); - $this->assertIsArray($iterator->moveNext()->entity()); - $this->assertInstanceOf(ModelPublic::class, $iterator->moveNext()->entity()); - $this->assertFalse($iterator->hasNext()); + $this->assertTrue($iterator->valid()); + $this->assertIsArray($iterator->current()->entity()); + + $iterator->next(); + $this->assertTrue($iterator->valid()); + $this->assertInstanceOf(ModelPublic::class, $iterator->current()->entity()); + + $iterator->next(); + $this->assertFalse($iterator->valid()); + $this->assertNull($iterator->current()); } public function testIterator() @@ -459,8 +466,9 @@ public function testIterator() // Iterator GenericIterator $result = []; $iterator = $anydata->getIterator(); - while ($iterator->hasNext()) { - $result[] = $iterator->moveNext()->toArray(); + while ($iterator->valid()) { + $result[] = $iterator->current()->toArray(); + $iterator->next(); } $this->assertEquals($expected, $result); } From e711eff4dcd6c77aa38cdca48d7503c8721b6d13 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 22 Mar 2025 20:07:35 -0500 Subject: [PATCH 14/14] Remove hasNext and moveNext --- src/IteratorInterface.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/IteratorInterface.php b/src/IteratorInterface.php index c170ac5..47b94ef 100644 --- a/src/IteratorInterface.php +++ b/src/IteratorInterface.php @@ -4,6 +4,9 @@ use Iterator; +/** + * @extends Iterator + */ interface IteratorInterface extends Iterator { /**