[FEATURE] Add categorization into the Core
authorFabien Udriot <fabien.udriot@ecodev.ch>
Fri, 6 Jul 2012 12:34:54 +0000 (14:34 +0200)
committerSteffen Ritter <info@rs-websystems.de>
Fri, 13 Jul 2012 10:56:55 +0000 (12:56 +0200)
A common use case in every advanced website is to be able to
categorize records. Besides that, there is the need to share
categories across records. So far, each extension has to bring
its own category implementation which is not an ideal situation.

To fill the gap, we would like to introduce a new category record
type along with an API where extension developers could register
their own tables to be categorized. The relations will be stored
within "mm" tables as a less time consuming approach.

This patch provides:

* SQL definition for "sys_category" and "sys_category_mm" with
  their TCA
* a registration mechanism where third party extension can have
  their SQL fields + TCA generated on the fly. This is done by
  using the Extension Manager method:

t3lib_extMgm::makeCategorizable(
  $extensionKey, $tableName,
  $fieldName = 'categories', $options = array()
);

Change-Id: I461252b6d5f6c6c4a4eb2c1942a66250cbb95aa9
Resolves: #38711
Releases: 6.0
Reviewed-on: http://review.typo3.org/12672
Reviewed-by: Fabien Udriot
Tested-by: Fabien Udriot
Reviewed-by: Marcus Schwemer
Tested-by: Marcus Schwemer
Reviewed-by: Dominik Mathern
Tested-by: Dominik Mathern
Reviewed-by: Steffen Ritter
Tested-by: Steffen Ritter
14 files changed:
t3lib/category/Registry.php [new file with mode: 0644]
t3lib/class.t3lib_extmgm.php
t3lib/core_autoload.php
t3lib/stddb/tables.php
t3lib/stddb/tables.sql
t3lib/stddb/tca_sys_category.php [new file with mode: 0644]
tests/Unit/t3lib/category/RegistryTest.php [new file with mode: 0644]
tests/Unit/t3lib/class.t3lib_extmgmTest.php
typo3/sysext/em/classes/install/class.tx_em_install.php
typo3/sysext/install/mod/class.tx_install.php
typo3/sysext/lang/locallang_tca.xlf
typo3/sysext/t3skin/images/icons/mimetypes/x-sys_category.png [new file with mode: 0644]
typo3/sysext/t3skin/images/sprites/t3skin.png
typo3/sysext/t3skin/stylesheets/sprites/t3skin.css

diff --git a/t3lib/category/Registry.php b/t3lib/category/Registry.php
new file mode 100644 (file)
index 0000000..0962b4d
--- /dev/null
@@ -0,0 +1,157 @@
+<?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!
+ ***************************************************************/
+
+/**
+ * Class to register category configurations.
+ *
+ * @author Fabien Udriot <fabien.udriot@typo3.org>
+ * @author Oliver Hader <oliver.hader@typo3.org>
+ * @package TYPO3
+ * @subpackage t3lib
+ */
+class t3lib_category_Registry implements t3lib_Singleton {
+       /**
+        * @var array
+        */
+       protected $registry = array();
+
+       /**
+        * @var string
+        */
+       protected $template = '';
+
+       /**
+        * Returns a class instance
+        *
+        * @return t3lib_category_Registry
+        */
+       public static function getInstance() {
+               return t3lib_div::makeInstance('t3lib_category_Registry');
+       }
+
+       /**
+        * Creates this object.
+        */
+       public function __construct() {
+               $this->template = str_repeat(PHP_EOL, 3) . 'CREATE TABLE %s (' . PHP_EOL .
+                       '  %s int(11) DEFAULT \'0\' NOT NULL' . PHP_EOL .
+                       ');' . str_repeat(PHP_EOL, 3);
+       }
+
+       /**
+        * Adds a new category configuration to this registry.
+        *
+        * @param string $extensionKey Extension key to be used
+        * @param string $tableName Name of the table to be registered
+        * @param string $fieldName Name of the field to be registered
+        * @return boolean Whether fieldName of tableName is registered
+        */
+       public function add($extensionKey, $tableName, $fieldName) {
+               $result = FALSE;
+
+                       // Makes sure there is an existing table configuration and nothing registered yet:
+               if (!empty($GLOBALS['TCA'][$tableName])) {
+                       if (!$this->isRegistered($tableName, $fieldName)) {
+                               $this->registry[$extensionKey][$tableName] = $fieldName;
+                       }
+
+                       $result = TRUE;
+               }
+
+               return $result;
+       }
+
+       /**
+        * Gets the registered category configurations.
+        *
+        * @return array
+        */
+       public function get() {
+               return $this->registry;
+       }
+
+       /**
+        * Gets all extension keys that registered a category configuration.
+        *
+        * @return array
+        */
+       public function getExtensionKeys() {
+               return array_keys($this->registry);
+       }
+
+       /**
+        * Tells whether a table has a category configuration in the registry.
+        *
+        * @param string $tableName Name of the table to be looked up
+        * @param string $fieldName Name of the field to be looked up
+        * @return boolean
+        */
+       public function isRegistered($tableName, $fieldName) {
+               $isRegistered = FALSE;
+
+               foreach ($this->registry as $configuration) {
+                       if (!empty($configuration[$tableName]) && $configuration[$tableName] === $fieldName) {
+                               $isRegistered = TRUE;
+                               break;
+                       }
+               }
+
+               return $isRegistered;
+       }
+
+       /**
+        * Generates tables definitions for all registered tables.
+        *
+        * @return string
+        */
+       public function getDatabaseTableDefinitions() {
+               $sql = '';
+
+               foreach ($this->getExtensionKeys() as $extensionKey) {
+                       $sql .= $this->getDatabaseTableDefinition($extensionKey);
+               }
+
+               return $sql;
+       }
+
+       /**
+        * Generates table definitions for registered tables by an extension.
+        *
+        * @param string $extensionKey Extension key to have the database definitions created for
+        * @return string
+        */
+       public function getDatabaseTableDefinition($extensionKey) {
+               $sql = '';
+
+               foreach ($this->registry[$extensionKey] as $tableName => $fieldName) {
+                       $sql .= sprintf($this->template, $tableName, $fieldName);
+               }
+
+               return $sql;
+       }
+}
+?>
\ No newline at end of file
index 4fbaa57..379a67e 100644 (file)
@@ -2047,6 +2047,116 @@ tt_content.' . $key . $prefix . ' {
 
                return $ignoredExtensionList;
        }
+
+       /**
+        * Makes a table categorizable by extending its TCA.
+        *
+        * @param string $extensionKey Extension key to be used
+        * @param string $tableName Name of the table to be categoriezed
+        * @param string $fieldName Name of the field to be used to store categories
+        * @param array $options Additional configuration options
+        *              + fieldList: field configuration to be added to showitems
+        *              + typesList: list of types that shall visualize the categories field
+        *              + position: insert position of the categories field
+        *              + fieldConfiguration: TCA field config array to override defaults
+        * @see addTCAcolumns
+        * @see addToAllTCAtypes
+        */
+       public static function makeCategorizable($extensionKey, $tableName, $fieldName = 'categories', array $options = array())  {
+                       // Load TCA first
+               t3lib_div::loadTCA($tableName);
+
+                       // Update the category registry
+               $result = t3lib_category_Registry::getInstance()->add($extensionKey, $tableName, $fieldName);
+
+               if ($result === FALSE) {
+                       $message = 't3lib_categoryRegistry: no category registered for table "%s". Double check if there is a TCA configured';
+                       t3lib_div::devLog(sprintf($message, $tableName), 'Core', 2);
+               }
+
+                       // Makes sure to add more TCA to an existing structure
+               if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
+                               // Forges a new field, default name is "categories"
+                       $fieldConfiguration = array(
+                               'type' => 'select',
+                               'foreign_table' => 'sys_category',
+                               'foreign_table_where' => ' ORDER BY sys_category.title ASC',
+                               'MM' => 'sys_category_record_mm',
+                               'MM_opposite_field' => 'items',
+                               'MM_match_fields' => array('tablenames' => $tableName),
+                               'size' => 10,
+                               'autoSizeMax' => 50,
+                               'maxitems' => 9999,
+                               'renderMode' => 'tree',
+                               'treeConfig' => array(
+                                       'parentField' => 'parent',
+                                       'appearance' => array(
+                                               'expandAll' => TRUE,
+                                               'showHeader' => TRUE,
+                                       ),
+                               ),
+                               'wizards' => array(
+                                       '_PADDING' => 1,
+                                       '_VERTICAL' => 1,
+                                       'edit' => array(
+                                               'type' => 'popup',
+                                               'title' => 'Edit',
+                                               'script' => 'wizard_edit.php',
+                                               'icon' => 'edit2.gif',
+                                               'popup_onlyOpenIfSelected' => 1,
+                                               'JSopenParams' => 'height=350,width=580,status=0,menubar=0,scrollbars=1',
+                                       ),
+                                       'add' => Array(
+                                               'type' => 'script',
+                                               'title' => 'Create new',
+                                               'icon' => 'add.gif',
+                                               'params' => array(
+                                                       'table' => 'sys_category',
+                                                       'pid' => '###CURRENT_PID###',
+                                                       'setValue' => 'prepend'
+                                               ),
+                                               'script' => 'wizard_add.php',
+                                       ),
+                               ),
+                       );
+
+                       if (!empty($options['fieldConfiguration'])) {
+                               $fieldConfiguration = t3lib_div::array_merge_recursive_overrule(
+                                       $fieldConfiguration,
+                                       $options['fieldConfiguration']
+                               );
+                       }
+
+                       $columns = array(
+                               $fieldName => array(
+                                       'exclude' => 0,
+                                       'label' => 'LLL:EXT:lang/locallang_tca.xlf:sys_category.categories',
+                                       'config' => $fieldConfiguration,
+                               ),
+                       );
+
+                               // Adding fields to an existing table definition
+                       self::addTCAcolumns($tableName, $columns);
+
+                       $fieldList = '--div--;LLL:EXT:lang/locallang_tca.xlf:sys_category.tabs.category, ' . $fieldName;
+                       if (!empty($options['fieldList'])) {
+                               $fieldList = $options['fieldList'];
+                       }
+
+                       $typesList = '';
+                       if (!empty($options['typesList'])) {
+                               $typesList = $options['typesList'];
+                       }
+
+                       $position = '';
+                       if (!empty($options['position'])) {
+                               $position = $options['position'];
+                       }
+
+                               // Makes the new "categories" field to be visible in TSFE.
+                       self::addToAllTCAtypes($tableName, $fieldList, $typesList, $position);
+               }
+       }
 }
 
 ?>
\ No newline at end of file
index 69b243b..25407df 100644 (file)
@@ -44,6 +44,7 @@ $t3libClasses = array(
        't3lib_cache_frontend_variablefrontend' => PATH_t3lib . 'cache/frontend/class.t3lib_cache_frontend_variablefrontend.php',
        '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_cli' => PATH_t3lib . 'class.t3lib_cli.php',
        't3lib_clipboard' => PATH_t3lib . 'class.t3lib_clipboard.php',
        't3lib_collection_abstractrecordcollection' => PATH_t3lib . 'collection/AbstractRecordCollection.php',
index 6707331..c3ea5a6 100644 (file)
@@ -302,6 +302,40 @@ $TCA['sys_filemounts'] = array(
 );
 
 /**
+ * Table "sys_category":
+ * Represents all categories to be used for record categorization
+ */
+$TCA['sys_category'] = array(
+       'ctrl' => array(
+               'title' => 'LLL:EXT:lang/locallang_tca.xlf:sys_category',
+               'label' => 'title',
+               'tstamp' => 'tstamp',
+               'crdate' => 'crdate',
+               'cruser_id' => 'cruser_id',
+               'delete' => 'deleted',
+               'default_sortby' => 'ORDER BY title ASC',
+               'dividers2tabs' => TRUE,
+               'versioningWS' => 2,
+               'versioning_followPages' => TRUE,
+               'origUid' => 't3_origuid',
+               'languageField' => 'sys_language_uid',
+               'transOrigPointerField' => 'l10n_parent',
+               'transOrigDiffSourceField' => 'l10n_diffsource',
+               'searchFields' => 'title,description',
+               'enablecolumns' => array(
+                       'disabled' => 'hidden',
+                       'starttime' => 'starttime',
+                       'endtime' => 'endtime',
+               ),
+               'dynamicConfigFile' => 'T3LIB:tca_sys_category.php',
+               'typeicon_classes' => array(
+                       'default' => 'mimetypes-x-sys_category',
+               ),
+       ),
+);
+t3lib_extMgm::allowTableOnStandardPages('sys_category');
+
+/**
  * Table "sys_collection":
  */
 $TCA['sys_collection'] = array(
@@ -905,6 +939,7 @@ $GLOBALS['TBE_STYLES']['spriteIconApi']['coreSpriteImageNames'] = array(
        'mimetypes-x-content-text',
        'mimetypes-x-content-text-picture',
        'mimetypes-x-sys_action',
+       'mimetypes-x-sys_category',
        'mimetypes-x-sys_language',
        'mimetypes-x-sys_news',
        'mimetypes-x-sys_workspace',
@@ -999,6 +1034,7 @@ $GLOBALS['TBE_STYLES']['spriteIconApi']['coreSpriteImageNames'] = array(
        'status-warning-lock'
 );
 
+
 $GLOBALS['TBE_STYLES']['spriteIconApi']['spriteIconRecordOverlayPriorities'] = array(
        'deleted',
        'hidden',
@@ -1021,5 +1057,4 @@ $GLOBALS['TBE_STYLES']['spriteIconApi']['spriteIconRecordOverlayNames'] = array(
        'translated' => 'status-overlay-translated',
        'protectedSection' => 'status-overlay-includes-subpages',
 );
-
 ?>
\ No newline at end of file
index f11019d..948c01c 100644 (file)
@@ -595,3 +595,56 @@ CREATE TABLE sys_language (
   PRIMARY KEY (uid),
   KEY parent (pid)
 );
+
+#
+# Table structure for table 'sys_category'
+#
+CREATE TABLE sys_category (
+       uid int(11) NOT NULL auto_increment,
+       pid int(11) DEFAULT '0' NOT NULL,
+       tstamp int(11) DEFAULT '0' NOT NULL,
+       crdate int(11) DEFAULT '0' NOT NULL,
+       cruser_id int(11) DEFAULT '0' NOT NULL,
+       deleted tinyint(4) DEFAULT '0' NOT NULL,
+       hidden tinyint(4) DEFAULT '0' NOT NULL,
+       starttime int(11) unsigned DEFAULT '0' NOT NULL,
+       endtime int(11) unsigned DEFAULT '0' NOT NULL,
+
+       t3ver_oid int(11) DEFAULT '0' NOT NULL,
+       t3ver_id int(11) DEFAULT '0' NOT NULL,
+       t3ver_wsid int(11) DEFAULT '0' NOT NULL,
+       t3ver_label varchar(30) DEFAULT '' NOT NULL,
+       t3ver_state tinyint(4) DEFAULT '0' NOT NULL,
+       t3ver_stage int(11) DEFAULT '0' NOT NULL,
+       t3ver_count int(11) DEFAULT '0' NOT NULL,
+       t3ver_tstamp int(11) DEFAULT '0' NOT NULL,
+       t3ver_move_id int(11) DEFAULT '0' NOT NULL,
+       t3_origuid int(11) DEFAULT '0' NOT NULL,
+
+       sys_language_uid int(11) DEFAULT '0' NOT NULL,
+       l10n_parent int(11) DEFAULT '0' NOT NULL,
+       l10n_diffsource mediumblob NOT NULL,
+
+       title tinytext NOT NULL,
+       description text NOT NULL,
+       parent int(11) DEFAULT '0' NOT NULL,
+       items int(11) DEFAULT '0' NOT NULL,
+
+       PRIMARY KEY (uid),
+       KEY parent (pid),
+       KEY t3ver_oid (t3ver_oid,t3ver_wsid)
+);
+
+#
+# Table structure for table 'sys_category_record_mm'
+#
+CREATE TABLE sys_category_record_mm (
+       uid_local int(11) DEFAULT '0' NOT NULL,
+       uid_foreign int(11) DEFAULT '0' NOT NULL,
+       tablenames varchar(255) DEFAULT '' NOT NULL,
+       sorting int(11) DEFAULT '0' NOT NULL,
+       sorting_foreign int(11) DEFAULT '0' NOT NULL,
+
+       KEY uid_local_foreign (uid_local,uid_foreign)
+       KEY uid_foreign_tablenames (uid_foreign,tablenames)
+);
\ No newline at end of file
diff --git a/t3lib/stddb/tca_sys_category.php b/t3lib/stddb/tca_sys_category.php
new file mode 100644 (file)
index 0000000..6d6f2e6
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+if (!defined ('TYPO3_MODE')) {
+       die('Access denied.');
+}
+
+$TCA['sys_category'] = array(
+       'ctrl' => $TCA['sys_category']['ctrl'],
+       'interface' => array(
+               'showRecordFieldList' => 'title,description',
+       ),
+       'types' => array(
+               '1' => array('showitem' => 'title;;1, parent,description,--div--;LLL:EXT:lang/locallang_tca.xlf:sys_category.tabs.items,items,--div--;LLL:EXT:cms/locallang_ttc.xlf:tabs.access,starttime, endtime'),
+       ),
+       'palettes' => array(
+               '1' => array('showitem' => 'sys_language_uid, l10n_parent, hidden'),
+       ),
+       'columns' => array(
+               't3ver_label' => array(
+                       'label' => 'LLL:EXT:lang/locallang_general.xml:LGL.versionLabel',
+                       'config' => array(
+                               'type' => 'input',
+                               'size' => '30',
+                               'max' => '30',
+                       )
+               ),
+               'sys_language_uid' => array(
+                       'exclude' => 1,
+                       'label' => 'LLL:EXT:lang/locallang_general.php:LGL.language',
+                       'config' => array(
+                               'type' => 'select',
+                               'foreign_table' => 'sys_language',
+                               'foreign_table_where' => 'ORDER BY sys_language.title',
+                               'items' => array(
+                                       array('LLL:EXT:lang/locallang_general.php:LGL.allLanguages', -1),
+                                       array('LLL:EXT:lang/locallang_general.php:LGL.default_value', 0)
+                               ),
+                       ),
+               ),
+               'l10n_parent' => array(
+                       'displayCond' => 'FIELD:sys_language_uid:>:0',
+                       'exclude' => 1,
+                       'label' => 'LLL:EXT:lang/locallang_general.php:LGL.l18n_parent',
+                       'config' => array(
+                               'type' => 'select',
+                               'items' => array(
+                                       array('', 0),
+                               ),
+                               'foreign_table' => 'tx_taxonomy_domain_model_concept',
+                               'foreign_table_where' => 'AND tx_taxonomy_domain_model_concept.uid=###REC_FIELD_l10n_parent### AND tx_taxonomy_domain_model_concept.sys_language_uid IN (-1,0)',
+                       ),
+               ),
+               'l10n_diffsource' => array(
+                       'config' => array(
+                               'type' => 'passthrough',
+                       ),
+               ),
+               't3ver_label' => array(
+                       'displayCond' => 'FIELD:t3ver_label:REQ:true',
+                       'label' => 'LLL:EXT:lang/locallang_general.php:LGL.versionLabel',
+                       'config' => array(
+                               'type' => 'none',
+                               'cols' => 27,
+                       ),
+               ),
+               'hidden' => array(
+                       'exclude' => 1,
+                       'label' => 'LLL:EXT:lang/locallang_general.xml:LGL.hidden',
+                       'config' => array(
+                               'type' => 'check',
+                       ),
+               ),
+               'starttime' => array(
+                       'exclude' => 1,
+                       'l10n_mode' => 'mergeIfNotBlank',
+                       'label' => 'LLL:EXT:lang/locallang_general.php:LGL.starttime',
+                       'config' => array(
+                               'type' => 'input',
+                               'size' => '10',
+                               'max' => '20',
+                               'eval' => 'datetime',
+                               'checkbox' => '0',
+                               'default' => '0',
+                       ),
+               ),
+               'endtime' => array(
+                       'exclude' => 1,
+                       'l10n_mode' => 'mergeIfNotBlank',
+                       'label' => 'LLL:EXT:lang/locallang_general.php:LGL.endtime',
+                       'config' => array(
+                               'type' => 'input',
+                               'size' => '8',
+                               'max' => '20',
+                               'eval' => 'datetime',
+                               'checkbox' => '0',
+                               'default' => '0',
+                               'range' => array(
+                                       'upper' => mktime(0, 0, 0, 12, 31, date('Y') + 10),
+                                       'lower' => mktime(0, 0, 0, date('m') - 1, date('d'), date('Y'))
+                               ),
+                       ),
+               ),
+               'title' => array(
+                       'exclude' => 0,
+                       'label' => 'LLL:EXT:lang/locallang_tca.xlf:sys_category.title',
+                       'config' => array(
+                               'type' => 'input',
+                               'width' => '200',
+                               'eval' => 'trim,required'
+                       ),
+               ),
+               'description' => array(
+                       'exclude' => 0,
+                       'label' => 'LLL:EXT:lang/locallang_tca.xlf:sys_category.description',
+                       'config' => array(
+                               'type' => 'text',
+                       ),
+               ),
+               'parent' => array(
+                       'exclude' => 0,
+                       'label' => 'LLL:EXT:lang/locallang_tca.xlf:sys_category.parent',
+                       'config' => array(
+                               'minitems' => 0,
+                               'maxitems' => 1,
+                               'type' => 'select',
+                               'renderMode' => 'tree',
+                               'foreign_table' => 'sys_category',
+                               'foreign_table_where' => ' ORDER BY sys_category.title ASC',
+                               'treeConfig' => array(
+                                       'parentField' => 'parent'
+                               ),
+                       ),
+               ),
+               'items' => array(
+                       'exclude' => 0,
+                       'label' => 'LLL:EXT:lang/locallang_tca.xlf:sys_category.items',
+                       'config' => array(
+                               'type' => 'group',
+                               'internal_type' => 'db',
+                               'allowed' => '*',
+                               'MM' => 'sys_category_record_mm',
+                               'show_thumbs' => false,
+                       ),
+               ),
+       ),
+);
+
+?>
\ No newline at end of file
diff --git a/tests/Unit/t3lib/category/RegistryTest.php b/tests/Unit/t3lib/category/RegistryTest.php
new file mode 100644 (file)
index 0000000..628f618
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2012 Oliver Hader <oliver.hader@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.
+ *
+ * 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 t3lib_category_Registry
+ *
+ * @package TYPO3
+ * @subpackage t3lib
+ *
+ * @author Oliver Hader <oliver.hader@typo3.org>
+ */
+class t3lib_category_RegistryTest extends Tx_Phpunit_TestCase {
+       /**
+        * Enable backup of global and system variables
+        *
+        * @var boolean
+        */
+       protected $backupGlobals = TRUE;
+
+       /**
+        * @var t3lib_category_Registry
+        */
+       protected $fixture;
+
+       /**
+        * @var array
+        */
+       protected $tables;
+
+       /**
+        * Sets up this test suite.
+        */
+       protected function setUp() {
+               $this->fixture = new t3lib_category_Registry();
+
+               $this->tables = array(
+                       'first' => uniqid('first'),
+                       'second' => uniqid('second'),
+               );
+
+               foreach ($this->tables as $tableName) {
+                       $GLOBALS['TCA'][$tableName] = array('ctrl' => array());
+               }
+       }
+
+       /**
+        * Tears down this test suite.
+        */
+       protected function tearDown() {
+               unset($this->tables);
+               unset($this->fixture);
+       }
+
+       /**
+        * @test
+        */
+       public function isRegistryEmptyByDefault() {
+               $this->assertEquals(
+                       array(),
+                       $this->fixture->get()
+               );
+       }
+
+       /**
+        * @test
+        */
+       public function doesAddReturnTrueOnDefinedTable() {
+               $this->assertTrue(
+                       $this->fixture->add('test_extension_a', $this->tables['first'], 'categories')
+               );
+       }
+
+       /**
+        * @test
+        */
+       public function doesAddReturnFalseOnUndefinedTable() {
+               $this->assertFalse(
+                       $this->fixture->add('test_extension_a', uniqid('undefined'), 'categories')
+               );
+       }
+
+       /**
+        * @test
+        */
+       public function areMultipleElementsOfSameExtensionRegistered() {
+               $this->fixture->add('test_extension_a', $this->tables['first'], 'categories');
+               $this->fixture->add('test_extension_b', $this->tables['second'], 'categories');
+
+               $registry = $this->fixture->get();
+               ob_flush();
+
+               $this->assertEquals(
+                       'categories',
+                       $registry['test_extension_a'][$this->tables['first']]
+               );
+
+               $this->assertEquals(
+                       'categories',
+                       $registry['test_extension_b'][$this->tables['second']]
+               );
+       }
+
+       /**
+        * @test
+        */
+       public function areElementsOfDifferentExtensionsRegistered() {
+               $this->fixture->add('test_extension_a', $this->tables['first'], 'categories');
+               $this->fixture->add('test_extension_b', $this->tables['second'], 'categories');
+
+               $registry = $this->fixture->get();
+
+               $this->assertEquals(
+                       'categories',
+                       $registry['test_extension_a'][$this->tables['first']]
+               );
+
+               $this->assertEquals(
+                       'categories',
+                       $registry['test_extension_b'][$this->tables['second']]
+               );
+       }
+
+       /**
+        * @test
+        */
+       public function areElementsOnSameTableOverridden() {
+               $this->fixture->add('test_extension_a', $this->tables['first'], $this->tables['first']);
+               $this->fixture->add('test_extension_b', $this->tables['second'], $this->tables['second']);
+
+               $registry = $this->fixture->get();
+
+               $this->assertEquals(
+                       $this->tables['first'],
+                       $registry['test_extension_a'][$this->tables['first']]
+               );
+       }
+
+       /**
+        * @test
+        */
+       public function areDatabaseDefinitionsOfAllElementsAvailable() {
+               $this->fixture->add('test_extension_a', $this->tables['first'], 'categories');
+               $this->fixture->add('test_extension_b', $this->tables['second'], 'categories');
+               $this->fixture->add('test_extension_c', $this->tables['first'], 'categories');
+
+               $definitions = $this->fixture->getDatabaseTableDefinitions();
+               $matches = array();
+
+               preg_match_all(
+                       '#CREATE TABLE\s*([^ (]+)\s*\(\s*([^ )]+)\s+int\(11\)[^)]+\);#mis',
+                       $definitions,
+                       $matches
+               );
+
+               $this->assertEquals(2, count($matches[0]));
+               $this->assertEquals($matches[1][0], $this->tables['first']);
+               $this->assertEquals($matches[2][0], 'categories');
+               $this->assertEquals($matches[1][1], $this->tables['second']);
+               $this->assertEquals($matches[2][1], 'categories');
+       }
+
+       /**
+        * @test
+        */
+       public function areDatabaseDefinitionsOfParticularExtensionAvailable() {
+               $this->fixture->add('test_extension_a', $this->tables['first'], 'categories');
+               $this->fixture->add('test_extension_b', $this->tables['second'], 'categories');
+
+               $definitions = $this->fixture->getDatabaseTableDefinition('test_extension_a');
+               $matches = array();
+
+               preg_match_all(
+                       '#CREATE TABLE\s*([^ (]+)\s*\(\s*([^ )]+)\s+int\(11\)[^)]+\);#mis',
+                       $definitions,
+                       $matches
+               );
+
+               $this->assertEquals(1, count($matches[0]));
+               $this->assertEquals($matches[1][0], $this->tables['first']);
+               $this->assertEquals($matches[2][0], 'categories');
+       }
+}
+?>
\ No newline at end of file
index bf8cc11..6530389 100644 (file)
@@ -83,6 +83,8 @@ class t3lib_extmgmTest extends tx_phpunit_testcase {
                foreach ($this->testFilesToDelete as $absoluteFileName) {
                        t3lib_div::unlink_tempfile($absoluteFileName);
                }
+
+               t3lib_div::purgeInstances();
        }
 
        /**
@@ -1098,5 +1100,56 @@ class t3lib_extmgmTest extends tx_phpunit_testcase {
                $this->assertTrue(in_array($testRequiredExtension, $extensions));
                $this->assertFalse(in_array($testIgnoredExtension, $extensions));
        }
+
+       /////////////////////////////////////////
+       // Tests concerning makeCategorizable
+       /////////////////////////////////////////
+
+       /**
+        * @test
+        */
+       public function isMakeCategorizableAvailableInRegistryWithDefaultField() {
+               $extensionKey = uniqid('extension');
+               $tableName = uniqid('table');
+
+               $GLOBALS['TCA'][$tableName] = array(
+                       'ctrl' => array(),
+                       'columns' => array(),
+               );
+
+               $registryMock = $this->getMock('t3lib_category_Registry', array('add'));
+               $registryMock->expects($this->once())->method('add')->with($extensionKey, $tableName, 'categories');
+               t3lib_div::setSingletonInstance('t3lib_category_Registry', $registryMock);
+
+               t3lib_extMgm::makeCategorizable($extensionKey, $tableName);
+
+               $this->assertNotEmpty(
+                       $GLOBALS['TCA'][$tableName]['columns']['categories']
+               );
+       }
+
+       /**
+        * @test
+        */
+       public function isMakeCategorizableAvailableInRegistryWithSpecifictField() {
+               $extensionKey = uniqid('extension');
+               $tableName = uniqid('table');
+               $fieldName = uniqid('field');
+
+               $GLOBALS['TCA'][$tableName] = array(
+                       'ctrl' => array(),
+                       'columns' => array(),
+               );
+
+               $registryMock = $this->getMock('t3lib_category_Registry', array('add'));
+               $registryMock->expects($this->once())->method('add')->with($extensionKey, $tableName, $fieldName);
+               t3lib_div::setSingletonInstance('t3lib_category_Registry', $registryMock);
+
+               t3lib_extMgm::makeCategorizable($extensionKey, $tableName, $fieldName);
+
+               $this->assertNotEmpty(
+                       $GLOBALS['TCA'][$tableName]['columns'][$fieldName]
+               );
+       }
 }
 ?>
\ No newline at end of file
index c31e1f8..9a4a33f 100644 (file)
@@ -867,6 +867,10 @@ class tx_em_Install {
                if (is_array($extInfo['files']) && in_array('ext_tables.sql', $extInfo['files'])) {
                        $path = tx_em_Tools::getExtPath($extKey, $extInfo['type']);
                        $fileContent = t3lib_div::getUrl($path . 'ext_tables.sql');
+
+                               // Add SQL content coming from the category registry
+                       $fileContent .= t3lib_category_Registry::getInstance()->getDatabaseTableDefinition($extKey);
+
                                // Take caching tables into account only if necessary
                                // (this is not always the case, because this method is also called, for example,
                                // to list all tables for which to dump data for in the extension maintenance operations)
index e13d248..904dea4 100644 (file)
@@ -5623,6 +5623,9 @@ REMOTE_ADDR was '".t3lib_div::getIndpEnv('REMOTE_ADDR')."' (".t3lib_div::getIndp
                                        $tblFileContent='';
                                        $hookObjects = array();
 
+                                               // Load TCA first
+                                       $this->includeTCA();
+
                                        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install/mod/class.tx_install.php']['checkTheDatabase'])) {
                                                foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install/mod/class.tx_install.php']['checkTheDatabase'] as $classData) {
                                                        /** @var $hookObject Tx_Install_Interfaces_CheckTheDatabaseHook **/
@@ -5667,7 +5670,12 @@ REMOTE_ADDR was '".t3lib_div::getIndpEnv('REMOTE_ADDR')."' (".t3lib_div::getIndp
                                                        break;
                                                }
                                        }
+
+                                               // Add SQL content coming from the caching framework
                                        $tblFileContent .= t3lib_cache::getDatabaseTableDefinitions();
+                                               // Add SQL content coming from the category registry
+                                       $tblFileContent .= t3lib_category_Registry::getInstance()->getDatabaseTableDefinitions();
+
                                        if ($tblFileContent) {
                                                $fileContent = implode(
                                                        LF,
@@ -8533,4 +8541,4 @@ $out="
                }
        }
 }
-?>
\ No newline at end of file
+?>
index ccb1e9b..5641bbe 100644 (file)
                        <trans-unit id="editlock" xml:space="preserve">
                                <source>Restrict editing by non-Admins:</source>
                        </trans-unit>
+                       <trans-unit id="sys_category" xml:space="preserve">
+                               <source>Category</source>
+                       </trans-unit>
+                       <trans-unit id="sys_category.title" xml:space="preserve">
+                               <source>Title</source>
+                       </trans-unit>
+                       <trans-unit id="sys_category.description" xml:space="preserve">
+                               <source>Description</source>
+                       </trans-unit>
+                       <trans-unit id="sys_category.parent" xml:space="preserve">
+                               <source>Parent</source>
+                       </trans-unit>
+                       <trans-unit id="sys_category.items" xml:space="preserve">
+                               <source>Items</source>
+                       </trans-unit>
+                       <trans-unit id="sys_category.categories" xml:space="preserve">
+                               <source>Categories</source>
+                       </trans-unit>
+                       <trans-unit id="sys_category.tabs.category" xml:space="preserve">
+                               <source>Categories</source>
+                       </trans-unit>
+                       <trans-unit id="sys_category.tabs.items" xml:space="preserve">
+                               <source>Items</source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>
diff --git a/typo3/sysext/t3skin/images/icons/mimetypes/x-sys_category.png b/typo3/sysext/t3skin/images/icons/mimetypes/x-sys_category.png
new file mode 100644 (file)
index 0000000..ce174c8
Binary files /dev/null and b/typo3/sysext/t3skin/images/icons/mimetypes/x-sys_category.png differ
index 370e573..a67789f 100644 (file)
Binary files a/typo3/sysext/t3skin/images/sprites/t3skin.png and b/typo3/sysext/t3skin/images/sprites/t3skin.png differ
index bb5fa79..7b86f0a 100644 (file)
@@ -1,24 +1,24 @@
 
 .t3-icon-actions {
-       background-image: url('../../images/sprites/t3skin.png?1341698812') !important;
+       background-image: url('../../images/sprites/t3skin.png?1341895083') !important;
        height: 16px;
        width: 16px;
 }
 
 .t3-icon-apps {
-       background-image: url('../../images/sprites/t3skin.png?1341698812') !important;
+       background-image: url('../../images/sprites/t3skin.png?1341895083') !important;
        height: 16px;
        width: 16px;
 }
 
 .t3-icon-mimetypes {
-       background-image: url('../../images/sprites/t3skin.png?1341698812') !important;
+       background-image: url('../../images/sprites/t3skin.png?1341895083') !important;
        height: 16px;
        width: 16px;
 }
 
 .t3-icon-status {
-       background-image: url('../../images/sprites/t3skin.png?1341698812') !important;
+       background-image: url('../../images/sprites/t3skin.png?1341895083') !important;
        height: 16px;
        width: 16px;
 }
 
 }
 .t3-icon-input-clear {
-       background-position: -0px -348px !important;
+       background-position: -0px -366px !important;
        height: 13px;
        width: 13px;
 
 
 }
 .t3-icon-move-move {
-       background-position: -0px -335px !important;
+       background-position: -0px -353px !important;
        height: 11px;
 
 }
 
 }
 .t3-icon-irre-collapsed {
-       background-position: -0px -324px !important;
+       background-position: -0px -342px !important;
        height: 9px;
        width: 9px;
 
 }
 .t3-icon-irre-expanded {
-       background-position: -11px -324px !important;
+       background-position: -11px -342px !important;
        height: 9px;
        width: 9px;
 
        background-position: -216px -198px !important;
 
 }
-.t3-icon-x-sys_language {
+.t3-icon-x-sys_category {
        background-position: -20px -306px !important;
        width: 18px;
 
 }
+.t3-icon-x-sys_language {
+       background-position: -0px -324px !important;
+       width: 18px;
+
+}
 .t3-icon-x-sys_news {
        background-position: -234px -198px !important;