Problem/Motivation
Under PHP 8 installing Drupal results in errors because \Drupal\Core\Database\StatementInterface::fetch(), ::fetchAll() and ::setFetchMode() do not match their \PDOStatement::* counterparts.
Ref https://github.com/php/php-src/pull/6220
Proposed resolution
Introduce a PHP 7 compatible interface and trait for these three methods, as well as PHP 8 compatible interface and trait. Use the right one based on the PHP version identified.
interface Php7StatementInterface {
public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL);
public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL);
public function setFetchMode($mode, $a1 = NULL, $a2 = []);
}
interface Php8StatementInterface {
public function fetch(int $mode = \PDO::FETCH_BOTH, int $cursor_orientation = \PDO::FETCH_ORI_NEXT, int $cursor_offset = 0);
public function fetchAll(int $mode = \PDO::FETCH_BOTH, ...$args);
public function setFetchMode(int $mode, ...$params);
}
Remaining tasks
Review.
User interface changes
None.
API changes
When using PHP 8, the three methods will have a different signature. Given that the interface is already based off of \PDOStatement which became more strict in PHP 8, we cannot go around this. The default values of the methods also changed from NULL to integer constants defined by PDO. Callers that wish to use some of the default values and specify some other values will encounter problems when running on PHP 8.
Data model changes
None.
Release notes snippet
The signatures of \Drupal\Core\Database\StatementInterface::fetch(), ::fetchAll() and ::setFetchMode() changed when running on PHP 8 due to the underlying changes in PHP's built in \PDOStatement signatures for the respective methods.
| Comment | File | Size | Author |
|---|---|---|---|
| #16 | 3156881-16.patch | 24.39 KB | hussainweb |
| #16 | interdiff-14-16.txt | 1.08 KB | hussainweb |
Comments
Comment #2
alexpottComment #3
catchShould we remove this method altogether so that it just inherits from PHP?
Comment #4
daffie commentedWe can just remove these changes from the patch. The method
computeToken()does not change the value of the variable$token.fetch(). I am not if/how we can fix that.Comment #5
daffie commentedWe should add testing for this change.
Comment #6
alexpott@daffie thanks for #5 That change was not meant to be here. It's part of #3156880: \Drupal\Core\Access\CsrfTokenGenerator::validate() - ensure $token is a string before calling hash_equals() - fwiw there is test coverage of this returning false in this case it is just that in PHP 8 we need to return early because there's a scalar typehint on the PHP method.
Re #2 this is an odd one. The docs from the interface say:
If we remove this method then the second case of defining their own class is problematic. See \Drupal\Core\Database\StatementEmpty
#3. The BC concerns.
So this is a really tricky. Here are something we can do. Move this method into another interface where we can maintain 2 versions. Then implement two traits that implement the correct signature for each version and call a doFetch method. Then contrib can upgrade to this system in their own time to support PHP 8. Doing this has some disadvantages:
tldr; I think we have to do something awful with classes/interfaces/traits in order to make this work. Because it is too late for Drupal 9 and we also have tried to make it easy to support code across D8 and D9 and we want D8 to be compatible with PHP 8 so we're going to need to do it anyway.
Patch attached removes the unintended code. BC shenanigans is yet to be implemented.
Comment #7
alexpottHere's an implementation will a complete BC layer for PHP 7 and will also work for Drupal 8 (i hope). Do this will mean that contrib drivers use the same methods to only make a minimal change when they choose to support PHP 8 and they won't break on sites using PHP 7.
As promised this is not a pretty solution but I'm not sure there's much else we can do.
Comment #8
alexpottDoesn't apply to 8.9.x - conflict in StatementEmpty. Here's a patch for 8.9.x to show the BC layer works all the way back to 8.9.x / PHP7
Also testing on PHP 8 on #3156595-50: Make Drupal 9 installable on PHP8
Comment #9
alexpottWhoops... adding returns :)
Comment #12
alexpottSo #9 didn't quite work on PHP 8 because of the change of default value from NULL to \PDO::FETCH_BOTH
Here's a fix for that.
Comment #13
hussainwebThere are related changes in PDOStatement which were introduced in PHP 8.0.0beta3. These are the two commits which made the relevant changes:
I think it makes sense to include these changes here as well. Hence, renaming the issue for clarity.
Comment #14
hussainwebI noticed a few issues during manual testing. It was funny how such a critical error was not visible except in dynamic routes.
Comment #16
hussainwebHopefully, this workaround should be fine to fix the problem.
Comment #17
andypostIt looks big API change for db layer, so could use wait til RC1 of PHP 8 but ++ to keep it single issue
Comment #18
alexpottWe shouldn't really be making other people's concrete class methods our interface. This mess is being caused by borrowing parts of PDO's API without proper encapsulation. Fortunately some of this appears to be completely dead code.
Comment #19
hussainwebI agree. Case in point: The site installed and ran well when I forgot to use the new
StatementTraitin the\Drupal\Core\Database\Statementclass. I then realised I was missing it. I added it and created the patch in #13 (which failed manual testing).I wonder if it would be better to refactor after making it work with PHP 8 or how much refactoring would even be possible in the current release cycle. I would at least want to move away from the current
func_get_args()logic which now gets repeated in multiple places in the patch.Comment #20
gábor hojtsyClever! I agree this is not easy to maintain per say, but we would need to do the same in Drupal 8 and also to keep BC in 9. We can get rid of it in Drupal 10 ;) Should we include our own deprecation here of the old interfaces that we exposed, targeting Drupal 10?
Comment #21
alexpottLooking HEAD I don't understand why we have the code in \Drupal\Core\Database\Statement
Like is it even needed at all? Like there's no logic here and passing the exact same number of arguments you're passed is what's going to happen even if these overrides did not exist.
Comment #22
alexpottAh I know why we're doing #21 - because we have different defaults :( :( :( :(
Comment #23
alexpottFWIW #21 was added in #2521832: Uncomment StatementInterface methods
Comment #24
andypostComment #25
andypostComment #26
hussainweb@alexpott, re #21 and #22, you're right. This seems a little unintuitive (especially the fact that this happens twice in case of
\Drupal\Core\Database\Statement) but it's required. The comment explains why and not having this resulted in the bugs I mentioned where routes with dynamic parameters wouldn't work.Sidenote about that issue: It's weird that a PDO behaviour would cause impact to dynamic parameters in routes but this happens because a PDO exception is swallowed in
\Drupal\Core\Routing\RouteProvider::getRoutesByPath.Comment #27
hussainweb@Gábor Hojtsy, re: #20
Removing this in Drupal 10 would mean that we would also have to remove PHP 7.4 support. We'll have to refactor the methods into new ones and then deprecate existing methods. But that would be tricky too as these are actually concrete methods in
\PDOStatementand we extend that class.Comment #28
gábor hojtsy@hussainweb: PHP 7.4 is the last version series of PHP 7 and is EOL is a few months after the release of Drupal 10 (https://www.php.net/supported-versions.php). The current plan in #3118147: [meta] Set Drupal 10 platform and browser requirements six months before the release is to require PHP 8 in Drupal 10.x as soon as the branch opens for development. That would be too late to deprecate anything for Drupal 10. I don't know how to make sure we revisit this as a possible deprecation once the PHP requirement is decided but soon enough so contrib can act on it.
Comment #29
gábor hojtsyAdded a change record at https://www.drupal.org/node/3170913, the issue summary totally needs updating though. I am not sure I am the best person to do that. Also need reviews on the change record :D
Comment #30
andypostComment #31
gábor hojtsyUpdated the issue summary as well (please verify). Also added a release notes snippet, as this kind of change cannot go without one. Will need a release notes tag once we know which version it lands in.
Comment #32
andypostWhy they are not using ::class - from PHP POV strings are slower and code should prevent using duplicated strings in the same file
Comment #33
alexpott@andypost class aliases tend to use the fully qualified class name because:
See how vendor is using class_alias()
Comment #34
catchI think we can deprecate for Drupal 10 now on the assumption we require PHP 8, and we always have the option of updating the deprecation to Drupal 11 for whatever reason later.
Comment #35
alexpottI keep on looking at this issue and feel that we should be addressing the root cause. It feels dangerous that \Drupal\Core\Database\StatementInterface::fetchAll(),\Drupal\Core\Database\StatementInterface::setFetchMode() and \Drupal\Core\Database\StatementInterface::fetch() exist. These are not our methods. These are from \PDOStatement and their signature is owned by PHP and not by Drupal. The reason we have this interface is so that we can have alternate implementations like \Drupal\Core\Database\StatementPrefetch and \Drupal\Core\Database\StatementEmpty
Looking for prior art... looks like Doctrine is going to have the same issues... https://github.com/doctrine/dbal/blob/2.11.x/lib/Doctrine/DBAL/Driver/Re... - ah I see the 3.0.x branch will be PHP 8 compatible. Glancing at that code - they've moved away from copying PHP's code to their interface - https://github.com/doctrine/dbal/blob/3.0.x/src/Driver/Result.php and they've properly decoupled there Statement from PDO's statement - see https://github.com/doctrine/dbal/blob/3.0.x/src/Driver/PDO/Statement.php
Side note this blog post is interesting.
Comment #36
mondrake+1 on #35. I filed #3174662: Encapsulate \PDOStatement instead of extending from it to (potentially) address the root cause.
Comment #37
andypostAdded https://github.com/php/php-src/pull/6220 to IS
Comment #38
catch#3174662: Encapsulate \PDOStatement instead of extending from it is looking very viable, so going to mark this postponed - we can mark duplicate once that issue lands, or re-open here if there's a nasty surprise.
Comment #39
andypostOne more change in PDO parameters https://github.com/php/php-src/pull/6272
Comment #41
mondrakeThe alternative approach in #3174662: Encapsulate \PDOStatement instead of extending from it was committed.
Comment #42
mikemadison commentedFYI this cropped up for me today, and I beat my head on it for an hour before I realized what happened (and thought I would share here). We updated an environment to PHP 8.0 and Drupal 9.3.x. Except, that the deployment of 9.3.x silently failed and we were actually still running Drupal 8.9.x on a PHP 8.0 server (which obviously isn't going to work). So, if you are hitting this, definitely take the 30 seconds required to 100% confirm that you are running Drupal 9.1.x or newer (which actually supports PHP 8.0)!