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('
Fred | Blue | Small | ', $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' => '5 | Max | 30 | ',
+ ],
+ 'reorder example #2' => [
+ 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'],
+ 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5],
+ 'expectContainsString' => '5 | 30 | Fred | ',
+ ],
+ '2 col heading, 3 col data row' => [
+ 'heading' => ['id' => 'ID', 'name' => 'Name'],
+ 'row' => ['name' => 'Fred', 'age' => 30, 'id' => 5],
+ 'expectContainsString' => '5 | Fred | ',
+ ],
+ '3 col heading, 2 col data row' => [
+ 'heading' => ['id' => 'ID', 'age' => 'Age', 'name' => 'Name'],
+ 'row' => ['name' => 'Fred', 'id' => 5],
+ 'expectContainsString' => '5 | | Fred | ',
+ ],
+ ];
+ }
}
// 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();
+?>
+
+
+
+
+
+ | Name |
+ Color |
+ Size |
+
+
+
+
+ | Fred |
+ Blue |
+ Small |
+
+
+ | Mary |
+ |
+ Large |
+
+
+ |
+ 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);