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

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\Database;
  10. use Contao\Database;
  11. use Doctrine\DBAL\Connection;
  12. use Doctrine\DBAL\Driver\Result as DoctrineResult;
  13. use Doctrine\DBAL\Exception\DriverException;
  14. use Doctrine\DBAL\ParameterType;
  15. /**
  16.  * Create and execute queries
  17.  *
  18.  * The class creates the database queries replacing the wildcards and escaping
  19.  * the values. It then executes the query and returns a result object.
  20.  *
  21.  * Usage:
  22.  *
  23.  *     $db = Database::getInstance();
  24.  *     $stmt = $db->prepare("SELECT * FROM tl_member WHERE city=?");
  25.  *     $stmt->limit(10);
  26.  *     $res = $stmt->execute('London');
  27.  *
  28.  * @property string  $query        The query string
  29.  * @property string  $error        The last error message
  30.  * @property integer $affectedRows The number of affected rows
  31.  * @property integer $insertId     The last insert ID
  32.  */
  33. class Statement
  34. {
  35.     /**
  36.      * Connection ID
  37.      * @var Connection
  38.      */
  39.     protected $resConnection;
  40.     /**
  41.      * Database statement
  42.      * @var DoctrineResult
  43.      */
  44.     protected $statement;
  45.     /**
  46.      * Query string
  47.      * @var string
  48.      */
  49.     protected $strQuery;
  50.     /**
  51.      * @var array
  52.      */
  53.     private $arrSetParams = array();
  54.     /**
  55.      * @var array
  56.      */
  57.     private $arrLastUsedParams = array();
  58.     /**
  59.      * Validate the connection resource and store the query string
  60.      *
  61.      * @param Connection $resConnection The connection resource
  62.      */
  63.     public function __construct(Connection $resConnection)
  64.     {
  65.         $this->resConnection $resConnection;
  66.     }
  67.     /**
  68.      * Return an object property
  69.      *
  70.      * @param string $strKey The property name
  71.      *
  72.      * @return mixed|null The property value or null
  73.      */
  74.     public function __get($strKey)
  75.     {
  76.         switch ($strKey)
  77.         {
  78.             case 'query':
  79.                 return $this->strQuery;
  80.             case 'affectedRows':
  81.                 return $this->statement->rowCount();
  82.             case 'insertId':
  83.                 return $this->resConnection->lastInsertId();
  84.         }
  85.         return null;
  86.     }
  87.     /**
  88.      * Prepare a query string so the following functions can handle it
  89.      *
  90.      * @param string $strQuery The query string
  91.      *
  92.      * @return Statement The statement object
  93.      *
  94.      * @throws \Exception If $strQuery is empty
  95.      */
  96.     public function prepare($strQuery)
  97.     {
  98.         if (!$strQuery)
  99.         {
  100.             throw new \Exception('Empty query string');
  101.         }
  102.         $this->strQuery trim($strQuery);
  103.         $this->arrLastUsedParams = array();
  104.         return $this;
  105.     }
  106.     /**
  107.      * Autogenerate the SET/VALUES subpart of a query from an associative array
  108.      *
  109.      * Usage:
  110.      *
  111.      *     $set = array(
  112.      *         'firstname' => 'Leo',
  113.      *         'lastname'  => 'Feyer'
  114.      *     );
  115.      *     $stmt->prepare("UPDATE tl_member %s")->set($set);
  116.      *
  117.      * @param array $arrParams The associative array
  118.      *
  119.      * @return Statement The statement object
  120.      */
  121.     public function set($arrParams)
  122.     {
  123.         if (substr_count($this->strQuery'%s') !== || !\in_array(strtoupper(substr($this->strQuery06)), array('INSERT''UPDATE'), true))
  124.         {
  125.             trigger_deprecation('contao/core-bundle''4.13''Using "%s()" is only supported for INSERT and UPDATE queries with the "%%s" placeholder. This will throw an exception in Contao 5.0.'__METHOD__);
  126.             return $this;
  127.         }
  128.         $this->arrSetParams array_values($arrParams);
  129.         $arrParamNames array_map(
  130.             static function ($strName)
  131.             {
  132.                 if (!preg_match('/^(?:[A-Za-z0-9_$]+|`[^`]+`)$/'$strName))
  133.                 {
  134.                     throw new \RuntimeException(sprintf('Invalid column name "%s" in %s()'$strName__METHOD__));
  135.                 }
  136.                 return Database::quoteIdentifier($strName);
  137.             },
  138.             array_keys($arrParams)
  139.         );
  140.         // INSERT
  141.         if (strncasecmp($this->strQuery'INSERT'6) === 0)
  142.         {
  143.             $strQuery sprintf(
  144.                 '(%s) VALUES (%s)',
  145.                 implode(', '$arrParamNames),
  146.                 implode(', 'array_fill(0\count($arrParams), '?'))
  147.             );
  148.         }
  149.         // UPDATE
  150.         else
  151.         {
  152.             if (!$arrParamNames)
  153.             {
  154.                 throw new \InvalidArgumentException('Set array must not be empty for UPDATE queries');
  155.             }
  156.             $strQuery 'SET ' implode('=?, '$arrParamNames) . '=?';
  157.         }
  158.         $this->strQuery str_replace('%s'$strQuery$this->strQuery);
  159.         return $this;
  160.     }
  161.     /**
  162.      * Handle limit and offset
  163.      *
  164.      * @param integer $intRows   The maximum number of rows
  165.      * @param integer $intOffset The number of rows to skip
  166.      *
  167.      * @return Statement The statement object
  168.      */
  169.     public function limit($intRows$intOffset=0)
  170.     {
  171.         if ($intRows <= 0)
  172.         {
  173.             // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible
  174.             $intRows '18446744073709551615';
  175.         }
  176.         if ($intOffset 0)
  177.         {
  178.             $intOffset 0;
  179.         }
  180.         if (strncasecmp($this->strQuery'SELECT'6) === 0)
  181.         {
  182.             $this->strQuery .= ' LIMIT ' $intOffset ',' $intRows;
  183.         }
  184.         else
  185.         {
  186.             $this->strQuery .= ' LIMIT ' $intRows;
  187.         }
  188.         return $this;
  189.     }
  190.     /**
  191.      * Execute the query and return the result object
  192.      *
  193.      * @return Result The result object
  194.      */
  195.     public function execute()
  196.     {
  197.         $arrParams \func_get_args();
  198.         if (\count($arrParams) === && \is_array($arrParams[0]))
  199.         {
  200.             trigger_deprecation('contao/core-bundle''4.13''Using "%s()" with an array parameter has been deprecated and will no longer work in Contao 5.0. Use argument unpacking via ... instead."'__METHOD__);
  201.             $arrParams array_values($arrParams[0]);
  202.         }
  203.         return $this->query(''array_merge($this->arrSetParams$arrParams));
  204.     }
  205.     /**
  206.      * Directly send a query string to the database
  207.      *
  208.      * @param string $strQuery The query string
  209.      *
  210.      * @return Result|Statement The result object or the statement object if there is no result set
  211.      *
  212.      * @throws \Exception If the query string is empty
  213.      */
  214.     public function query($strQuery='', array $arrParams = array(), array $arrTypes = array())
  215.     {
  216.         if (!empty($strQuery))
  217.         {
  218.             $this->strQuery trim($strQuery);
  219.         }
  220.         // Make sure there is a query string
  221.         if (!$this->strQuery)
  222.         {
  223.             throw new \Exception('Empty query string');
  224.         }
  225.         foreach ($arrParams as $key => $varParam)
  226.         {
  227.             // Automatically set type to boolean when no type is defined,
  228.             // otherwise "false" will be converted to an empty string.
  229.             if (null === ($arrTypes[$key] ?? null))
  230.             {
  231.                 $arrTypes[$key] = \is_bool($varParam) ? ParameterType::BOOLEAN ParameterType::STRING;
  232.             }
  233.             if (\is_string($varParam) || \is_bool($varParam) || \is_float($varParam) || \is_int($varParam) || $varParam === null)
  234.             {
  235.                 continue;
  236.             }
  237.             $arrParams[$key] = serialize($varParam);
  238.         }
  239.         $this->arrLastUsedParams $arrParams;
  240.         // Execute the query
  241.         // TODO: remove the try/catch block in Contao 5.0
  242.         try
  243.         {
  244.             $this->statement $this->resConnection->executeQuery($this->strQuery$arrParams$arrTypes);
  245.         }
  246.         catch (DriverException|\ArgumentCountError $exception)
  247.         {
  248.             // SQLSTATE[HY000]: This command is not supported in the prepared statement protocol
  249.             if ($exception->getCode() === 1295)
  250.             {
  251.                 $this->resConnection->executeStatement($this->strQuery$arrParams$arrTypes);
  252.                 trigger_deprecation('contao/core-bundle''4.13''Using "%s()" for statements (instead of queries) has been deprecated and will no longer work in Contao 5.0. Use "%s::executeStatement()" instead.'__METHOD__Connection::class);
  253.                 return $this;
  254.             }
  255.             if (!$arrParams)
  256.             {
  257.                 throw $exception;
  258.             }
  259.             $intTokenCount substr_count(preg_replace("/('[^']*')/"''$this->strQuery), '?');
  260.             if (\count($arrParams) <= $intTokenCount)
  261.             {
  262.                 throw $exception;
  263.             }
  264.             // If we get here, there are more parameters than tokens, so we slice the array and try to execute the query again
  265.             $this->statement $this->resConnection->executeQuery($this->strQuery\array_slice($arrParams0$intTokenCount), $arrTypes);
  266.             // Only trigger the deprecation if the parameter count was the reason for the exception and the previous call did not throw
  267.             if ($this->arrLastUsedParams === array(null))
  268.             {
  269.                 trigger_deprecation('contao/core-bundle''4.13''Using "%s::execute(null)" has been deprecated and will no longer work in Contao 5.0. Omit the NULL parameters instead.'__CLASS__);
  270.             }
  271.             else
  272.             {
  273.                 trigger_deprecation('contao/core-bundle''4.13''Passing more parameters than "?" tokens has been deprecated and will no longer work in Contao 5.0. Use the correct number of parameters instead.');
  274.             }
  275.         }
  276.         // No result set available
  277.         if ($this->statement->columnCount() < 1)
  278.         {
  279.             return $this;
  280.         }
  281.         // Instantiate a result object
  282.         return new Result($this->statement$this->strQuery);
  283.     }
  284.     /**
  285.      * Replace the wildcards in the query string
  286.      *
  287.      * @param array $arrValues The values array
  288.      *
  289.      * @throws \Exception If $arrValues has too few values to replace the wildcards in the query string
  290.      *
  291.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0.
  292.      */
  293.     protected function replaceWildcards($arrValues)
  294.     {
  295.         trigger_deprecation('contao/core-bundle''4.13''Using "%s()" has been deprecated and will no longer work in Contao 5.0.'__METHOD__);
  296.         $arrValues $this->escapeParams($arrValues);
  297.         $this->strQuery preg_replace('/(?<!%)%([^bcdufosxX%])/''%%$1'$this->strQuery);
  298.         // Replace wildcards
  299.         if (!$this->strQuery = @vsprintf($this->strQuery$arrValues))
  300.         {
  301.             throw new \Exception('Too few arguments to build the query string');
  302.         }
  303.     }
  304.     /**
  305.      * Escape the values and serialize objects and arrays
  306.      *
  307.      * @param array $arrValues The values array
  308.      *
  309.      * @return array The array with the escaped values
  310.      *
  311.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0.
  312.      */
  313.     protected function escapeParams($arrValues)
  314.     {
  315.         trigger_deprecation('contao/core-bundle''4.13''Using "%s()" has been deprecated and will no longer work in Contao 5.0.'__METHOD__);
  316.         foreach ($arrValues as $k=>$v)
  317.         {
  318.             switch (\gettype($v))
  319.             {
  320.                 case 'string':
  321.                     $arrValues[$k] = $this->resConnection->quote($v);
  322.                     break;
  323.                 case 'boolean':
  324.                     $arrValues[$k] = ($v === true) ? 0;
  325.                     break;
  326.                 case 'object':
  327.                 case 'array':
  328.                     $arrValues[$k] = $this->resConnection->quote(serialize($v));
  329.                     break;
  330.                 default:
  331.                     $arrValues[$k] = $v ?? 'NULL';
  332.                     break;
  333.             }
  334.         }
  335.         return $arrValues;
  336.     }
  337.     /**
  338.      * Explain the current query
  339.      *
  340.      * @return string The explanation string
  341.      *
  342.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0.
  343.      */
  344.     public function explain()
  345.     {
  346.         trigger_deprecation('contao/core-bundle''4.13''Using "%s()" has been deprecated and will no longer work in Contao 5.0.'__METHOD__);
  347.         return $this->resConnection->fetchAssociative('EXPLAIN ' $this->strQuery$this->arrLastUsedParams);
  348.     }
  349.     /**
  350.      * Bypass the cache and always execute the query
  351.      *
  352.      * @return Result The result object
  353.      *
  354.      * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  355.      *             Use Statement::execute() instead.
  356.      */
  357.     public function executeUncached()
  358.     {
  359.         trigger_deprecation('contao/core-bundle''4.0''Using "Contao\Statement::executeUncached()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Statement::execute()" instead.');
  360.         return \call_user_func_array(array($this'execute'), \func_get_args());
  361.     }
  362.     /**
  363.      * Always execute the query and add or replace an existing cache entry
  364.      *
  365.      * @return Result The result object
  366.      *
  367.      * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  368.      *             Use Statement::execute() instead.
  369.      */
  370.     public function executeCached()
  371.     {
  372.         trigger_deprecation('contao/core-bundle''4.0''Using "Contao\Statement::executeCached()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Statement::execute()" instead.');
  373.         return \call_user_func_array(array($this'execute'), \func_get_args());
  374.     }
  375. }
  376. class_alias(Statement::class, 'Database\Statement');