[TASK] Add helper function for array handling
authorSusanne Moog <typo3@susannemoog.de>
Sun, 17 Apr 2011 14:09:27 +0000 (16:09 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Mon, 25 Apr 2011 21:29:23 +0000 (23:29 +0200)
Adds a helper functions for easier handling of arrays.
New class t3lib_utility_Array adds a method to reduce an
array by a search value.
New method in t3lib_extMgm adds a method to add new items
to a TCA select list at a spcified position.

Change-Id: I589294525a7eacc7af6e54c5b78f5ee411574f12
Resolves: #26069
Relates: #22408
Reviewed-on: http://review.typo3.org/1647
Reviewed-by: Alexander Stehlik
Tested-by: Alexander Stehlik
Reviewed-by: Christian Kuhn
Tested-by: Christian Kuhn
t3lib/class.t3lib_extmgm.php
t3lib/core_autoload.php
t3lib/utility/class.t3lib_utility_array.php [new file with mode: 0644]
tests/t3lib/class.t3lib_extmgmTest.php
tests/t3lib/utility/class.t3lib_utility_arrayTest.php [new file with mode: 0644]

index 10dd894..dbbb7ae 100644 (file)
@@ -442,6 +442,109 @@ final class t3lib_extMgm {
        }
 
        /**
+        * Add an item to a select field item list.
+        *
+        * Warning: Do not use this method for radio or check types, especially not
+        * with $relativeToField and $relativePosition parameters. This would shift
+        * existing database data 'off by one'.
+        *
+        * As an example, this can be used to add an item to tt_content CType select
+        * drop-down after the existing 'mailform' field with these parameters:
+        * - $table = 'tt_content'
+        * - $field = 'CType'
+        * - $item = array(
+        *              'LLL:EXT:cms/locallang_ttc.xml:CType.I.10',
+        *              'login',
+        *              'i/tt_content_login.gif',
+        *      ),
+        * - $relativeToField = mailform
+        * - $relativePosition = after
+        *
+        * @throws InvalidArgumentException If given paramenters are not of correct
+        *              type or out of bounds
+        * @throws RuntimeException If reference to related position fields can not
+        *              be found or if select field is not defined
+        *
+        * @param string $table Name of TCA table
+        * @param string $field Name of TCA field
+        * @param array $item New item to add
+        * @param string $relativeToField Add item relative to existing field
+        * @param string $relativePosition Valid keywords: 'before', 'after'
+        *              or 'replace' to relativeToField field
+        */
+       public static function addTcaSelectItem($table, $field, array $item, $relativeToField = '', $relativePosition = '') {
+               if (!is_string($table)) {
+                       throw new InvalidArgumentException(
+                               'Given table is of type "' . gettype($table) . '" but a string is expected.',
+                               1303236963
+                       );
+               }
+               if (!is_string($field)) {
+                       throw new InvalidArgumentException(
+                               'Given field is of type "' . gettype($field) . '" but a string is expected.',
+                               1303236964
+                       );
+               }
+               if (!is_string($relativeToField)) {
+                       throw new InvalidArgumentException(
+                               'Given relative field is of type "' . gettype($relativeToField) . '" but a string is expected.',
+                               1303236965
+                       );
+               }
+               if (!is_string($relativePosition)) {
+                       throw new InvalidArgumentException(
+                               'Given relative position is of type "' . gettype($relativePosition) . '" but a string is expected.',
+                               1303236966
+                       );
+               }
+               if ($relativePosition !== '' && $relativePosition !== 'before' && $relativePosition !== 'after' && $relativePosition !== 'replace') {
+                       throw new InvalidArgumentException(
+                               'Relative position must be either empty or one of "before", "after", "replace".',
+                               1303236967
+                       );
+               }
+
+               t3lib_div::loadTCA($table);
+
+               if (!is_array($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'])) {
+                       throw new RuntimeException(
+                               'Given select field item list was not found.',
+                               1303237468
+                       );
+               }
+
+                       // Make sure item keys are integers
+               $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'] = array_values($GLOBALS['TCA'][$table]['columns'][$field]['config']['items']);
+
+               if (strlen($relativePosition) > 0) {
+                               // Insert at specified position
+                       $matchedPosition = t3lib_utility_Array::filterByValueRecursive(
+                               $relativeToField,
+                               $GLOBALS['TCA'][$table]['columns'][$field]['config']['items']
+                       );
+                       if (count($matchedPosition) > 0) {
+                               $relativeItemKey = key($matchedPosition);
+                               if ($relativePosition === 'replace') {
+                                       $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][$relativeItemKey] = $item;
+                               } else {
+                                       if ($relativePosition === 'before') {
+                                               $offset = $relativeItemKey;
+                                       } else {
+                                               $offset = $relativeItemKey + 1;
+                                       }
+                                       array_splice($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'], $offset, 0, array(0 => $item));
+                               }
+                       } else {
+                                       // Insert at new item at the end of the array if relative position was not found
+                               $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item;
+                       }
+               } else {
+                               // Insert at new item at the end of the array
+                       $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item;
+               }
+       }
+
+       /**
         * Adds a list of new fields to the TYPO3 USER SETTINGS configuration "showitem" list, the array with
         * the new fields itself needs to be added additionally to show up in the user setup, like
         * $GLOBALS['TYPO3_USER_SETTINGS']['columns'] += $tempColumns
index f9719e6..f259df3 100644 (file)
@@ -166,6 +166,7 @@ $t3libClasses = array(
        't3lib_utility_mail' => PATH_t3lib . 'utility/class.t3lib_utility_mail.php',
        't3lib_utility_phpoptions' => PATH_t3lib . 'utility/class.t3lib_utility_phpoptions.php',
        't3lib_utility_debug' => PATH_t3lib . 'utility/class.t3lib_utility_debug.php',
+       't3lib_utility_array' => PATH_t3lib . 'utility/class.t3lib_utility_array.php',
        't3lib_spritemanager' => PATH_t3lib . 'class.t3lib_spritemanager.php',
        't3lib_spritemanager_spritegenerator' => PATH_t3lib . 'spritemanager/class.t3lib_spritemanager_spritegenerator.php',
        't3lib_spritemanager_spriteicongenerator' => PATH_t3lib . 'interfaces/interface.t3lib_spritemanager_spriteicongenerator.php',
@@ -218,4 +219,4 @@ $t3libClasses = array(
 $tslibClasses = require(PATH_typo3 . 'sysext/cms/ext_autoload.php');
 
 return array_merge($t3libClasses, $tslibClasses);
-?>
\ No newline at end of file
+?>
diff --git a/t3lib/utility/class.t3lib_utility_array.php b/t3lib/utility/class.t3lib_utility_array.php
new file mode 100644 (file)
index 0000000..b0a9722
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2011 Susanne Moog <typo3@susanne-moog.de>
+ * All rights reserved
+ *
+ * This script is part of the TYPO3 project. The TYPO3 project is
+ * free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * The GNU General Public License can be found at
+ * http://www.gnu.org/copyleft/gpl.html.
+ * A copy is found in the textfile GPL.txt and important notices to the license
+ * from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ * This script is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+/**
+ * Class with helper functions for array handling
+ *
+ * @author Susanne Moog <typo3@susanne-moog.de>
+ * @package TYPO3
+ * @subpackage t3lib
+ */
+final class t3lib_utility_Array {
+
+       /**
+        * Reduce an array by a search value and keep the array structure.
+        *
+        * Comparison is type strict:
+        * - For a given needle of type string, integer, array or boolean,
+        * value and value type must match to occur in result array
+        * - For a given object, a object within the array must be a reference to
+        * the same object to match (not just different instance of same class)
+        *
+        * Example:
+        * - Needle: 'findMe'
+        * - Given array:
+        *      array(
+        *              'foo' => 'noMatch',
+        *              'bar' => 'findMe',
+        *              'foobar => array(
+        *                      'foo' => 'findMe',
+        *              ),
+        *      );
+        * - Result:
+        *      array(
+        *              'bar' => 'findMe',
+        *              'foobar' => array(
+        *                      'foo' => findMe',
+        *              ),
+        *      );
+        *
+        * See the unit tests for more examples and expected behaviour
+        *
+        * @static
+        * @param mixed $needle The value to search for
+        * @param array $haystack The array in which to search
+        * @return array $haystack array reduced matching $needle values
+        */
+       public static function filterByValueRecursive($needle = '', array $haystack = array()) {
+               $resultArray = array();
+
+                       // Define a lambda function to be applied to all members of this array dimension
+                       // Call recursive if current value is of type array
+                       // Write to $resultArray (by reference!) if types and value match
+               $callback = function(&$value, $key) use ($needle, &$resultArray) {
+                       if ($value === $needle) {
+                               $resultArray[$key] = $value;
+                       } elseif (is_array($value)) {
+                                       // self does not work in lambda functions, use t3lib_utility_Array for recursion
+                               $subArrayMatches = t3lib_utility_Array::filterByValueRecursive($needle, $value, $array[$key]);
+                               if (count($subArrayMatches) > 0) {
+                                       $resultArray[$key] = $subArrayMatches;
+                               }
+                       }
+               };
+
+                       // array_walk() is not affected by the internal pointers, no need to reset
+               array_walk($haystack, $callback);
+
+                       // Pointers to result array are reset internally
+               return $resultArray;
+       }
+}
+
+?>
\ No newline at end of file
index 1423663..261a8b1 100644 (file)
@@ -453,6 +453,145 @@ class t3lib_extmgmTest extends tx_phpunit_testcase {
 
 
        /////////////////////////////////////////
+       // Tests concerning addTcaSelectItem
+       /////////////////////////////////////////
+
+       /**
+        * @test
+        * @expectedException InvalidArgumentException
+        */
+       public function addTcaSelectItemThrowsExceptionIfTableIsNotOfTypeString() {
+               t3lib_extMgm::addTcaSelectItem(array(), 'foo', array());
+       }
+
+       /**
+        * @test
+        * @expectedException InvalidArgumentException
+        */
+       public function addTcaSelectItemThrowsExceptionIfFieldIsNotOfTypeString() {
+               t3lib_extMgm::addTcaSelectItem('foo', array(), array());
+       }
+
+       /**
+        * @test
+        * @expectedException InvalidArgumentException
+        */
+       public function addTcaSelectItemThrowsExceptionIfRelativeToFieldIsNotOfTypeString() {
+               t3lib_extMgm::addTcaSelectItem('foo', 'bar', array(), array());
+       }
+
+       /**
+        * @test
+        * @expectedException InvalidArgumentException
+        */
+       public function addTcaSelectItemThrowsExceptionIfRelativePositionIsNotOfTypeString() {
+               t3lib_extMgm::addTcaSelectItem('foo', 'bar', array(), 'foo', array());
+       }
+
+       /**
+        * @test
+        * @expectedException InvalidArgumentException
+        */
+       public function addTcaSelectItemThrowsExceptionIfRelativePositionIsNotOneOfValidKeywords() {
+               t3lib_extMgm::addTcaSelectItem('foo', 'bar', array(), 'foo', 'not allowed keyword');
+       }
+
+       /**
+        * @test
+        * @expectedException RuntimeException
+        */
+       public function addTcaSelectItemThrowsExceptionIfFieldIsNotFoundInTca() {
+               $GLOBALS['TCA'] = array();
+               t3lib_extMgm::addTcaSelectItem('foo', 'bar', array());
+       }
+
+       /**
+        * Data provider for addTcaSelectItemInsertsItemAtSpecifiedPosition
+        */
+       public function addTcaSelectItemDataProvider() {
+                       // Every array splits into:
+                       // - relativeToField
+                       // - relativePosition
+                       // - expectedResultArray
+               return array(
+                       'add at end of array' => array(
+                               '',
+                               '',
+                               array(
+                                       0 => array('firstElement'),
+                                       1 => array('matchMe'),
+                                       2 => array('thirdElement'),
+                                       3 => array('insertedElement'),
+                               ),
+                       ),
+                       'replace element' => array(
+                               'matchMe',
+                               'replace',
+                               array(
+                                       0 => array('firstElement'),
+                                       1 => array('insertedElement'),
+                                       2 => array('thirdElement'),
+                               ),
+                       ),
+                       'add element after' => array(
+                               'matchMe',
+                               'after',
+                               array(
+                                       0 => array('firstElement'),
+                                       1 => array('matchMe'),
+                                       2 => array('insertedElement'),
+                                       3 => array('thirdElement'),
+                               ),
+                       ),
+                       'add element before' => array(
+                               'matchMe',
+                               'before',
+                               array(
+                                       0 => array('firstElement'),
+                                       1 => array('insertedElement'),
+                                       2 => array('matchMe'),
+                                       3 => array('thirdElement'),
+                               ),
+                       ),
+                       'add at end if relative position was not found' => array(
+                               'notExistingItem',
+                               'after',
+                               array(
+                                       0 => array('firstElement'),
+                                       1 => array('matchMe'),
+                                       2 => array('thirdElement'),
+                                       3 => array('insertedElement'),
+                               ),
+                       ),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider addTcaSelectItemDataProvider
+        */
+       public function addTcaSelectItemInsertsItemAtSpecifiedPosition($relativeToField, $relativePosition, $expectedResultArray) {
+               $GLOBALS['TCA'] = array(
+                       'testTable' => array(
+                               'columns' => array(
+                                       'testField' => array(
+                                               'config' => array(
+                                                       'items' => array(
+                                                               '0' => array('firstElement'),
+                                                               '1' => array('matchMe'),
+                                                               2 => array('thirdElement'),
+                                                       ),
+                                               ),
+                                       ),
+                               ),
+                       ),
+               );
+               t3lib_extMgm::addTcaSelectItem('testTable', 'testField', array('insertedElement'), $relativeToField, $relativePosition);
+               $this->assertEquals($expectedResultArray, $GLOBALS['TCA']['testTable']['columns']['testField']['config']['items']);
+       }
+
+
+       /////////////////////////////////////////
        // Tests concerning getExtensionVersion
        /////////////////////////////////////////
 
diff --git a/tests/t3lib/utility/class.t3lib_utility_arrayTest.php b/tests/t3lib/utility/class.t3lib_utility_arrayTest.php
new file mode 100644 (file)
index 0000000..108deed
--- /dev/null
@@ -0,0 +1,188 @@
+<?php
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2011 Susanne Moog <typo3@susanne-moog.de>
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+/**
+ * Testcase for class t3lib_utility_array
+ *
+ * @author Susanne Moog <typo3@susanne-moog.de>
+ * @author Christian Kuhn <lolli@schwarzbu.ch>
+ *
+ * @package TYPO3
+ * @subpackage t3lib
+ */
+class t3lib_utility_ArrayTest extends tx_phpunit_testcase {
+
+       /**
+        * Data provider for filterByValueRecursiveCorrectlyFiltersArray
+        */
+       public function filterByValueRecursive() {
+                       // Every array splits into:
+                       // - String value to search for
+                       // - Input array
+                       // - Expected result array
+               return array(
+                       'empty search array' => array(
+                               'banana',
+                               array(),
+                               array(),
+                       ),
+                       'empty string as needle' => array(
+                               '',
+                               array(
+                                       '',
+                                       'apple',
+                               ),
+                               array(
+                                       '',
+                               ),
+                       ),
+                       'flat array searching for string' => array(
+                               'banana',
+                               array(
+                                       'apple',
+                                       'banana',
+                               ),
+                               array(
+                                       1 => 'banana',
+                               ),
+                       ),
+                       'flat array searching for string with two matches' => array(
+                               'banana',
+                               array(
+                                       'foo' => 'apple',
+                                       'firstbanana' => 'banana',
+                                       'secondbanana' => 'banana',
+                               ),
+                               array(
+                                       'firstbanana' => 'banana',
+                                       'secondbanana' => 'banana',
+                               ),
+                       ),
+                       'multi dimensional array searching for string with multiple matches' => array(
+                               'banana',
+                               array(
+                                       'foo' => 'apple',
+                                       'firstbanana' => 'banana',
+                                       'grape' => array(
+                                               'foo2' => 'apple2',
+                                               'secondbanana' => 'banana',
+                                               'foo3' => array(),
+                                       ),
+                                       'bar' => 'orange',
+                               ),
+                               array(
+                                       'firstbanana' => 'banana',
+                                       'grape' => array(
+                                               'secondbanana' => 'banana',
+                                       ),
+                               ),
+                       ),
+                       'multi dimensional array searching for integer with multiple matches' => array(
+                               42,
+                               array(
+                                       'foo' => 23,
+                                       'bar' => 42,
+                                       array(
+                                               'foo' => 23,
+                                               'bar' => 42,
+                                       ),
+                               ),
+                               array(
+                                       'bar' => 42,
+                                       array(
+                                               'bar' => 42,
+                                       ),
+                               ),
+                       ),
+                       'flat array searching for boolean TRUE' => array(
+                               TRUE,
+                               array(
+                                       23 => FALSE,
+                                       42 => TRUE,
+                               ),
+                               array(
+                                       42 => TRUE,
+                               ),
+                       ),
+                       'multi dimensional array searching for boolean FALSE' => array(
+                               FALSE,
+                               array(
+                                       23 => FALSE,
+                                       42 => TRUE,
+                                       'foo' => array(
+                                               23 => FALSE,
+                                               42 => TRUE,
+                                       ),
+                               ),
+                               array(
+                                       23 => FALSE,
+                                       'foo' => array(
+                                               23 => FALSE,
+                                       ),
+                               ),
+                       ),
+                       'flat array searching for array' => array(
+                               array(
+                                       'foo' => 'bar',
+                               ),
+                               array(
+                                       'foo' => 'bar',
+                                       'foobar' => array(
+                                               'foo' => 'bar',
+                                       ),
+                               ),
+                               array(
+                                       'foobar' => array(
+                                               'foo' => 'bar',
+                                       ),
+                               ),
+                       ),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider filterByValueRecursive
+        */
+       public function filterByValueRecursiveCorrectlyFiltersArray($needle, $haystack, $expectedResult) {
+               $this->assertEquals($expectedResult, t3lib_utility_Array::filterByValueRecursive($needle, $haystack));
+       }
+
+       /**
+        * @test
+        */
+       public function filterByValueRecursiveMatchesReferencesToSameObject() {
+               $instance = new stdClass();
+               $this->assertEquals(array($instance), t3lib_utility_Array::filterByValueRecursive($instance, array($instance)));
+       }
+
+       /**
+        * @test
+        */
+       public function filterByValueRecursiveDoesNotMatchDifferentInstancesOfSameClass() {
+               $this->assertEquals(array(), t3lib_utility_Array::filterByValueRecursive(new stdClass(), array(new stdClass())));
+       }
+}
+
+?>
\ No newline at end of file