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

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