Unverified Commit 5478a95d authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3186934 by mondrake, daffie, donquixote, alexpott: Introduce an...

Issue #3186934 by mondrake, daffie, donquixote, alexpott: Introduce an ExceptionHandler class in the database API, deprecate Connection::handleQueryException
parent c01dee41
Loading
Loading
Loading
Loading
+65 −15
Changes for core/lib/Drupal/Core/Database/Connection.php: 65 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -546,18 +546,33 @@ public function getFullQualifiedTableName($table) {
   *
   * @return \Drupal\Core\Database\StatementInterface
   *   A PDO prepared statement ready for its execute() method.
   *
   * @throws \Drupal\Core\Database\DatabaseExceptionWrapper
   */
  public function prepareStatement(string $query, array $options): StatementInterface {
    $query = $this->prefixTables($query);
    if (!($options['allow_square_brackets'] ?? FALSE)) {
      $query = $this->quoteIdentifiers($query);
    }
    try {
      // @todo in Drupal 10, only return the StatementWrapper.
      // @see https://www.drupal.org/node/3177490
    return $this->statementWrapperClass ?
      $statement = $this->statementWrapperClass ?
        new $this->statementWrapperClass($this, $this->connection, $query, $options['pdo'] ?? []) :
        $this->connection->prepare($query, $options['pdo'] ?? []);
    }
    catch (\Exception $e) {
      $this->exceptionHandler()->handleStatementException($e, $query, $options);
    }
    // BC layer: $options['throw_exception'] = FALSE or a \PDO::prepare() call
    // returning false would lead to returning a value that fails the return
    // typehint. Throw an exception in that case.
    // @todo in Drupal 10, remove the check.
    if (!isset($statement) || !$statement instanceof StatementInterface) {
      throw new DatabaseExceptionWrapper("Statement preparation failure for query: $query");
    }
    return $statement;
  }

  /**
   * Prepares a query string and returns the prepared statement.
@@ -793,21 +808,10 @@ public function query($query, array $args = [], $options = []) {
    $options += $this->defaultOptions();
    assert(!isset($options['target']), 'Passing "target" option to query() has no effect. See https://www.drupal.org/node/2993033');

    try {
    // We allow either a pre-bound statement object (deprecated) or a literal
    // string. In either case, we want to end up with an executed statement
      // object, which we pass to PDOStatement::execute.
      if ($query instanceof StatementInterface) {
        @trigger_error('Passing a StatementInterface object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
        $stmt = $query;
        $stmt->execute(NULL, $options);
      }
      elseif ($query instanceof \PDOStatement) {
        @trigger_error('Passing a \\PDOStatement object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
        $stmt = $query;
        $stmt->execute();
      }
      else {
    // object, which we pass to StatementInterface::execute.
    if (is_string($query)) {
      $this->expandArguments($query, $args);
      // To protect against SQL injection, Drupal only supports executing one
      // statement at a time.  Thus, the presence of a SQL delimiter (the
@@ -824,8 +828,26 @@ public function query($query, array $args = [], $options = []) {
        throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.');
      }
      $stmt = $this->prepareStatement($query, $options);
    }
    elseif ($query instanceof StatementInterface) {
      @trigger_error('Passing a StatementInterface object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
      $stmt = $query;
    }
    elseif ($query instanceof \PDOStatement) {
      @trigger_error('Passing a \\PDOStatement object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
      $stmt = $query;
    }

    try {
      if (is_string($query)) {
        $stmt->execute($args, $options);
      }
      elseif ($query instanceof StatementInterface) {
        $stmt->execute(NULL, $options);
      }
      elseif ($query instanceof \PDOStatement) {
        $stmt->execute();
      }

      // Depending on the type of query we may need to return a different value.
      // See DatabaseConnection::defaultOptions() for a description of each
@@ -847,15 +869,21 @@ public function query($query, array $args = [], $options = []) {

        default:
          throw new \PDOException('Invalid return directive: ' . $options['return']);

      }
    }
    catch (\PDOException $e) {
    catch (\Exception $e) {
      // Most database drivers will return NULL here, but some of them
      // (e.g. the SQLite driver) may need to re-run the query, so the return
      // value will be the same as for static::query().
      if (is_string($query)) {
        return $this->exceptionHandler()->handleExecutionException($e, $stmt, $args, $options);
      }
      else {
        return $this->handleQueryException($e, $query, $args, $options);
      }
    }
  }

  /**
   * Wraps and re-throws any PDO exception thrown by static::query().
@@ -876,8 +904,15 @@ public function query($query, array $args = [], $options = []) {
   *
   * @throws \Drupal\Core\Database\DatabaseExceptionWrapper
   * @throws \Drupal\Core\Database\IntegrityConstraintViolationException
   *
   * @deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. Get a
   *   handler through $this->exceptionHandler() instead, and use one of its
   *   methods.
   *
   * @see https://www.drupal.org/node/3187222
   */
  protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) {
    @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED);
    if ($options['throw_exception']) {
      // Wrap the exception in another exception, because PHP does not allow
      // overriding Exception::getMessage(). Its message is the extra database
@@ -999,6 +1034,10 @@ public function getDriverClass($class) {
            $this->driverClasses[$class] = Delete::class;
            break;

          case 'ExceptionHandler':
            $this->driverClasses[$class] = ExceptionHandler::class;
            break;

          case 'Insert':
            $this->driverClasses[$class] = Insert::class;
            break;
@@ -1039,6 +1078,17 @@ public function getDriverClass($class) {
    return $this->driverClasses[$class];
  }

  /**
   * Returns the database exceptions handler.
   *
   * @return \Drupal\Core\Database\ExceptionHandler
   *   The database exceptions handler.
   */
  public function exceptionHandler() {
    $class = $this->getDriverClass('ExceptionHandler');
    return new $class();
  }

  /**
   * Prepares and returns a SELECT query object.
   *
+1 −20
Changes for core/lib/Drupal/Core/Database/Driver/mysql/Connection.php: 1 added line, 20 removed lines.
Original line number Diff line number Diff line
@@ -10,7 +10,6 @@
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Database\Connection as DatabaseConnection;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\TransactionNoActiveException;

/**
@@ -89,25 +88,6 @@ class Connection extends DatabaseConnection {
   */
  protected $identifierQuotes = ['"', '"'];

  /**
   * {@inheritdoc}
   */
  public function query($query, array $args = [], $options = []) {
    try {
      return parent::query($query, $args, $options);
    }
    catch (DatabaseException $e) {
      if ($e->getPrevious()->errorInfo[1] == 1153) {
        // If a max_allowed_packet error occurs the message length is truncated.
        // This should prevent the error from recurring if the exception is
        // logged to the database using dblog or the like.
        $message = Unicode::truncateBytes($e->getMessage(), self::MIN_MAX_ALLOWED_PACKET);
        $e = new DatabaseExceptionWrapper($message, $e->getCode(), $e->getPrevious());
      }
      throw $e;
    }
  }

  /**
   * {@inheritdoc}
   */
@@ -118,6 +98,7 @@ protected function handleQueryException(\PDOException $e, $query, array $args =
    // drivers do, to avoid the parent class to throw a generic
    // DatabaseExceptionWrapper instead.
    if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 1364) {
      @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED);
      $query_string = ($query instanceof StatementInterface) ? $query->getQueryString() : $query;
      $message = $e->getMessage() . ": " . $query_string . "; " . print_r($args, TRUE);
      throw new IntegrityConstraintViolationException($message, is_int($e->getCode()) ? $e->getCode() : 0, $e);
+56 −0
Changes for core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php: 56 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Database\Driver\mysql;

use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\ExceptionHandler as BaseExceptionHandler;
use Drupal\Core\Database\IntegrityConstraintViolationException;
use Drupal\Core\Database\StatementInterface;

/**
 * MySql database exception handler class.
 */
class ExceptionHandler extends BaseExceptionHandler {

  /**
   * {@inheritdoc}
   */
  public function handleExecutionException(\Exception $exception, StatementInterface $statement, array $arguments = [], array $options = []): void {
    if (!($options['throw_exception'] ?? TRUE)) {
      return;
    }

    if ($exception instanceof \PDOException) {
      // Wrap the exception in another exception, because PHP does not allow
      // overriding Exception::getMessage(). Its message is the extra database
      // debug information.
      $code = is_int($exception->getCode()) ? $exception->getCode() : 0;

      // If a max_allowed_packet error occurs the message length is truncated.
      // This should prevent the error from recurring if the exception is logged
      // to the database using dblog or the like.
      if (($exception->errorInfo[1] ?? NULL) === 1153) {
        $message = Unicode::truncateBytes($exception->getMessage(), Connection::MIN_MAX_ALLOWED_PACKET);
        throw new DatabaseExceptionWrapper($message, $code, $exception);
      }

      $message = $exception->getMessage() . ": " . $statement->getQueryString() . "; " . print_r($arguments, TRUE);

      // SQLSTATE 23xxx errors indicate an integrity constraint violation. Also,
      // in case of attempted INSERT of a record with an undefined column and no
      // default value indicated in schema, MySql returns a 1364 error code.
      if (
        substr($exception->getCode(), -6, -3) == '23' ||
        ($exception->errorInfo[1] ?? NULL) === 1364
      ) {
        throw new IntegrityConstraintViolationException($message, $code, $exception);
      }

      throw new DatabaseExceptionWrapper($message, 0, $exception);
    }

    throw $exception;
  }

}
+1 −0
Changes for core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -373,6 +373,7 @@ protected function handleQueryException(\PDOException $e, $query, array $args =
    // @see http://www.sqlite.org/faq.html#q15
    // @see http://www.sqlite.org/rescode.html#schema
    if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
      @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED);
      return $this->query($query, $args, $options);
    }

+6 −0
Changes for core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -90,6 +90,12 @@ public function execute($args = [], $options = []) {
      $return = parent::execute($args, $options);
    }
    catch (\PDOException $e) {
      // The database schema might be changed by another process in between the
      // time that the statement was prepared and the time the statement was run
      // (e.g. usually happens when running tests). In this case, we need to
      // re-run the query.
      // @see http://www.sqlite.org/faq.html#q15
      // @see http://www.sqlite.org/rescode.html#schema
      if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
        // The schema has changed. SQLite specifies that we must resend the query.
        $return = parent::execute($args, $options);
Loading