From 36698f9c141bf1ff5ca8f225b842493b115c6996 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Thu, 31 Aug 2023 09:31:52 +0100 Subject: [PATCH] Issue #3345938 by mondrake, longwave, daffie, smustgrave: Deprecate support for unused \PDO::FETCH_* modes --- .../Drupal/Core/Database/FetchModeTrait.php | 53 +++++++++++++ .../Database/StatementPrefetchIterator.php | 10 +++ .../Database/StatementWrapperIterator.php | 10 +++ .../KernelTests/Core/Database/FetchTest.php | 6 ++ .../Tests/Core/Database/ConnectionTest.php | 74 +++++++++++++++++++ 5 files changed, 153 insertions(+) diff --git a/core/lib/Drupal/Core/Database/FetchModeTrait.php b/core/lib/Drupal/Core/Database/FetchModeTrait.php index d74f0ef8f128..b9e0018d519b 100644 --- a/core/lib/Drupal/Core/Database/FetchModeTrait.php +++ b/core/lib/Drupal/Core/Database/FetchModeTrait.php @@ -7,6 +7,41 @@ */ trait FetchModeTrait { + /** + * Map FETCH_* modes to their literal for inclusion in messages. + * + * @see https://github.com/php/php-src/blob/master/ext/pdo/php_pdo_driver.h#L65-L80 + */ + protected array $fetchModeLiterals = [ + \PDO::FETCH_DEFAULT => 'FETCH_DEFAULT', + \PDO::FETCH_LAZY => 'FETCH_LAZY', + \PDO::FETCH_ASSOC => 'FETCH_ASSOC', + \PDO::FETCH_NUM => 'FETCH_NUM', + \PDO::FETCH_BOTH => 'FETCH_BOTH', + \PDO::FETCH_OBJ => 'FETCH_OBJ', + \PDO::FETCH_BOUND => 'FETCH_BOUND', + \PDO::FETCH_COLUMN => 'FETCH_COLUMN', + \PDO::FETCH_CLASS => 'FETCH_CLASS', + \PDO::FETCH_INTO => 'FETCH_INTO', + \PDO::FETCH_FUNC => 'FETCH_FUNC', + \PDO::FETCH_NAMED => 'FETCH_NAMED', + \PDO::FETCH_KEY_PAIR => 'FETCH_KEY_PAIR', + \PDO::FETCH_CLASS | \PDO::FETCH_CLASSTYPE => 'FETCH_CLASS | FETCH_CLASSTYPE', + \PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE => 'FETCH_CLASS | FETCH_PROPS_LATE', + ]; + + /** + * The fetch modes supported. + */ + protected array $supportedFetchModes = [ + \PDO::FETCH_ASSOC, + \PDO::FETCH_CLASS, + \PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, + \PDO::FETCH_COLUMN, + \PDO::FETCH_NUM, + \PDO::FETCH_OBJ, + ]; + /** * Converts a row of data in FETCH_ASSOC format to FETCH_BOTH. * @@ -15,8 +50,14 @@ trait FetchModeTrait { * * @return array * The row in FETCH_BOTH format. + * + * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use + * supported modes only. + * + * @see https://www.drupal.org/node/3377999 */ protected function assocToBoth(array $rowAssoc): array { + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); // \PDO::FETCH_BOTH returns an array indexed by both the column name // and the column number. return $rowAssoc + array_values($rowAssoc); @@ -79,8 +120,14 @@ protected function assocToClass(array $rowAssoc, string $className, array $const * * @return object * The row in FETCH_CLASS format. + * + * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use + * supported modes only. + * + * @see https://www.drupal.org/node/3377999 */ protected function assocToClassType(array $rowAssoc, array $constructorArguments): object { + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); $className = array_shift($rowAssoc); return $this->assocToClass($rowAssoc, $className, $constructorArguments); } @@ -95,8 +142,14 @@ protected function assocToClassType(array $rowAssoc, array $constructorArguments * * @return object * The object receiving the data. + * + * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use + * supported modes only. + * + * @see https://www.drupal.org/node/3377999 */ protected function assocIntoObject(array $rowAssoc, object $object): object { + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); foreach ($rowAssoc as $column => $value) { $object->$column = $value; } diff --git a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php index e88b3131173a..d12d41f90338 100644 --- a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php +++ b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php @@ -188,6 +188,9 @@ public function getQueryString() { * {@inheritdoc} */ public function setFetchMode($mode, $a1 = NULL, $a2 = []) { + if (!in_array($mode, $this->supportedFetchModes)) { + @trigger_error('Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); + } $this->defaultFetchStyle = $mode; switch ($mode) { case \PDO::FETCH_CLASS: @@ -237,14 +240,18 @@ public function fetch($fetch_style = NULL, $cursor_orientation = \PDO::FETCH_ORI // Now, format the next prefetched record according to the required fetch // style. + // @todo in Drupal 11, remove arms for deprecated fetch modes. $rowAssoc = $this->data[$currentKey]; $row = match($fetch_style ?? $this->defaultFetchStyle) { \PDO::FETCH_ASSOC => $rowAssoc, + // @phpstan-ignore-next-line \PDO::FETCH_BOTH => $this->assocToBoth($rowAssoc), \PDO::FETCH_NUM => $this->assocToNum($rowAssoc), \PDO::FETCH_LAZY, \PDO::FETCH_OBJ => $this->assocToObj($rowAssoc), + // @phpstan-ignore-next-line \PDO::FETCH_CLASS | \PDO::FETCH_CLASSTYPE => $this->assocToClassType($rowAssoc, $this->fetchOptions['constructor_args']), \PDO::FETCH_CLASS => $this->assocToClass($rowAssoc, $this->fetchOptions['class'], $this->fetchOptions['constructor_args']), + // @phpstan-ignore-next-line \PDO::FETCH_INTO => $this->assocIntoObject($rowAssoc, $this->fetchOptions['object']), \PDO::FETCH_COLUMN => $this->assocToColumn($rowAssoc, $this->columnNames, $this->fetchOptions['column']), // @todo in Drupal 11, throw an exception if the fetch style cannot be @@ -297,6 +304,9 @@ public function fetchAssoc() { * {@inheritdoc} */ public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) { + if (isset($mode) && !in_array($mode, $this->supportedFetchModes)) { + @trigger_error('Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); + } $fetchStyle = $mode ?? $this->defaultFetchStyle; if (isset($column_index)) { $this->fetchOptions['column'] = $column_index; diff --git a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php index 440b27e96da4..59738a2fab7f 100644 --- a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php +++ b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php @@ -28,6 +28,7 @@ class StatementWrapperIterator implements \Iterator, StatementInterface { use StatementIteratorTrait; + use FetchModeTrait; /** * The client database Statement object. @@ -248,6 +249,9 @@ public function rowCount() { * {@inheritdoc} */ public function setFetchMode($mode, $a1 = NULL, $a2 = []) { + if (!in_array($mode, $this->supportedFetchModes)) { + @trigger_error('Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); + } // Call \PDOStatement::setFetchMode to set fetch mode. // \PDOStatement is picky about the number of arguments in some cases so we // need to be pass the exact number of arguments we where given. @@ -262,6 +266,9 @@ public function setFetchMode($mode, $a1 = NULL, $a2 = []) { * {@inheritdoc} */ public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) { + if (isset($mode) && !in_array($mode, $this->supportedFetchModes)) { + @trigger_error('Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); + } // Call \PDOStatement::fetchAll to fetch all rows. // \PDOStatement is picky about the number of arguments in some cases so we // need to pass the exact number of arguments we were given. @@ -285,6 +292,9 @@ public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = * {@inheritdoc} */ public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) { + if (isset($mode) && !in_array($mode, $this->supportedFetchModes)) { + @trigger_error('Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999', E_USER_DEPRECATED); + } // Call \PDOStatement::fetchAll to fetch all rows. // \PDOStatement is picky about the number of arguments in some cases so we // need to be pass the exact number of arguments we where given. diff --git a/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php b/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php index 8704425e2aae..73dc244bc39d 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php @@ -103,8 +103,11 @@ public function testQueryFetchObjectClass() { * The name of the class is determined from a value of the first column. * * @see \Drupal\Tests\system\Functional\Database\FakeRecord + * + * @group legacy */ public function testQueryFetchClasstype() { + $this->expectDeprecation('Fetch mode FETCH_CLASS | FETCH_CLASSTYPE is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999'); $records = []; $result = $this->connection->query('SELECT [classname], [name], [job] FROM {test_classtype} WHERE [age] = :age', [':age' => 26], ['fetch' => \PDO::FETCH_CLASS | \PDO::FETCH_CLASSTYPE]); foreach ($result as $record) { @@ -136,8 +139,11 @@ public function testQueryFetchNum() { /** * Confirms that we can fetch a record into a doubly-keyed array explicitly. + * + * @group legacy */ public function testQueryFetchBoth() { + $this->expectDeprecation('Fetch mode FETCH_BOTH is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999'); $records = []; $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_BOTH]); foreach ($result as $record) { diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php index 28670b717533..02c4c6b38fb3 100644 --- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php @@ -5,6 +5,7 @@ use Composer\Autoload\ClassLoader; use Drupal\Core\Database\Database; use Drupal\Core\Database\StatementPrefetch; +use Drupal\Core\Database\StatementPrefetchIterator; use Drupal\Tests\Core\Database\Stub\StubConnection; use Drupal\Tests\Core\Database\Stub\StubPDO; use Drupal\Tests\UnitTestCase; @@ -906,4 +907,77 @@ public function testStatementPrefetchDeprecation() { $this->assertInstanceOf(StatementPrefetch::class, $statement); } + /** + * Provides data for testSupportedFetchModes. + * + * @return array + * An associative array of simple arrays, each having the following + * elements: + * - a PDO fetch mode. + */ + public static function providerSupportedFetchModes(): array { + return [ + 'FETCH_ASSOC' => [\PDO::FETCH_ASSOC], + 'FETCH_CLASS' => [\PDO::FETCH_CLASS], + 'FETCH_CLASS | FETCH_PROPS_LATE' => [\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE], + 'FETCH_COLUMN' => [\PDO::FETCH_COLUMN], + 'FETCH_NUM' => [\PDO::FETCH_NUM], + 'FETCH_OBJ' => [\PDO::FETCH_OBJ], + ]; + } + + /** + * Tests supported fetch modes. + * + * @dataProvider providerSupportedFetchModes + */ + public function testSupportedFetchModes(int $mode): void { + $mockPdo = $this->createMock(StubPDO::class); + $mockConnection = new StubConnection($mockPdo, []); + $statement = new StatementPrefetchIterator($mockPdo, $mockConnection, ''); + $this->assertInstanceOf(StatementPrefetchIterator::class, $statement); + $statement->setFetchMode($mode); + } + + /** + * Provides data for testDeprecatedFetchModes. + * + * @return array + * An associative array of simple arrays, each having the following + * elements: + * - a PDO fetch mode. + */ + public static function providerDeprecatedFetchModes(): array { + return [ + 'FETCH_DEFAULT' => [\PDO::FETCH_DEFAULT], + 'FETCH_LAZY' => [\PDO::FETCH_LAZY], + 'FETCH_BOTH' => [\PDO::FETCH_BOTH], + 'FETCH_BOUND' => [\PDO::FETCH_BOUND], + 'FETCH_INTO' => [\PDO::FETCH_INTO], + 'FETCH_FUNC' => [\PDO::FETCH_FUNC], + 'FETCH_NAMED' => [\PDO::FETCH_NAMED], + 'FETCH_KEY_PAIR' => [\PDO::FETCH_KEY_PAIR], + 'FETCH_CLASS | FETCH_CLASSTYPE' => [\PDO::FETCH_CLASS | \PDO::FETCH_CLASSTYPE], + ]; + } + + /** + * Tests deprecated fetch modes. + * + * @todo in drupal:11.0.0, do not remove this test but convert it to expect + * exceptions instead of deprecations. + * + * @dataProvider providerDeprecatedFetchModes + * + * @group legacy + */ + public function testDeprecatedFetchModes(int $mode): void { + $this->expectDeprecation('Fetch mode %A is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use supported modes only. See https://www.drupal.org/node/3377999'); + $mockPdo = $this->createMock(StubPDO::class); + $mockConnection = new StubConnection($mockPdo, []); + $statement = new StatementPrefetchIterator($mockPdo, $mockConnection, ''); + $this->assertInstanceOf(StatementPrefetchIterator::class, $statement); + $statement->setFetchMode($mode); + } + } -- GitLab