diff --git a/system/View/Table.php b/system/View/Table.php index a7ded4fd5cb7..b66a18c338e6 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -83,6 +83,11 @@ class Table */ public $function; + /** + * Order each inserted row by heading keys + */ + private bool $syncRowsWithHeading = false; + /** * Set the template from the table config file if it exists * @@ -161,7 +166,8 @@ public function makeColumns($array = [], $columnLimit = 0) // Turn off the auto-heading feature since it's doubtful we // will want headings from a one-dimensional array - $this->autoHeading = false; + $this->autoHeading = false; + $this->syncRowsWithHeading = false; if ($columnLimit === 0) { return $array; @@ -207,7 +213,40 @@ public function setEmpty($value) */ public function addRow() { - $this->rows[] = $this->_prepArgs(func_get_args()); + $tmpRow = $this->_prepArgs(func_get_args()); + + if ($this->syncRowsWithHeading && ! empty($this->heading)) { + // each key has an index + $keyIndex = array_flip(array_keys($this->heading)); + + // figure out which keys need to be added + $missingKeys = array_diff_key($keyIndex, $tmpRow); + + // Remove all keys which don't exist in $keyIndex + $tmpRow = array_filter($tmpRow, static fn ($k) => array_key_exists($k, $keyIndex), ARRAY_FILTER_USE_KEY); + + // add missing keys to row, but use $this->emptyCells + $tmpRow = array_merge($tmpRow, array_map(fn ($v) => ['data' => $this->emptyCells], $missingKeys)); + + // order keys by $keyIndex values + uksort($tmpRow, static fn ($k1, $k2) => $keyIndex[$k1] <=> $keyIndex[$k2]); + } + $this->rows[] = $tmpRow; + + return $this; + } + + /** + * Set to true if each row column should be synced by keys defined in heading. + * + * If a row has a key which does not exist in heading, it will be filtered out + * If a row does not have a key which exists in heading, the field will stay empty + * + * @return $this + */ + public function setSyncRowsWithHeading(bool $orderByKey) + { + $this->syncRowsWithHeading = $orderByKey; return $this; } @@ -436,7 +475,7 @@ protected function _setFromArray($data) } foreach ($data as &$row) { - $this->rows[] = $this->_prepArgs($row); + $this->addRow($row); } } diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index 6e56d2a3fe22..2895f31df037 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -763,6 +763,59 @@ public function testInvalidCallback() $this->assertStringContainsString('FredBlueSmall', $generated); } + + /** + * @dataProvider orderedColumnUsecases + */ + public function testAddRowAndGenerateOrderedColumns(array $heading, array $row, string $expectContainsString): void + { + $this->table->setHeading($heading); + $this->table->setSyncRowsWithHeading(true); + $this->table->addRow($row); + + $generated = $this->table->generate(); + + $this->assertStringContainsString($expectContainsString, $generated); + } + + /** + * @dataProvider orderedColumnUsecases + */ + public function testGenerateOrderedColumns(array $heading, array $row, string $expectContainsString): void + { + $this->table->setHeading($heading); + $this->table->setSyncRowsWithHeading(true); + + $generated = $this->table->generate([$row]); + + $this->assertStringContainsString($expectContainsString, $generated); + } + + public function orderedColumnUsecases(): iterable + { + yield from [ + 'reorder example #1' => [ + 'heading' => ['id' => 'ID', 'name' => 'Name', 'age' => 'Age'], + 'row' => ['name' => 'Max', 'age' => 30, 'id' => 5], + 'expectContainsString' => '5Max30', + ], + 'reorder example #2' => [ + 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], + 'expectContainsString' => '530Fred', + ], + '2 col heading, 3 col data row' => [ + 'heading' => ['id' => 'ID', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5], + 'expectContainsString' => '5Fred', + ], + '3 col heading, 2 col data row' => [ + 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'], + 'row' => ['name' => 'Fred', 'id' => 5], + 'expectContainsString' => '5Fred', + ], + ]; + } } // We need this for the _set_from_db_result() test diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index f06814ab1677..51ad4fe229ab 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -91,6 +91,7 @@ Others See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. +- **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. Message Changes diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst index 0ccf8da0904d..bec15f86dbbf 100644 --- a/user_guide_src/source/outgoing/table.rst +++ b/user_guide_src/source/outgoing/table.rst @@ -71,6 +71,34 @@ to the Table constructor: .. literalinclude:: table/008.php +.. _table-sync-rows-with-headings: + +Synchronizing Rows with Headings +================================ + +.. versionadded:: 4.4.0 + +The ``setSyncRowsWithHeading(true)`` method enables that each data value +is placed in the same column as defined in ``setHeading()`` if an +associative array was used as parameter. This is especially useful +when dealing with data loaded via REST API where the order is not to +your liking, or if the API returned too much data. + +If a data row contains a key that is not present in the heading, its value is +filtered. Conversely, if a data row does not have a key listed in the heading, +an empty cell will be placed in its place. + +.. literalinclude:: table/019.php + +.. important:: You must call ``setSyncRowsWithHeading(true)`` and + ``setHeading([...])`` before adding any rows via ``addRow([...])`` where + the rearrangement of columns takes place. + +Using an array as input to ``generate()`` produces the same result: + +.. literalinclude:: table/020.php + + *************** Class Reference *************** @@ -188,3 +216,12 @@ Class Reference Example .. literalinclude:: table/018.php + + .. php:method:: setSyncRowsWithHeading(bool $orderByKey) + + :returns: Table instance (method chaining) + :rtype: Table + + Enables each row data key to be ordered by heading keys. This gives + more control of data being displaced in the correct column. Make + sure to set this value before calling the first ``addRow()`` method. diff --git a/user_guide_src/source/outgoing/table/019.php b/user_guide_src/source/outgoing/table/019.php new file mode 100644 index 000000000000..5478867fddfa --- /dev/null +++ b/user_guide_src/source/outgoing/table/019.php @@ -0,0 +1,40 @@ +setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) + ->setSyncRowsWithHeading(true) + ->addRow(['color' => 'Blue', 'name' => 'Fred', 'size' => 'Small']) + ->addRow(['size' => 'Large', 'age' => '24', 'name' => 'Mary']) + ->addRow(['color' => 'Green']); + +echo $table->generate(); +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameColorSize
FredBlueSmall
MaryLarge
Green
diff --git a/user_guide_src/source/outgoing/table/020.php b/user_guide_src/source/outgoing/table/020.php new file mode 100644 index 000000000000..42bda4d2f3bc --- /dev/null +++ b/user_guide_src/source/outgoing/table/020.php @@ -0,0 +1,24 @@ + 'Blue', + 'name' => 'Fred', + 'size' => 'Small', + ], + [ + 'size' => 'Large', + 'age' => '24', + 'name' => 'Mary', + ], + [ + 'color' => 'Green', + ], +]; + +$table = new \CodeIgniter\View\Table(); + +$table->setHeading(['name' => 'Name', 'color' => 'Color', 'size' => 'Size']) + ->setSyncRowsWithHeading(true); + +echo $table->generate($data);