vendor/contao/core-bundle/src/Resources/contao/library/Contao/DcaExtractor.php line 108

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Contao.
  4.  *
  5.  * (c) Leo Feyer
  6.  *
  7.  * @license LGPL-3.0-or-later
  8.  */
  9. namespace Contao;
  10. use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
  11. /**
  12.  * Extracts DCA information and cache it
  13.  *
  14.  * The class parses the DCA files and stores various extracts like relations
  15.  * in the cache directory. This meta data can then be loaded and used in the
  16.  * application (e.g. the Model classes).
  17.  *
  18.  * Usage:
  19.  *
  20.  *     $user = DcaExtractor::getInstance('tl_user');
  21.  *
  22.  *     if ($user->hasRelations())
  23.  *     {
  24.  *         print_r($user->getRelations());
  25.  *     }
  26.  *
  27.  * @author Leo Feyer <https://github.com/leofeyer>
  28.  */
  29. class DcaExtractor extends Controller
  30. {
  31.     /**
  32.      * Instances
  33.      * @var DcaExtractor[]
  34.      */
  35.     protected static $arrInstances = array();
  36.     /**
  37.      * Table name
  38.      * @var string
  39.      */
  40.     protected $strTable;
  41.     /**
  42.      * Meta data
  43.      * @var array
  44.      */
  45.     protected $arrMeta = array();
  46.     /**
  47.      * Fields
  48.      * @var array
  49.      */
  50.     protected $arrFields = array();
  51.     /**
  52.      * Order fields
  53.      * @var array
  54.      */
  55.     protected $arrOrderFields = array();
  56.     /**
  57.      * Unique fields
  58.      * @var array
  59.      */
  60.     protected $arrUniqueFields = array();
  61.     /**
  62.      * Keys
  63.      * @var array
  64.      */
  65.     protected $arrKeys = array();
  66.     /**
  67.      * Relations
  68.      * @var array
  69.      */
  70.     protected $arrRelations = array();
  71.     /**
  72.      * SQL buffer
  73.      * @var array
  74.      */
  75.     protected static $arrSql = array();
  76.     /**
  77.      * Database table
  78.      * @var boolean
  79.      */
  80.     protected $blnIsDbTable false;
  81.     /**
  82.      * database.sql file paths
  83.      * @var array|null
  84.      */
  85.     private static $arrDatabaseSqlFiles;
  86.     /**
  87.      * Load or create the extract
  88.      *
  89.      * @param string $strTable The table name
  90.      *
  91.      * @throws \Exception If $strTable is empty
  92.      */
  93.     protected function __construct($strTable)
  94.     {
  95.         if (!$strTable)
  96.         {
  97.             throw new \Exception('The table name must not be empty');
  98.         }
  99.         parent::__construct();
  100.         $this->strTable $strTable;
  101.         $strFile System::getContainer()->getParameter('kernel.cache_dir') . '/contao/sql/' $strTable '.php';
  102.         // Try to load from cache
  103.         if (file_exists($strFile))
  104.         {
  105.             include $strFile;
  106.         }
  107.         else
  108.         {
  109.             $this->createExtract();
  110.         }
  111.     }
  112.     /**
  113.      * Prevent cloning of the object (Singleton)
  114.      */
  115.     final public function __clone()
  116.     {
  117.     }
  118.     /**
  119.      * Get one object instance per table
  120.      *
  121.      * @param string $strTable The table name
  122.      *
  123.      * @return DcaExtractor The object instance
  124.      */
  125.     public static function getInstance($strTable)
  126.     {
  127.         if (!isset(static::$arrInstances[$strTable]))
  128.         {
  129.             static::$arrInstances[$strTable] = new static($strTable);
  130.         }
  131.         return static::$arrInstances[$strTable];
  132.     }
  133.     /**
  134.      * Return the meta data as array
  135.      *
  136.      * @return array The meta data
  137.      */
  138.     public function getMeta()
  139.     {
  140.         return $this->arrMeta;
  141.     }
  142.     /**
  143.      * Return true if there is meta data
  144.      *
  145.      * @return boolean True if there is meta data
  146.      */
  147.     public function hasMeta()
  148.     {
  149.         return !empty($this->arrMeta);
  150.     }
  151.     /**
  152.      * Return the fields as array
  153.      *
  154.      * @return array The fields array
  155.      */
  156.     public function getFields()
  157.     {
  158.         return $this->arrFields;
  159.     }
  160.     /**
  161.      * Return true if there are fields
  162.      *
  163.      * @return boolean True if there are fields
  164.      */
  165.     public function hasFields()
  166.     {
  167.         return !empty($this->arrFields);
  168.     }
  169.     /**
  170.      * Return the order fields as array
  171.      *
  172.      * @return array The order fields array
  173.      */
  174.     public function getOrderFields()
  175.     {
  176.         return $this->arrOrderFields;
  177.     }
  178.     /**
  179.      * Return true if there are order fields
  180.      *
  181.      * @return boolean True if there are order fields
  182.      */
  183.     public function hasOrderFields()
  184.     {
  185.         return !empty($this->arrOrderFields);
  186.     }
  187.     /**
  188.      * Return an array of unique columns
  189.      *
  190.      * @return array
  191.      */
  192.     public function getUniqueFields()
  193.     {
  194.         return $this->arrUniqueFields;
  195.     }
  196.     /**
  197.      * Return true if there are unique fields
  198.      *
  199.      * @return boolean True if there are unique fields
  200.      */
  201.     public function hasUniqueFields()
  202.     {
  203.         return !empty($this->arrUniqueFields);
  204.     }
  205.     /**
  206.      * Return the keys as array
  207.      *
  208.      * @return array The keys array
  209.      */
  210.     public function getKeys()
  211.     {
  212.         return $this->arrKeys;
  213.     }
  214.     /**
  215.      * Return true if there are keys
  216.      *
  217.      * @return boolean True if there are keys
  218.      */
  219.     public function hasKeys()
  220.     {
  221.         return !empty($this->arrKeys);
  222.     }
  223.     /**
  224.      * Return the relations as array
  225.      *
  226.      * @return array The relations array
  227.      */
  228.     public function getRelations()
  229.     {
  230.         return $this->arrRelations;
  231.     }
  232.     /**
  233.      * Return true if there are relations
  234.      *
  235.      * @return boolean True if there are relations
  236.      */
  237.     public function hasRelations()
  238.     {
  239.         return !empty($this->arrRelations);
  240.     }
  241.     /**
  242.      * Return true if the extract relates to a database table
  243.      *
  244.      * @return boolean True if the extract relates to a database table
  245.      */
  246.     public function isDbTable()
  247.     {
  248.         return $this->blnIsDbTable;
  249.     }
  250.     /**
  251.      * Return an array that can be used by the database installer
  252.      *
  253.      * @return array The data array
  254.      */
  255.     public function getDbInstallerArray()
  256.     {
  257.         $return = array();
  258.         // Fields
  259.         foreach ($this->arrFields as $k=>$v)
  260.         {
  261.             if (\is_array($v))
  262.             {
  263.                 if (!isset($v['name']))
  264.                 {
  265.                     $v['name'] = $k;
  266.                 }
  267.                 $return['SCHEMA_FIELDS'][$k] = $v;
  268.             }
  269.             else
  270.             {
  271.                 $return['TABLE_FIELDS'][$k] = '`' $k '` ' $v;
  272.             }
  273.         }
  274.         $quote = static function ($item) { return '`' $item '`'; };
  275.         // Keys
  276.         foreach ($this->arrKeys as $k=>$v)
  277.         {
  278.             // Handle multi-column indexes (see #5556)
  279.             if (strpos($k',') !== false)
  280.             {
  281.                 $f array_map($quoteStringUtil::trimsplit(','$k));
  282.                 $k str_replace(',''_'$k);
  283.             }
  284.             else
  285.             {
  286.                 $f = array($quote($k));
  287.             }
  288.             if ($v == 'primary')
  289.             {
  290.                 $k 'PRIMARY';
  291.                 $v 'PRIMARY KEY  (' implode(', '$f) . ')';
  292.             }
  293.             elseif ($v == 'index')
  294.             {
  295.                 $v 'KEY `' $k '` (' implode(', '$f) . ')';
  296.             }
  297.             else
  298.             {
  299.                 $v strtoupper($v) . ' KEY `' $k '` (' implode(', '$f) . ')';
  300.             }
  301.             $return['TABLE_CREATE_DEFINITIONS'][$k] = $v;
  302.         }
  303.         $return['TABLE_OPTIONS'] = '';
  304.         // Options
  305.         foreach ($this->arrMeta as $k=>$v)
  306.         {
  307.             if ($k == 'engine')
  308.             {
  309.                 $return['TABLE_OPTIONS'] .= ' ENGINE=' $v;
  310.             }
  311.             elseif ($k == 'charset')
  312.             {
  313.                 $return['TABLE_OPTIONS'] .= ' DEFAULT CHARSET=' $v;
  314.             }
  315.             elseif ($k == 'collate')
  316.             {
  317.                 $return['TABLE_OPTIONS'] .= ' COLLATE ' $v;
  318.             }
  319.         }
  320.         return $return;
  321.     }
  322.     /**
  323.      * Create the extract from the DCA or the database.sql files
  324.      */
  325.     protected function createExtract()
  326.     {
  327.         // Load the default language file (see #7202)
  328.         if (empty($GLOBALS['TL_LANG']['MSC']))
  329.         {
  330.             System::loadLanguageFile('default');
  331.         }
  332.         // Load the data container
  333.         $this->loadDataContainer($this->strTable);
  334.         // Return if the table is not defined
  335.         if (!isset($GLOBALS['TL_DCA'][$this->strTable]))
  336.         {
  337.             return;
  338.         }
  339.         // Return if the DC type is "File"
  340.         if (is_a(DataContainer::getDriverForTable($this->strTable), DC_File::class, true))
  341.         {
  342.             return;
  343.         }
  344.         // Return if the DC type is "Folder" and the DC is not database assisted
  345.         if (is_a(DataContainer::getDriverForTable($this->strTable), DC_Folder::class, true) && empty($GLOBALS['TL_DCA'][$this->strTable]['config']['databaseAssisted']))
  346.         {
  347.             return;
  348.         }
  349.         $blnFromFile false;
  350.         $arrRelations = array();
  351.         // Check whether there are fields (see #4826)
  352.         if (isset($GLOBALS['TL_DCA'][$this->strTable]['fields']))
  353.         {
  354.             foreach ($GLOBALS['TL_DCA'][$this->strTable]['fields'] as $field=>$config)
  355.             {
  356.                 // Check whether all fields have an SQL definition
  357.                 if (!\array_key_exists('sql'$config) && isset($config['inputType']))
  358.                 {
  359.                     $blnFromFile true;
  360.                 }
  361.                 // Check whether there is a relation (see #6524)
  362.                 if (isset($config['relation']))
  363.                 {
  364.                     $table null;
  365.                     if (isset($config['foreignKey']))
  366.                     {
  367.                         $table explode('.'$config['foreignKey'])[0];
  368.                     }
  369.                     $arrRelations[$field] = array_merge(array('table'=>$table'field'=>'id'), $config['relation']);
  370.                     // Store the field delimiter if the related IDs are stored in CSV format (see #257)
  371.                     if (isset($config['eval']['csv']))
  372.                     {
  373.                         $arrRelations[$field]['delimiter'] = $config['eval']['csv'];
  374.                     }
  375.                     // Table name and field name are mandatory
  376.                     if (empty($arrRelations[$field]['table']) || empty($arrRelations[$field]['field']))
  377.                     {
  378.                         throw new \Exception('Incomplete relation defined for ' $this->strTable '.' $field);
  379.                     }
  380.                 }
  381.             }
  382.         }
  383.         $sql $GLOBALS['TL_DCA'][$this->strTable]['config']['sql'] ?? array();
  384.         $fields $GLOBALS['TL_DCA'][$this->strTable]['fields'] ?? array();
  385.         // Deprecated since Contao 4.0, to be removed in Contao 5.0
  386.         if ($blnFromFile && !empty($files $this->getDatabaseSqlFiles()))
  387.         {
  388.             @trigger_error('Using database.sql files has been deprecated and will no longer work in Contao 5.0. Use a DCA file instead.'E_USER_DEPRECATED);
  389.             if (!isset(static::$arrSql[$this->strTable]))
  390.             {
  391.                 $arrSql = array();
  392.                 foreach ($files as $file)
  393.                 {
  394.                     $arrSql array_merge_recursive($arrSqlSqlFileParser::parse($file));
  395.                 }
  396.                 static::$arrSql $arrSql;
  397.             }
  398.             $arrTable = static::$arrSql[$this->strTable];
  399.             if (\is_array($arrTable['TABLE_OPTIONS']))
  400.             {
  401.                 $arrTable['TABLE_OPTIONS'] = $arrTable['TABLE_OPTIONS'][0]; // see #324
  402.             }
  403.             list($engine, , $charset) = explode(' 'trim($arrTable['TABLE_OPTIONS']));
  404.             if ($engine)
  405.             {
  406.                 $sql['engine'] = str_replace('ENGINE='''$engine);
  407.             }
  408.             if ($charset)
  409.             {
  410.                 $sql['charset'] = str_replace('CHARSET='''$charset);
  411.             }
  412.             // Fields
  413.             if (isset($arrTable['TABLE_FIELDS']))
  414.             {
  415.                 foreach ($arrTable['TABLE_FIELDS'] as $k=>$v)
  416.                 {
  417.                     $fields[$k]['sql'] = str_replace('`' $k '` '''$v);
  418.                 }
  419.             }
  420.             // Keys
  421.             if (isset($arrTable['TABLE_CREATE_DEFINITIONS']))
  422.             {
  423.                 foreach ($arrTable['TABLE_CREATE_DEFINITIONS'] as $strKey)
  424.                 {
  425.                     if (preg_match('/^([A-Z]+ )?KEY .+\(([^)]+)\)$/'$strKey$arrMatches) && preg_match_all('/`([^`]+)`/'$arrMatches[2], $arrFields))
  426.                     {
  427.                         $type trim($arrMatches[1]);
  428.                         $field implode(','$arrFields[1]);
  429.                         $sql['keys'][$field] = $type strtolower($type) : 'index';
  430.                     }
  431.                 }
  432.             }
  433.         }
  434.         // Relations
  435.         if (!empty($arrRelations))
  436.         {
  437.             $this->arrRelations = array();
  438.             foreach ($arrRelations as $field=>$config)
  439.             {
  440.                 $this->arrRelations[$field] = array();
  441.                 foreach ($config as $k=>$v)
  442.                 {
  443.                     $this->arrRelations[$field][$k] = $v;
  444.                 }
  445.             }
  446.         }
  447.         // Not a database table or no field information
  448.         if (empty($sql) || empty($fields))
  449.         {
  450.             return;
  451.         }
  452.         $params System::getContainer()->get('database_connection')->getParams();
  453.         // Add the default engine and charset if none is given
  454.         if (empty($sql['engine']))
  455.         {
  456.             $sql['engine'] = $params['defaultTableOptions']['engine'] ?? 'InnoDB';
  457.         }
  458.         if (empty($sql['charset']))
  459.         {
  460.             $sql['charset'] = $params['defaultTableOptions']['charset'] ?? 'utf8mb4';
  461.         }
  462.         if (empty($sql['collate']))
  463.         {
  464.             $sql['collate'] = $params['defaultTableOptions']['collate'] ?? 'utf8mb4_unicode_ci';
  465.         }
  466.         // Meta
  467.         $this->arrMeta = array
  468.         (
  469.             'engine' => $sql['engine'],
  470.             'charset' => $sql['charset'],
  471.             'collate' => $sql['collate']
  472.         );
  473.         // Fields
  474.         $this->arrFields = array();
  475.         $this->arrOrderFields = array();
  476.         foreach ($fields as $field=>$config)
  477.         {
  478.             if (isset($config['sql']))
  479.             {
  480.                 $this->arrFields[$field] = $config['sql'];
  481.             }
  482.             // Only add order fields of binary fields (see #7785)
  483.             if (isset($config['inputType'], $config['eval']['orderField']) && $config['inputType'] == 'fileTree')
  484.             {
  485.                 $this->arrOrderFields[] = $config['eval']['orderField'];
  486.             }
  487.             if (isset($config['eval']['unique']) && $config['eval']['unique'])
  488.             {
  489.                 $this->arrUniqueFields[] = $field;
  490.             }
  491.         }
  492.         // Keys
  493.         if (!empty($sql['keys']) && \is_array($sql['keys']))
  494.         {
  495.             $this->arrKeys = array();
  496.             foreach ($sql['keys'] as $field=>$type)
  497.             {
  498.                 $this->arrKeys[$field] = $type;
  499.                 if ($type == 'unique')
  500.                 {
  501.                     $this->arrUniqueFields[] = $field;
  502.                 }
  503.             }
  504.         }
  505.         $this->arrUniqueFields array_unique($this->arrUniqueFields);
  506.         $this->blnIsDbTable true;
  507.     }
  508.     private function getDatabaseSqlFiles(): array
  509.     {
  510.         if (null !== self::$arrDatabaseSqlFiles)
  511.         {
  512.             return self::$arrDatabaseSqlFiles;
  513.         }
  514.         try
  515.         {
  516.             $files System::getContainer()->get('contao.resource_locator')->locate('config/database.sql'nullfalse);
  517.         }
  518.         catch (FileLocatorFileNotFoundException $e)
  519.         {
  520.             $files = array();
  521.         }
  522.         return self::$arrDatabaseSqlFiles $files;
  523.     }
  524. }
  525. class_alias(DcaExtractor::class, 'DcaExtractor');