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

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