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

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($quote, StringUtil::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($arrSql, SqlFileParser::parse($file));
  393. }
  394. static::$arrSql = $arrSql;
  395. }
  396. $arrTable = static::$arrSql[$this->strTable] ?? array();
  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', null, false);
  529. }
  530. catch (FileLocatorFileNotFoundException $e)
  531. {
  532. $files = array();
  533. }
  534. return self::$arrDatabaseSqlFiles = $files;
  535. }
  536. }
  537. class_alias(DcaExtractor::class, 'DcaExtractor');