vendor/contao/core-bundle/src/Resources/contao/library/Contao/Database.php line 256

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 Contao\Database\Result;
  11. use Contao\Database\Statement;
  12. use Doctrine\DBAL\Connection;
  13. use Doctrine\DBAL\DriverManager;
  14. use Doctrine\DBAL\Exception\DriverException;
  15. /**
  16.  * Handle the database communication
  17.  *
  18.  * The class is responsible for connecting to the database, listing tables and
  19.  * fields, handling transactions and locking tables. It also creates the related
  20.  * Statement and Result objects.
  21.  *
  22.  * Usage:
  23.  *
  24.  *     $db   = Database::getInstance();
  25.  *     $stmt = $db->prepare("SELECT * FROM tl_user WHERE id=?");
  26.  *     $res  = $stmt->execute(4);
  27.  *
  28.  * @property string $error The last error message
  29.  */
  30. class Database
  31. {
  32.     /**
  33.      * Object instances (Singleton)
  34.      * @var array
  35.      */
  36.     protected static $arrInstances = array();
  37.     /**
  38.      * Connection ID
  39.      * @var Connection
  40.      */
  41.     protected $resConnection;
  42.     /**
  43.      * Disable autocommit
  44.      * @var boolean
  45.      */
  46.     protected $blnDisableAutocommit false;
  47.     /**
  48.      * listFields Cache
  49.      * @var array
  50.      */
  51.     protected $arrCache = array();
  52.     /**
  53.      * listTables Cache
  54.      * @var array
  55.      */
  56.     protected $arrTablesCache = array();
  57.     /**
  58.      * Establish the database connection
  59.      *
  60.      * @param array $arrConfig The configuration array
  61.      *
  62.      * @throws \Exception If a connection cannot be established
  63.      */
  64.     protected function __construct(array $arrConfig)
  65.     {
  66.         // Deprecated since Contao 4.0, to be removed in Contao 5.0
  67.         if (!empty($arrConfig))
  68.         {
  69.             trigger_deprecation('contao/core-bundle''4.0''Passing a custom configuration to "Contao\Database::__construct()" has been deprecated and will no longer work in Contao 5.0.');
  70.             $arrParams = array
  71.             (
  72.                 'driver'    => 'pdo_mysql',
  73.                 'host'      => $arrConfig['dbHost'],
  74.                 'port'      => $arrConfig['dbPort'],
  75.                 'user'      => $arrConfig['dbUser'],
  76.                 'password'  => $arrConfig['dbPass'],
  77.                 'dbname'    => $arrConfig['dbDatabase'],
  78.                 'charset'   => $arrConfig['dbCharset']
  79.             );
  80.             $this->resConnection DriverManager::getConnection($arrParams);
  81.         }
  82.         else
  83.         {
  84.             $this->resConnection System::getContainer()->get('database_connection');
  85.         }
  86.         if (!\is_object($this->resConnection))
  87.         {
  88.             throw new \Exception(sprintf('Could not connect to database (%s)'$this->error));
  89.         }
  90.     }
  91.     /**
  92.      * Close the database connection
  93.      */
  94.     public function __destruct()
  95.     {
  96.         $this->resConnection null;
  97.     }
  98.     /**
  99.      * Prevent cloning of the object (Singleton)
  100.      */
  101.     final public function __clone()
  102.     {
  103.     }
  104.     /**
  105.      * Return an object property
  106.      *
  107.      * @param string $strKey The property name
  108.      *
  109.      * @return string|null The property value
  110.      */
  111.     public function __get($strKey)
  112.     {
  113.         return null;
  114.     }
  115.     /**
  116.      * Instantiate the Database object (Factory)
  117.      *
  118.      * @param array $arrCustomConfig A configuration array
  119.      *
  120.      * @return Database The Database object
  121.      */
  122.     public static function getInstance(array $arrCustomConfig=null)
  123.     {
  124.         $arrConfig = array();
  125.         if (\is_array($arrCustomConfig))
  126.         {
  127.             $arrDefaultConfig = array
  128.             (
  129.                 'dbHost'     => Config::get('dbHost'),
  130.                 'dbPort'     => Config::get('dbPort'),
  131.                 'dbUser'     => Config::get('dbUser'),
  132.                 'dbPass'     => Config::get('dbPass'),
  133.                 'dbDatabase' => Config::get('dbDatabase')
  134.             );
  135.             $arrConfig array_merge($arrDefaultConfig$arrCustomConfig);
  136.         }
  137.         // Sort the array before generating the key
  138.         ksort($arrConfig);
  139.         $strKey md5(implode(''$arrConfig));
  140.         if (!isset(static::$arrInstances[$strKey]))
  141.         {
  142.             static::$arrInstances[$strKey] = new static($arrConfig);
  143.         }
  144.         return static::$arrInstances[$strKey];
  145.     }
  146.     /**
  147.      * Prepare a query and return a Statement object
  148.      *
  149.      * @param string $strQuery The query string
  150.      *
  151.      * @return Statement The Statement object
  152.      */
  153.     public function prepare($strQuery)
  154.     {
  155.         $objStatement = new Statement($this->resConnection);
  156.         return $objStatement->prepare($strQuery);
  157.     }
  158.     /**
  159.      * Execute a query and return a Result object
  160.      *
  161.      * @param string $strQuery The query string
  162.      *
  163.      * @return Result The Result object
  164.      */
  165.     public function execute($strQuery)
  166.     {
  167.         return $this->prepare($strQuery)->execute();
  168.     }
  169.     /**
  170.      * Execute a statement and return the number of affected rows
  171.      *
  172.      * @param string $strQuery The query string
  173.      *
  174.      * @return int The number of affected rows
  175.      */
  176.     public function executeStatement(string $strQuery): int
  177.     {
  178.         return (int) $this->resConnection->executeStatement($strQuery);
  179.     }
  180.     /**
  181.      * Execute a raw query and return a Result object
  182.      *
  183.      * @param string $strQuery The query string
  184.      *
  185.      * @return Result The Result object
  186.      */
  187.     public function query($strQuery)
  188.     {
  189.         $objStatement = new Statement($this->resConnection);
  190.         return $objStatement->query($strQuery);
  191.     }
  192.     /**
  193.      * Auto-generate a FIND_IN_SET() statement
  194.      *
  195.      * @param string  $strKey     The field name
  196.      * @param mixed   $varSet     The set to find the key in
  197.      * @param boolean $blnIsField If true, the set will not be quoted
  198.      *
  199.      * @return string The FIND_IN_SET() statement
  200.      */
  201.     public function findInSet($strKey$varSet$blnIsField=false)
  202.     {
  203.         if (\is_array($varSet))
  204.         {
  205.             $varSet implode(','$varSet);
  206.         }
  207.         if ($blnIsField)
  208.         {
  209.             $varSet = static::quoteIdentifier($varSet);
  210.         }
  211.         else
  212.         {
  213.             $varSet $this->resConnection->quote($varSet);
  214.         }
  215.         return "FIND_IN_SET(" . static::quoteIdentifier($strKey) . ", " $varSet ")";
  216.     }
  217.     /**
  218.      * Return all tables as array
  219.      *
  220.      * @param string  $strDatabase The database name
  221.      * @param boolean $blnNoCache  If true, the cache will be bypassed
  222.      *
  223.      * @return array An array of table names
  224.      */
  225.     public function listTables($strDatabase=null$blnNoCache=false)
  226.     {
  227.         if ($blnNoCache || !isset($this->arrTablesCache[$strDatabase]))
  228.         {
  229.             $strOldDatabase $this->resConnection->getDatabase();
  230.             // Change the database
  231.             if ($strDatabase !== null && $strDatabase != $strOldDatabase)
  232.             {
  233.                 $this->setDatabase($strDatabase);
  234.             }
  235.             $this->arrTablesCache[$strDatabase] = $this->resConnection->getSchemaManager()->listTableNames();
  236.             // Restore the database
  237.             if ($strDatabase !== null && $strDatabase != $strOldDatabase)
  238.             {
  239.                 $this->setDatabase($strOldDatabase);
  240.             }
  241.         }
  242.         return $this->arrTablesCache[$strDatabase];
  243.     }
  244.     /**
  245.      * Determine if a particular database table exists
  246.      *
  247.      * @param string  $strTable    The table name
  248.      * @param string  $strDatabase The optional database name
  249.      * @param boolean $blnNoCache  If true, the cache will be bypassed
  250.      *
  251.      * @return boolean True if the table exists
  252.      */
  253.     public function tableExists($strTable$strDatabase=null$blnNoCache=false)
  254.     {
  255.         if (!$strTable)
  256.         {
  257.             return false;
  258.         }
  259.         return \in_array($strTable$this->listTables($strDatabase$blnNoCache));
  260.     }
  261.     /**
  262.      * Return all columns of a particular table as array
  263.      *
  264.      * @param string  $strTable   The table name
  265.      * @param boolean $blnNoCache If true, the cache will be bypassed
  266.      *
  267.      * @return array An array of column names
  268.      */
  269.     public function listFields($strTable$blnNoCache=false)
  270.     {
  271.         if ($blnNoCache || !isset($this->arrCache[$strTable]))
  272.         {
  273.             $arrReturn = array();
  274.             $objFields $this->query("SHOW FULL COLUMNS FROM $strTable");
  275.             while ($objFields->next())
  276.             {
  277.                 $arrTmp = array();
  278.                 $arrChunks preg_split('/(\([^)]+\))/'$objFields->Type, -1PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  279.                 $arrTmp['name'] = $objFields->Field;
  280.                 $arrTmp['type'] = $arrChunks[0];
  281.                 if (!empty($arrChunks[1]))
  282.                 {
  283.                     $arrChunks[1] = str_replace(array('('')'), ''$arrChunks[1]);
  284.                     // Handle enum fields (see #6387)
  285.                     if ($arrChunks[0] == 'enum')
  286.                     {
  287.                         $arrTmp['length'] = $arrChunks[1];
  288.                     }
  289.                     else
  290.                     {
  291.                         $arrSubChunks explode(','$arrChunks[1]);
  292.                         $arrTmp['length'] = trim($arrSubChunks[0]);
  293.                         if (!empty($arrSubChunks[1]))
  294.                         {
  295.                             $arrTmp['precision'] = trim($arrSubChunks[1]);
  296.                         }
  297.                     }
  298.                 }
  299.                 if (!empty($arrChunks[2]))
  300.                 {
  301.                     $arrTmp['attributes'] = trim($arrChunks[2]);
  302.                 }
  303.                 if ($objFields->Key)
  304.                 {
  305.                     switch ($objFields->Key)
  306.                     {
  307.                         case 'PRI':
  308.                             $arrTmp['index'] = 'PRIMARY';
  309.                             break;
  310.                         case 'UNI':
  311.                             $arrTmp['index'] = 'UNIQUE';
  312.                             break;
  313.                         case 'MUL':
  314.                             // Ignore
  315.                             break;
  316.                         default:
  317.                             $arrTmp['index'] = 'KEY';
  318.                             break;
  319.                     }
  320.                 }
  321.                 // Do not modify the order!
  322.                 $arrTmp['collation'] = $objFields->Collation;
  323.                 $arrTmp['null'] = ($objFields->Null == 'YES') ? 'NULL' 'NOT NULL';
  324.                 $arrTmp['default'] = $objFields->Default;
  325.                 $arrTmp['extra'] = $objFields->Extra;
  326.                 $arrTmp['origtype'] = $objFields->Type;
  327.                 $arrReturn[] = $arrTmp;
  328.             }
  329.             $objIndex $this->query("SHOW INDEXES FROM `$strTable`");
  330.             while ($objIndex->next())
  331.             {
  332.                 $strColumnName $objIndex->Column_name;
  333.                 if ($objIndex->Sub_part)
  334.                 {
  335.                     $strColumnName .= '(' $objIndex->Sub_part ')';
  336.                 }
  337.                 $arrReturn[$objIndex->Key_name]['name'] = $objIndex->Key_name;
  338.                 $arrReturn[$objIndex->Key_name]['type'] = 'index';
  339.                 $arrReturn[$objIndex->Key_name]['index_fields'][] = $strColumnName;
  340.                 $arrReturn[$objIndex->Key_name]['index'] = (($objIndex->Non_unique == 0) ? 'UNIQUE' 'KEY');
  341.             }
  342.             $this->arrCache[$strTable] = $arrReturn;
  343.         }
  344.         return $this->arrCache[$strTable];
  345.     }
  346.     /**
  347.      * Determine if a particular column exists
  348.      *
  349.      * @param string  $strField   The field name
  350.      * @param string  $strTable   The table name
  351.      * @param boolean $blnNoCache If true, the cache will be bypassed
  352.      *
  353.      * @return boolean True if the field exists
  354.      */
  355.     public function fieldExists($strField$strTable$blnNoCache=false)
  356.     {
  357.         if (!$strField || !$strTable)
  358.         {
  359.             return false;
  360.         }
  361.         foreach ($this->listFields($strTable$blnNoCache) as $arrField)
  362.         {
  363.             if ($arrField['name'] == $strField && $arrField['type'] != 'index')
  364.             {
  365.                 return true;
  366.             }
  367.         }
  368.         return false;
  369.     }
  370.     /**
  371.      * Determine if a particular index exists
  372.      *
  373.      * @param string  $strName    The index name
  374.      * @param string  $strTable   The table name
  375.      * @param boolean $blnNoCache If true, the cache will be bypassed
  376.      *
  377.      * @return boolean True if the index exists
  378.      */
  379.     public function indexExists($strName$strTable$blnNoCache=false)
  380.     {
  381.         if (!$strName || !$strTable)
  382.         {
  383.             return false;
  384.         }
  385.         foreach ($this->listFields($strTable$blnNoCache) as $arrField)
  386.         {
  387.             if ($arrField['name'] == $strName && $arrField['type'] == 'index')
  388.             {
  389.                 return true;
  390.             }
  391.         }
  392.         return false;
  393.     }
  394.     /**
  395.      * Return the field names of a particular table as array
  396.      *
  397.      * @param string  $strTable   The table name
  398.      * @param boolean $blnNoCache If true, the cache will be bypassed
  399.      *
  400.      * @return array An array of field names
  401.      */
  402.     public function getFieldNames($strTable$blnNoCache=false)
  403.     {
  404.         $arrNames = array();
  405.         $arrFields $this->listFields($strTable$blnNoCache);
  406.         foreach ($arrFields as $arrField)
  407.         {
  408.             if ($arrField['type'] != 'index')
  409.             {
  410.                 $arrNames[] = $arrField['name'];
  411.             }
  412.         }
  413.         return $arrNames;
  414.     }
  415.     /**
  416.      * Check whether a field value in the database is unique
  417.      *
  418.      * @param string  $strTable The table name
  419.      * @param string  $strField The field name
  420.      * @param mixed   $varValue The field value
  421.      * @param integer $intId    The ID of a record to exempt
  422.      *
  423.      * @return boolean True if the field value is unique
  424.      */
  425.     public function isUniqueValue($strTable$strField$varValue$intId=null)
  426.     {
  427.         $strQuery "SELECT * FROM $strTable WHERE " . static::quoteIdentifier($strField) . "=?";
  428.         if ($intId !== null)
  429.         {
  430.             $strQuery .= " AND id!=?";
  431.         }
  432.         $objUnique $this->prepare($strQuery)
  433.                           ->limit(1)
  434.                           ->execute($varValue$intId);
  435.         return $objUnique->numRows false true;
  436.     }
  437.     /**
  438.      * Return the IDs of all child records of a particular record (see #2475)
  439.      *
  440.      * @param mixed   $arrParentIds An array of parent IDs
  441.      * @param string  $strTable     The table name
  442.      * @param boolean $blnSorting   True if the table has a sorting field
  443.      * @param array   $arrReturn    The array to be returned
  444.      * @param string  $strWhere     Additional WHERE condition
  445.      *
  446.      * @return array An array of child record IDs
  447.      */
  448.     public function getChildRecords($arrParentIds$strTable$blnSorting=false$arrReturn=array(), $strWhere='')
  449.     {
  450.         if (!\is_array($arrParentIds))
  451.         {
  452.             $arrParentIds = array($arrParentIds);
  453.         }
  454.         // Remove zero IDs
  455.         $arrParentIds array_filter(array_map('\intval'$arrParentIds));
  456.         if (empty($arrParentIds))
  457.         {
  458.             return $arrReturn;
  459.         }
  460.         $objChilds $this->query("SELECT id, pid FROM " $strTable " WHERE pid IN(" implode(','$arrParentIds) . ")" . ($strWhere " AND $strWhere"") . ($blnSorting " ORDER BY " $this->findInSet('pid'$arrParentIds) . ", sorting" ""));
  461.         if ($objChilds->numRows 0)
  462.         {
  463.             if ($blnSorting)
  464.             {
  465.                 $arrChilds = array();
  466.                 $arrOrdered = array();
  467.                 while ($objChilds->next())
  468.                 {
  469.                     $arrChilds[] = $objChilds->id;
  470.                     $arrOrdered[$objChilds->pid][] = $objChilds->id;
  471.                 }
  472.                 foreach (array_reverse(array_keys($arrOrdered)) as $pid)
  473.                 {
  474.                     $pos = (int) array_search($pid$arrReturn);
  475.                     ArrayUtil::arrayInsert($arrReturn$pos+1$arrOrdered[$pid]);
  476.                 }
  477.                 $arrReturn $this->getChildRecords($arrChilds$strTable$blnSorting$arrReturn$strWhere);
  478.             }
  479.             else
  480.             {
  481.                 $arrChilds $objChilds->fetchEach('id');
  482.                 $arrReturn array_merge($arrChilds$this->getChildRecords($arrChilds$strTable$blnSorting$arrReturn$strWhere));
  483.             }
  484.         }
  485.         return array_map('\intval'$arrReturn);
  486.     }
  487.     /**
  488.      * Return the IDs of all parent records of a particular record
  489.      *
  490.      * @param integer $intId    The ID of the record
  491.      * @param string  $strTable The table name
  492.      * @param bool    $skipId   Omit the provided ID in the result set
  493.      *
  494.      * @return array An array of parent record IDs
  495.      */
  496.     public function getParentRecords($intId$strTablebool $skipId false)
  497.     {
  498.         // Limit to a nesting level of 10
  499.         $ids $this->prepare("SELECT id, @pid:=pid FROM $strTable WHERE id=?" str_repeat(" UNION SELECT id, @pid:=pid FROM $strTable WHERE id=@pid"9))
  500.                     ->execute($intId)
  501.                     ->fetchEach('id');
  502.         // Trigger recursion in case our query returned exactly 10 IDs in which case we might have higher parent records
  503.         if (\count($ids) === 10)
  504.         {
  505.             $ids array_merge($ids$this->getParentRecords(end($ids), $strTabletrue));
  506.         }
  507.         if ($skipId && ($key array_search($intId$ids)) !== false)
  508.         {
  509.             unset($ids[$key]);
  510.         }
  511.         return array_map('\intval'array_values($ids));
  512.     }
  513.     /**
  514.      * Change the current database
  515.      *
  516.      * @param string $strDatabase The name of the target database
  517.      */
  518.     public function setDatabase($strDatabase)
  519.     {
  520.         $this->resConnection->executeStatement("USE $strDatabase");
  521.     }
  522.     /**
  523.      * Begin a transaction
  524.      */
  525.     public function beginTransaction()
  526.     {
  527.         $this->resConnection->beginTransaction();
  528.     }
  529.     /**
  530.      * Commit a transaction
  531.      */
  532.     public function commitTransaction()
  533.     {
  534.         $this->resConnection->commit();
  535.     }
  536.     /**
  537.      * Rollback a transaction
  538.      */
  539.     public function rollbackTransaction()
  540.     {
  541.         $this->resConnection->rollBack();
  542.     }
  543.     /**
  544.      * Lock one or more tables
  545.      *
  546.      * @param array $arrTables An array of table names to be locked
  547.      */
  548.     public function lockTables($arrTables)
  549.     {
  550.         $arrLocks = array();
  551.         foreach ($arrTables as $table=>$mode)
  552.         {
  553.             $arrLocks[] = $this->resConnection->quoteIdentifier($table) . ' ' $mode;
  554.         }
  555.         $this->resConnection->executeStatement('LOCK TABLES ' implode(', '$arrLocks) . ';');
  556.     }
  557.     /**
  558.      * Unlock all tables
  559.      */
  560.     public function unlockTables()
  561.     {
  562.         $this->resConnection->executeStatement('UNLOCK TABLES;');
  563.     }
  564.     /**
  565.      * Return the table size in bytes
  566.      *
  567.      * @param string $strTable The table name
  568.      *
  569.      * @return integer The table size in bytes
  570.      */
  571.     public function getSizeOf($strTable)
  572.     {
  573.         try
  574.         {
  575.             // MySQL 8 compatibility
  576.             $this->resConnection->executeStatement('SET @@SESSION.information_schema_stats_expiry = 0');
  577.         }
  578.         catch (DriverException $e)
  579.         {
  580.         }
  581.         $status $this->resConnection->fetchAssociative('SHOW TABLE STATUS LIKE ' $this->resConnection->quote($strTable));
  582.         return $status['Data_length'] + $status['Index_length'];
  583.     }
  584.     /**
  585.      * Return the next autoincrement ID of a table
  586.      *
  587.      * @param string $strTable The table name
  588.      *
  589.      * @return integer The autoincrement ID
  590.      */
  591.     public function getNextId($strTable)
  592.     {
  593.         try
  594.         {
  595.             // MySQL 8 compatibility
  596.             $this->resConnection->executeStatement('SET @@SESSION.information_schema_stats_expiry = 0');
  597.         }
  598.         catch (DriverException $e)
  599.         {
  600.         }
  601.         $status $this->resConnection->fetchAssociative('SHOW TABLE STATUS LIKE ' $this->resConnection->quote($strTable));
  602.         return $status['Auto_increment'];
  603.     }
  604.     /**
  605.      * Return a universal unique identifier
  606.      *
  607.      * @return string The UUID string
  608.      */
  609.     public function getUuid()
  610.     {
  611.         static $ids;
  612.         if (empty($ids))
  613.         {
  614.             $ids $this->resConnection->fetchFirstColumn(implode(' UNION ALL 'array_fill(010"SELECT UNHEX(REPLACE(UUID(), '-', '')) AS uuid")));
  615.         }
  616.         return array_pop($ids);
  617.     }
  618.     /**
  619.      * Quote the column name if it is a reserved word
  620.      *
  621.      * @param string $strName
  622.      *
  623.      * @return string
  624.      */
  625.     public static function quoteIdentifier($strName)
  626.     {
  627.         // Quoted already or not an identifier (AbstractPlatform::quoteIdentifier() handles table.column so also allow . here)
  628.         if (!preg_match('/^[A-Za-z0-9_$.]+$/'$strName))
  629.         {
  630.             return $strName;
  631.         }
  632.         return System::getContainer()->get('database_connection')->quoteIdentifier($strName);
  633.     }
  634.     /**
  635.      * Execute a query and do not cache the result
  636.      *
  637.      * @param string $strQuery The query string
  638.      *
  639.      * @return Result The Result object
  640.      *
  641.      * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  642.      *             Use Database::execute() instead.
  643.      */
  644.     public function executeUncached($strQuery)
  645.     {
  646.         trigger_deprecation('contao/core-bundle''4.0''Using "Contao\Database::executeUncached()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Database::execute()" instead.');
  647.         return $this->execute($strQuery);
  648.     }
  649.     /**
  650.      * Always execute the query and add or replace an existing cache entry
  651.      *
  652.      * @param string $strQuery The query string
  653.      *
  654.      * @return Result The Result object
  655.      *
  656.      * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  657.      *             Use Database::execute() instead.
  658.      */
  659.     public function executeCached($strQuery)
  660.     {
  661.         trigger_deprecation('contao/core-bundle''4.0''Using "Contao\Database::executeCached()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Database::execute()" instead.');
  662.         return $this->execute($strQuery);
  663.     }
  664. }
  665. class_alias(Database::class, 'Database');