[FEATURE] Add a Category Collection as part of the Category API
authorFabien Udriot <fabien.udriot@ecodev.ch>
Sat, 14 Jul 2012 13:14:43 +0000 (15:14 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Sun, 19 Aug 2012 13:42:05 +0000 (15:42 +0200)
Category should make use of the Collection API as a cornerstone for
fetching and storing records related to a category. The Abstract
Collection object implements various PHP Interfaces such as
Iterator, Serializable, Countable , etc... that the Collection
Category will inherit and make the developer happy.

Category Collection enables this code:

$categoryUid = 1;
$tableName = 'tt_content';
$collection = t3lib_category_Collection_CategoryCollection::load(
    $categoryUid, TRUE, $tableName);
echo $collection->count();

Change-Id: Ieac9ee0225595d01e539678284b18ecd35541138
Resolves: #38773
Releases: 6.0
Reviewed-on: http://review.typo3.org/12791
Reviewed-by: Susanne Moog
Tested-by: Susanne Moog
Reviewed-by: Fabien Udriot
Tested-by: Fabien Udriot
Reviewed-by: Oliver Hader
Reviewed-by: Christian Kuhn
Tested-by: Oliver Hader
t3lib/category/Collection/CategoryCollection.php [new file with mode: 0644]
t3lib/core_autoload.php
tests/Unit/t3lib/category/Collection/CategoryCollectionTest.php [new file with mode: 0644]

diff --git a/t3lib/category/Collection/CategoryCollection.php b/t3lib/category/Collection/CategoryCollection.php
new file mode 100644 (file)
index 0000000..1fedbff
--- /dev/null
@@ -0,0 +1,271 @@
+<?php
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2012 Fabien Udriot <fabien.udriot@typo3.org>
+ * 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!
+ ***************************************************************/
+
+/**
+ * Category Collection to handle records attached to a category
+ *
+ * @author Fabien Udriot <fabien.udriot@typo3.org>
+ * @package TYPO3
+ * @subpackage t3lib
+ */
+class t3lib_category_Collection_CategoryCollection
+       extends t3lib_collection_AbstractRecordCollection
+       implements t3lib_collection_Editable {
+
+       /**
+        * The table name collections are stored to
+        *
+        * @var string
+        */
+       protected static $storageTableName = 'sys_category';
+
+       /**
+        * Creates this object.
+        *
+        * @param string $tableName Name of the table to be working on
+        * @throws RuntimeException
+        */
+       public function __construct($tableName = NULL) {
+               parent::__construct();
+
+               if (!empty($tableName)) {
+                       $this->setItemTableName($tableName);
+               } elseif (empty($this->itemTableName)) {
+                       throw new RuntimeException(
+                               't3lib_category_Collection_CategoryCollection needs a valid itemTableName.',
+                               1341826168
+                       );
+               }
+       }
+
+       /**
+        * Creates a new collection objects and reconstitutes the
+        * given database record to the new object.
+        *
+        * @param array $collectionRecord Database record
+        * @param boolean $fillItems Populates the entries directly on load, might be bad for memory on large collections
+        * @return t3lib_category_Collection_CategoryCollection
+        */
+       public static function create(array $collectionRecord, $fillItems = FALSE) {
+               /** @var $collection t3lib_category_Collection_CategoryCollection */
+               $collection = t3lib_div::makeInstance(
+                       't3lib_category_Collection_CategoryCollection',
+                       $collectionRecord['table_name']
+               );
+
+               $collection->fromArray($collectionRecord);
+
+               if ($fillItems) {
+                       $collection->loadContents();
+               }
+
+               return $collection;
+       }
+
+       /**
+        * Loads the collections with the given id from persistence
+        * For memory reasons, per default only f.e. title, database-table,
+        * identifier (what ever static data is defined) is loaded.
+        * Entries can be load on first access.
+        *
+        * @param integer $id Id of database record to be loaded
+        * @param boolean $fillItems Populates the entries directly on load, might be bad for memory on large collections
+        * @param string $tableName the table name
+        * @return t3lib_collection_Collection
+        */
+       public static function load($id, $fillItems = FALSE, $tableName = '') {
+               t3lib_div::loadTCA(static::$storageTableName);
+
+               $collectionRecord = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
+                       '*',
+                       static::$storageTableName,
+                       'uid=' . intval($id) . t3lib_BEfunc::deleteClause(static::$storageTableName)
+               );
+
+               $collectionRecord['table_name'] = $tableName;
+
+               return self::create($collectionRecord, $fillItems);
+       }
+
+       /**
+        * Gets the collected records in this collection, by
+        * looking up the MM relations of this record to the
+        * table name defined in the local field 'table_name'.
+        *
+        * @return array
+        */
+       protected function getCollectedRecords() {
+               $relatedRecords = array();
+
+               /** @var $GLOBALS['TYPO3_DB'] t3lib_DB */
+               $resource = $this->getDatabase()->exec_SELECT_mm_query(
+                       $this->getItemTableName() . '.*',
+                       self::$storageTableName,
+                       'sys_category_record_mm',
+                       $this->getItemTableName(),
+                       'AND ' . self::$storageTableName . '.uid=' . intval($this->getIdentifier())
+               );
+
+               if ($resource) {
+                       while ($record = $this->getDatabase()->sql_fetch_assoc($resource)) {
+                               $relatedRecords[] = $record;
+                       }
+                       $this->getDatabase()->sql_free_result($resource);
+               }
+
+               return $relatedRecords;
+       }
+
+       /**
+        * Populates the content-entries of the storage
+        * Queries the underlying storage for entries of the collection
+        * and adds them to the collection data.
+        * If the content entries of the storage had not been loaded on creation
+        * ($fillItems = false) this function is to be used for loading the contents
+        * afterwards.
+        *
+        * @return void
+        */
+       public function loadContents() {
+               $entries = $this->getCollectedRecords();
+               $this->removeAll();
+
+               foreach ($entries as $entry) {
+                       $this->add($entry);
+               }
+       }
+
+       /**
+        * Returns an array of the persistable properties and contents
+        * which are processable by TCEmain.
+        * for internal usage in persist only.
+        *
+        * @return array
+        */
+       protected function getPersistableDataArray() {
+               return array(
+                       'title' => $this->getTitle(),
+                       'description' => $this->getDescription(),
+                       'items' => $this->getItemUidList(TRUE),
+               );
+       }
+
+       /**
+        * Adds on entry to the collection
+        *
+        * @param mixed $data
+        * @return void
+        */
+       public function add($data) {
+               $this->storage->push($data);
+       }
+
+       /**
+        * Adds a set of entries to the collection
+        *
+        * @param t3lib_collection_Collection $other
+        * @return void
+        */
+       public function addAll(t3lib_collection_Collection $other) {
+               foreach ($other as $value) {
+                       $this->add($value);
+               }
+       }
+
+       /**
+        * Removes the given entry from collection
+        * Note: not the given "index"
+        *
+        * @param mixed $data
+        * @return void
+        */
+       public function remove($data) {
+               $offset = 0;
+               foreach ($this->storage as $value) {
+                       if ($value == $data) {
+                               break;
+                       }
+                       $offset++;
+               }
+               $this->storage->offsetUnset($offset);
+       }
+
+       /**
+        * Removes all entries from the collection
+        * collection will be empty afterwards
+        *
+        * @return void
+        */
+       public function removeAll() {
+               $this->storage = new SplDoublyLinkedList();
+       }
+
+       /**
+        * Gets the current available items.
+        *
+        * @return array
+        */
+       public function getItems() {
+               $itemArray = array();
+
+               /** @var $item t3lib_file_File */
+               foreach ($this->storage as $item) {
+                       $itemArray[] = $item;
+               }
+
+               return $itemArray;
+       }
+
+       /**
+        * Getter for the storage table name
+        *
+        * @return string
+        */
+       public static function getStorageTableName() {
+               return self::$storageTableName;
+       }
+
+       /**
+        * Getter for the storage items field
+        *
+        * @return string
+        */
+       public static function getStorageItemsField() {
+               return self::$storageItemsField;
+       }
+
+       /**
+        * Gets the database object.
+        *
+        * @return t3lib_DB
+        */
+       protected function getDatabase() {
+               return $GLOBALS['TYPO3_DB'];
+       }
+}
+
+?>
\ No newline at end of file
index 4eb0015..c1b32a2 100644 (file)
@@ -46,6 +46,7 @@ $t3libClasses = array(
        't3lib_cache_manager' => PATH_t3lib . 'cache/class.t3lib_cache_manager.php',
        't3lib_cachehash' => PATH_t3lib . 'class.t3lib_cacheHash.php',
        't3lib_category_registry' => PATH_t3lib . 'category/Registry.php',
+       't3lib_category_collection_categorycollection' => PATH_t3lib . 'category/Collection/CategoryCollection.php',
        't3lib_cli' => PATH_t3lib . 'class.t3lib_cli.php',
        't3lib_clipboard' => PATH_t3lib . 'class.t3lib_clipboard.php',
        't3lib_collection_abstractrecordcollection' => PATH_t3lib . 'collection/AbstractRecordCollection.php',
diff --git a/tests/Unit/t3lib/category/Collection/CategoryCollectionTest.php b/tests/Unit/t3lib/category/Collection/CategoryCollectionTest.php
new file mode 100644 (file)
index 0000000..f7c7726
--- /dev/null
@@ -0,0 +1,337 @@
+<?php
+/**
+ * Test case for t3lib_category_CategoryCollection
+ *
+ * @package TYPO3
+ * @subpackage t3lib
+ * @author Fabine Udriot <fabien.udriot@typo3.org>
+ */
+class t3lib_category_CategoryCollectionTest extends Tx_Phpunit_TestCase {
+
+       /**
+        * @var t3lib_category_Collection_CategoryCollection
+        */
+       private $fixture;
+
+       /**
+        * @var string
+        */
+       private $tableName = 'tx_foo_5001615c50bed';
+
+       /**
+        * @var array
+        */
+       private $tables = array('sys_category', 'sys_category_record_mm');
+
+       /**
+        * @var int
+        */
+       private $categoryUid = 0;
+
+       /**
+        * @var array
+        */
+       private $collectionRecord = array();
+
+       /**
+        * @var Tx_Phpunit_Framework
+        */
+       private $testingFramework;
+
+       /**
+        * @var t3lib_DB
+        */
+       private $database;
+
+       /**
+        * Sets up this test suite.
+        *
+        * @return void
+        */
+       public function setUp() {
+
+               $this->database = $GLOBALS['TYPO3_DB'];
+
+               $this->fixture = new t3lib_category_Collection_CategoryCollection($this->tableName);
+
+               $this->collectionRecord = array(
+                       'uid' => 0,
+                       'title' => uniqid('title'),
+                       'description' => uniqid('description'),
+                       'table_name' => 'content'
+               );
+
+               $GLOBALS['TCA'][$this->tableName] = array('ctrl' => array());
+
+                       // prepare environment
+               $this->createDummyTable();
+               $this->testingFramework = new Tx_Phpunit_Framework('sys_category', array('tx_foo'));
+               $this->populateDummyTable();
+               $this->prepareTables();
+               $this->makeRelationBetweenCategoryAndDummyTable();
+       }
+
+       /**
+        * Tears down this test suite.
+        *
+        * @return void
+        */
+       public function tearDown() {
+
+               $this->testingFramework->cleanUp();
+
+                       // clean up environment
+               $this->dropDummyTable();
+               $this->dropDummyField();
+
+               unset($this->testingFramework);
+               unset($this->collectionRecord);
+               unset($this->fixture);
+               unset($this->database);
+       }
+
+       /**
+        * @test
+        * @expectedException RuntimeException
+        * @covers t3lib_category_Collection_CategoryCollection::__construct
+        * @return void
+        */
+       public function missingTableNameArgumentForObjectCategoryCollection() {
+               new t3lib_category_Collection_CategoryCollection();
+       }
+
+       /**
+        * @test
+        * @covers t3lib_category_Collection_CategoryCollection::fromArray
+        * @return void
+        */
+       public function checkIfFromArrayMethodSetCorrectProperties() {
+               $this->fixture->fromArray($this->collectionRecord);
+
+               $this->assertEquals($this->collectionRecord['uid'], $this->fixture->getIdentifier());
+               $this->assertEquals($this->collectionRecord['uid'], $this->fixture->getUid());
+               $this->assertEquals($this->collectionRecord['title'], $this->fixture->getTitle());
+               $this->assertEquals($this->collectionRecord['description'], $this->fixture->getDescription());
+               $this->assertEquals($this->collectionRecord['table_name'], $this->fixture->getItemTableName());
+       }
+
+       /**
+        * @test
+        * @covers t3lib_category_Collection_CategoryCollection::create
+        * @return void
+        */
+       public function canCreateDummyCollection() {
+               $collection = t3lib_category_Collection_CategoryCollection::create($this->collectionRecord);
+               $this->assertInstanceOf('t3lib_category_collection_categorycollection', $collection);
+       }
+
+       /**
+        * @test
+        * @covers t3lib_category_Collection_CategoryCollection::create
+        * @return void
+        */
+       public function canCreateDummyCollectionAndFillItems() {
+               $collection = t3lib_category_Collection_CategoryCollection::create($this->collectionRecord, TRUE);
+               $this->assertInstanceOf('t3lib_category_collection_categorycollection', $collection);
+       }
+
+       /**
+        * @test
+        * @covers t3lib_category_Collection_CategoryCollection::getCollectedRecords
+        * @return void
+        */
+       public function getCollectedRecordsReturnsEmptyRecordSet() {
+               $method = new ReflectionMethod(
+                       't3lib_category_Collection_CategoryCollection', 'getCollectedRecords'
+               );
+
+               $method->setAccessible(TRUE);
+               $records = $method->invoke($this->fixture);
+               $this->assertInternalType('array', $records);
+               $this->assertEmpty($records);
+       }
+
+       /**
+        * @test
+        * @covers t3lib_category_Collection_CategoryCollection::getStorageTableName
+        * @return void
+        */
+       public function isStorageTableNameEqualsToSysCategory() {
+               $this->assertEquals('sys_category', t3lib_category_Collection_CategoryCollection::getStorageTableName());
+       }
+
+       /**
+        * @test
+        * @covers t3lib_category_Collection_CategoryCollection::getStorageItemsField
+        * @return void
+        */
+       public function isStorageItemsFieldEqualsToItems() {
+               $this->assertEquals('items', t3lib_category_Collection_CategoryCollection::getStorageItemsField());
+       }
+
+       /**
+        * @test
+        * @return void
+        */
+       public function canLoadADummyCollectionFromDatabase() {
+
+               /** @var $collection t3lib_category_Collection_CategoryCollection */
+               $collection = t3lib_category_Collection_CategoryCollection::load($this->categoryUid, TRUE, $this->tableName);
+
+                       // Check the number of record
+               $this->assertEquals($this->numberOfRecords, $collection->count());
+
+                       // Check that the first record is the one expected
+               $record = $this->database->exec_SELECTgetSingleRow('*', $this->tableName, 'uid=1');
+               $collection->rewind();
+               $this->assertEquals($record, $collection->current());
+
+                       // Add a new record
+               $fakeRecord = array(
+                       'uid' => $this->numberOfRecords + 1,
+                       'pid' => 0,
+                       'title' => uniqid('title'),
+                       'categories' => 0
+               );
+
+                       // Check the number of records
+               $collection->add($fakeRecord);
+               $this->assertEquals($this->numberOfRecords + 1, $collection->count());
+       }
+
+       /**
+        * @test
+        * @return void
+        */
+       public function canLoadADummyCollectionFromDatabaseAndAddRecord() {
+               $collection = t3lib_category_Collection_CategoryCollection::load($this->categoryUid, TRUE, $this->tableName);
+
+                       // Add a new record
+               $fakeRecord = array(
+                       'uid' => $this->numberOfRecords + 1,
+                       'pid' => 0,
+                       'title' => uniqid('title'),
+                       'categories' => 0
+               );
+
+                       // Check the number of records
+               $collection->add($fakeRecord);
+               $this->assertEquals($this->numberOfRecords + 1, $collection->count());
+       }
+
+       /**
+        * @test
+        * @return void
+        */
+       public function canLoadADummyCollectionWithoutContentFromDatabase() {
+
+               /** @var $collection t3lib_category_Collection_CategoryCollection */
+               $collection = t3lib_category_Collection_CategoryCollection::load($this->categoryUid, FALSE, $this->tableName);
+
+                       // Check the number of record
+               $this->assertEquals(0, $collection->count());
+       }
+
+       /********************/
+       /* INTERNAL METHODS */
+       /********************/
+
+       /**
+        * Create dummy table for testing purpose
+        *
+        * @return void
+        */
+       private function populateDummyTable() {
+               $this->numberOfRecords = 5;
+               for ($index = 1; $index <= $this->numberOfRecords; $index++) {
+                       $values = array(
+                               'title' => uniqid('title'),
+                       );
+                       $this->testingFramework->createRecord($this->tableName, $values);
+               }
+       }
+
+       /**
+        * Make relation between tables
+        *
+        * @return void
+        */
+       private function makeRelationBetweenCategoryAndDummyTable() {
+
+               for ($index = 1; $index <= $this->numberOfRecords; $index++) {
+
+                       $values = array(
+                               'uid_local' => $this->categoryUid,
+                               'uid_foreign' => $index,
+                               'tablenames' => $this->tableName
+                       );
+
+                       $this->testingFramework->createRecord('sys_category_record_mm', $values);
+               }
+       }
+
+       /**
+        * Create dummy table for testing purpose
+        *
+        * @return void
+        */
+       private function createDummyTable() {
+               $sql = <<<EOF
+CREATE TABLE {$this->tableName} (
+       uid int(11) auto_increment,
+       pid int(11) unsigned DEFAULT '0' NOT NULL,
+    title tinytext,
+       categories int(11) unsigned DEFAULT '0' NOT NULL,
+       sys_category_is_dummy_record int(11) unsigned DEFAULT '0' NOT NULL,
+
+    PRIMARY KEY (uid)
+);
+EOF;
+               $this->database->sql_query($sql);
+       }
+
+       /**
+     * Drop dummy table
+        *
+        * @return void
+        */
+       private function dropDummyTable() {
+               $sql = 'DROP TABLE ' . $this->tableName . ';';
+               $this->database->sql_query($sql);
+       }
+
+       /**
+        * Add is_dummy_record record and create dummy record
+        *
+        * @return void
+        */
+       private function prepareTables() {
+               $sql = 'ALTER TABLE %s ADD is_dummy_record tinyint(1) unsigned DEFAULT \'0\' NOT NULL';
+
+               foreach ($this->tables as $table) {
+                       $_sql = sprintf($sql, $table);
+                       $this->database->sql_query($_sql);
+               }
+
+               $values = array(
+                       'title' => uniqid('title'),
+                       'is_dummy_record' => 1,
+               );
+               $this->categoryUid = $this->testingFramework->createRecord('sys_category', $values);
+       }
+
+       /**
+        * Remove dummy record and drop field
+        *
+        * @return void
+        */
+       private function dropDummyField() {
+               $sql = 'ALTER TABLE %s DROP COLUMN is_dummy_record';
+               foreach ($this->tables as $table) {
+                       $_sql = sprintf($sql, $table);
+                       $this->database->sql_query($_sql);
+               }
+       }
+}
+
+?>
\ No newline at end of file