diff --git a/config/services.yml b/config/services.yml index eaa66b0..5bd5ab6 100644 --- a/config/services.yml +++ b/config/services.yml @@ -38,6 +38,10 @@ services: # arguments: # $tagsService: '@eztags.api.service.tags' + elbformat_field_helper.field_helper.matrix: + class: Elbformat\FieldHelperBundle\FieldHelper\MatrixFieldHelper + tags: ['elbformat_field_helper.field_helper'] + elbformat_field_helper.field_helper.number: class: Elbformat\FieldHelperBundle\FieldHelper\NumberFieldHelper tags: ['elbformat_field_helper.field_helper'] diff --git a/docs/changelog.md b/docs/changelog.md index dd8bc96..f13c11d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## v1.2.2 +Added field helper for +* Matrix + ## v1.2.1 Added forgotten field helper for * Selection diff --git a/docs/fields.md b/docs/fields.md index af63f2b..a8f6467 100644 --- a/docs/fields.md +++ b/docs/fields.md @@ -22,7 +22,7 @@ Implemented types are: | ISBN | | | | Keyword | | | | MapLocation | | | -| Matrix | ezmatrix | | +| Matrix | ezmatrix | MatrixFieldHelper | | Media | | | | Null | | | | Page | | | diff --git a/docs/testing.md b/docs/testing.md index ab07339..2d77a8e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -13,7 +13,7 @@ vendor/bin/psalm To run integration tests, you need to spin up a database first. To ease this, you can use the docker-compose setup provided ```bash docker-compose up -d -docker-compose run php bash +docker-compose exec php sh vendor/bin/phpunit --testsuite integration ``` diff --git a/src/FieldHelper/MatrixFieldHelper.php b/src/FieldHelper/MatrixFieldHelper.php new file mode 100644 index 0000000..dc5313d --- /dev/null +++ b/src/FieldHelper/MatrixFieldHelper.php @@ -0,0 +1,242 @@ + + */ +class MatrixFieldHelper extends AbstractFieldHelper +{ + /** + * Return a row-based 2-dimensional array. + * [ + * [A1,B1,C1], + * [A2,B2,C2], + * ] + * + * @return string[][] + * + * @throws FieldNotFoundException + * @throws InvalidFieldTypeException + */ + public function getArray(Content $content, string $fieldName): array + { + $rows = $this->getValue($content, $fieldName)->getRows(); + $result = []; + /** @var Value\Row $row */ + foreach ($rows as $row) { + /** @var string[] $cells */ + $cells = $row->getCells(); + $result[] = array_values($cells); + } + + return $result; + } + + /** + * Return a key-value based array structure of the table. Each row has the headlines as key. + * [ + * [A1 => A2, B1 => B2, C1 => C2], + * [A1 => A3, B1 => B3, C1 => C3], + * ] + * + * @return mixed[][] + * + * @throws FieldNotFoundException + * @throws InvalidFieldTypeException + */ + public function getAssoc(Content $content, string $fieldName): array + { + $rows = $this->getArray($content, $fieldName); + $headlines = $this->getHeadlineIds($content, $fieldName); + $numHeadlines = count($headlines); + + $result = []; + /** @var Value\Row $row */ + foreach ($rows as $cells) { + $rowData = []; + for ($i = 0; $i < $numHeadlines; $i++) { + $rowData[$headlines[$i]] = $cells[$i]; + } + $result[] = $rowData; + } + + return $result; + } + + /** + * Return a key-value list for 2-column tables. + * [A1 => B1, A2 => B2, A3 => B3] + * + * @return mixed[] + */ + public function getKeyValue(Content $content, string $fieldName, ?string $key = null, ?string $val = null): array + { + $rows = $this->getValue($content, $fieldName)->getRows(); + if (null === $key) { + $key = $this->getHeadlineIds($content, $fieldName)[0] ?? ''; + } + if (null === $val) { + $val = $this->getHeadlineIds($content, $fieldName)[1] ?? ''; + } + + $result = []; + /** @var Value\Row $row */ + foreach ($rows as $row) { + /** @var string[] $cells */ + $cells = $row->getCells(); + $result[$cells[$key]] = $cells[$val] ?? ''; + } + + return $result; + } + + /** + * Return a list for 1-column tables. + * [A1,A2,A3] + * + * @return string[] + */ + public function getList(Content $content, string $fieldName, string $columnName = null): array + { + if (null === $columnName) { + $columnName = $this->getHeadlineIds($content, $fieldName)[0] ?? ''; + } + $rows = $this->getValue($content, $fieldName)->getRows(); + $result = []; + /** @var Value\Row $row */ + foreach ($rows as $row) { + $cells = $row->getCells(); + $result[] = (string) $cells[$columnName]; + } + + return $result; + } + + public function isEmpty(Content $content, string $fieldName): bool + { + return $this->getValue($content, $fieldName)->getRows()->count() <= 0; + } + + /** + * @param array> $rowsWithCols + */ + public function updateAssoc(ContentStruct $struct, string $fieldName, array $rowsWithCols, ?Content $content = null): bool + { + // No changes + if (null !== $content) { + $current = $this->getArray($content, $fieldName); + if ($this->arrayEquals($current, $rowsWithCols)) { + return false; + } + } + + // Convert to rows + $rows = []; + foreach ($rowsWithCols as $cols) { + $rows[] = new Value\Row($cols); + } + + $struct->setField($fieldName, $rows); + + return true; + } + + /** + * @param string[] $values + */ + public function updateList(ContentStruct $struct, string $fieldName, array $values, string $columnName, ?Content $content = null): bool + { + // No changes + if (null !== $content) { + $field = $this->getList($content, $fieldName, $columnName); + if ($this->isListEqual($field, $values)) { + return false; + } + } + // Convert to rows + $rows = []; + foreach ($values as $value) { + $rows[] = new Value\Row([$columnName => $value]); + } + + $struct->setField($fieldName, $rows); + + return true; + } + + protected function getValue(Content $content, string $fieldName): Value + { + $field = $this->getField($content, $fieldName); + if (!$field->value instanceof Value) { + throw InvalidFieldTypeException::fromActualAndExpected($field->value, [Value::class]); + } + + return $field->value; + } + + /** + * @param string[] $list1 + * @param string[] $list2 + */ + protected function isListEqual(array $list1, array $list2): bool + { + if (\count($list1) !== \count($list2)) { + return false; + } + for ($i = 0, $iMax = \count($list1); $i < $iMax; ++$i) { + if ($list1[$i] !== $list2[$i]) { + return false; + } + } + + return true; + } + + /** @return string[] */ + protected function getHeadlineIds(Content $content, string $fieldName): array + { + $fieldDef = $content->getContentType()->getFieldDefinition($fieldName); + if (null === $fieldDef) { + return []; + } + + $colIds = []; + /** @var array[] $columns */ + $columns = $fieldDef->fieldSettings['columns']; + foreach ($columns as $colDef) { + $colIds[] = (string) $colDef['identifier']; + } + + return $colIds; + } + + protected function arrayEquals(array $arr1, array $arr2): bool + { + return $this->arrayContains($arr1, $arr2) && $this->arrayContains($arr2, $arr1); + } + + protected function arrayContains(array $arr1, array $arr2): bool + { + return 1 > count( + array_udiff($arr1, $arr2, function ($v1, $v2): int { + if (is_array($v1) && is_array($v2)) { + return !$this->arrayContains($v1, $v2) ? 1 : 0; + } + if (is_array($v1) || is_array($v2)) { + return 1; + } + + return $v1 !== $v2 ? 1 : 0; + }) + ); + } +} diff --git a/src/Registry/Registry.php b/src/Registry/Registry.php index 6d97510..ef0669c 100644 --- a/src/Registry/Registry.php +++ b/src/Registry/Registry.php @@ -12,6 +12,7 @@ use Elbformat\FieldHelperBundle\FieldHelper\FieldHelperInterface; use Elbformat\FieldHelperBundle\FieldHelper\FileFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\ImageFieldHelper; +use Elbformat\FieldHelperBundle\FieldHelper\MatrixFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\NetgenTagsFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\NumberFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\RelationFieldHelper; @@ -77,6 +78,11 @@ public function getImageFieldHelper(): ImageFieldHelper return $this->getFieldHelper(ImageFieldHelper::class); } + public function getMatrixFieldHelper(): MatrixFieldHelper + { + return $this->getFieldHelper(MatrixFieldHelper::class); + } + public function getNetgenTagsFieldHelper(): NetgenTagsFieldHelper { return $this->getFieldHelper(NetgenTagsFieldHelper::class); diff --git a/src/Registry/RegistryInterface.php b/src/Registry/RegistryInterface.php index b42022b..57874c5 100644 --- a/src/Registry/RegistryInterface.php +++ b/src/Registry/RegistryInterface.php @@ -8,6 +8,7 @@ use Elbformat\FieldHelperBundle\FieldHelper\FieldHelperInterface; use Elbformat\FieldHelperBundle\FieldHelper\FileFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\ImageFieldHelper; +use Elbformat\FieldHelperBundle\FieldHelper\MatrixFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\NetgenTagsFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\NumberFieldHelper; use Elbformat\FieldHelperBundle\FieldHelper\RelationFieldHelper; @@ -30,6 +31,7 @@ public function getBoolFieldHelper(): BoolFieldHelper; public function getDateTimeFieldHelper(): DateTimeFieldHelper; public function getFileFieldHelper(): FileFieldHelper; public function getImageFieldHelper(): ImageFieldHelper; + public function getMatrixFieldHelper(): MatrixFieldHelper; public function getNetgenTagsFieldHelper(): NetgenTagsFieldHelper; public function getNumberFieldHelper(): NumberFieldHelper; public function getRelationFieldHelper(): RelationFieldHelper; diff --git a/tests/DependencyInjection/Compile/FieldHelperPassTest.php b/tests/DependencyInjection/Compile/FieldHelperPassTest.php index a4209ca..284d309 100644 --- a/tests/DependencyInjection/Compile/FieldHelperPassTest.php +++ b/tests/DependencyInjection/Compile/FieldHelperPassTest.php @@ -25,7 +25,7 @@ public function testProcess(): void if (BoolFieldHelper::class !== array_keys($arg)[0]) { throw new \Exception('Invalid helper name: '.array_keys($arg)[0]); } - if (! array_values($arg)[0] instanceof Reference) { + if (!array_values($arg)[0] instanceof Reference) { throw new \Exception('Helper not a reference'); } return ('elbformat_field_helper.field_helper.test' === (string) array_values($arg)[0]); diff --git a/tests/FieldHelper/MatrixFieldHelperTest.php b/tests/FieldHelper/MatrixFieldHelperTest.php new file mode 100644 index 0000000..df2b80a --- /dev/null +++ b/tests/FieldHelper/MatrixFieldHelperTest.php @@ -0,0 +1,47 @@ + + */ +class MatrixFieldHelperTest extends TestCase +{ + /** + * @dataProvider isEmptyProvider + */ + public function testIsEmpty(MatrixValue $value, bool $expected): void + { + $fh = new MatrixFieldHelper(); + $content = $this->createContentFromValue($value); + $this->assertSame($expected, $fh->isEmpty($content, 'matrix_field')); + } + + + public function isEmptyProvider(): array + { + return [ + [new MatrixValue(), true], + [new MatrixValue([]), true], + [new MatrixValue([new MatrixValue\Row(['x'])]), false], + ]; + } + + protected function createContentFromValue(MatrixValue $value): Content + { + $field = new Field(['value' => $value]); + + $content = $this->createMock(Content::class); + $content->method('getField')->with('matrix_field')->willReturn($field); + + return $content; + } +}