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

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