Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
In #3177660: Remove public properties from StatementInterface implementations, we are stepping away from using Connection::query() for all purposes, and prepare/execute Statement objects in the db operation specific classes of the drivers instead.
This means that the exceptions thrown in the classes need to be caught in place, and the Connection::handleQueryException method, being protected, falls short to be able to manage.
Proposed resolution
- Introduce an ExceptionHandler class in the database API
- Deprecate Connection::handleQueryException
Remaining tasks
User interface changes
API changes
- A new ExceptionHandler class in the database API, which can be overridden by a database driver specific implementation.
- A new method called Connection::exceptionHandler(), which returns the database driver specific ExceptionHandler object.
Data model changes
Release notes snippet
Issue fork drupal-3186934
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3186934-introduce-an-exceptionhandler changes, plain diff MR !114
Comments
Comment #2
mondrakeOn this
Comment #4
mondrakeComment #5
mondrakeComment #6
mondrakeAdded CR, https://www.drupal.org/project/drupal/issues/3186934
Comment #7
daffie CreditAttribution: daffie commentedComment #8
daffie CreditAttribution: daffie commentedComment moved to MR.
Comment #9
daffie CreditAttribution: daffie commentedAll code changes look good to me.
The added deprecation has testing and a CR.
The patch fixes the problem from the IS.
For me it is RTBC.
@mondrake: Great work.
Comment #10
alexpottAdded comment on MR - need to set this back to needs review to get an answer...
Comment #11
mondrakeSo, please see comments on the MR. In essence,
prepareStatement
throwing a PDOException is a really edge case, which is covered now by a specific test that works on MySql only. The PDOException is caught and re-thrown as a DatabaseExceptionWrapper, which is a BC behavior.Setting back to RTBC, the changes are non-substantial, more test coverage added.
Comment #12
mondrakeThe default value for the 'throw_exception' query option is TRUE, fixed in MySql ExceptionHandler.
Comment #13
daffie CreditAttribution: daffie commentedBack to needs work for the 2 unresolved threads.
Comment #14
mondrakesee comments in MR
Comment #15
daffie CreditAttribution: daffie commentedBack to RTBC.
Comment #16
donquixote CreditAttribution: donquixote as a volunteer commentedI don't like this "new $class()".
Why can't we use the capabilities of OOP how it is meant to be used?
?>
Perhaps there could also be an interface for DatabaseExceptionHandler.
-----------------
This is meant to return StatementInterface, but here you potentially return void|null.
Comment #17
mondrakeWhy instantiating an exceptionHandler object every time to inject it, when probably 99% of the times is not used? Like this it's only instantiated when an exception has to be handled.
Comment #18
donquixote CreditAttribution: donquixote as a volunteer commentedI think the advantage will be quite small. The object will be very light-weight, without any properties. And we would only have one instance per request (one per db connection, or even one shared among db connections). The main benefit would be to avoid loading the class file with autoload.
We pay for it by using what I would call a Drupal anti-pattern of "new $class()", which prevents static code analysis.
I don't even see any class_exists() and then instanceof check.
But I see now that the ->getDriverClass() is already a common pattern in Connection.
This makes it hard for me to defend my point.
So don't let me stop you then.
Comment #19
alexpottI did a review in gitlab with some points that need addressing. See ^^
Comment #20
donquixote CreditAttribution: donquixote as a volunteer commented(sorry for the mix of issue comments vs MR comments)
@mondrake
Quoting myself in #18:
I see now that the
->getDriverClass()
pattern is only used for the mutable builder classes like Select, Insert, etc.Here is a grep (or rather, ag):
I think it somewhat makes sense to use this pattern for these classes, because we need a new instance every call.
The exception handler, on the other hand, only needs a single instance, which can be easily injected in the constructor. Or a mechanism with a setter and a lazy default initialization.
Comment #21
donquixote CreditAttribution: donquixote as a volunteer commentedOverall, I think instead of a pluggable exception handler, it would be better to agree on strict contracts for every method, and stick to them.
E.g.
->prepareStatement()
should either return a statement object, or throw an exception.We should agree on the exception type, and document it with a
@throws
doc.We should move away from conditional / parameterized contracts.
If we want an alternative implementation that returns FALSE or NULL instead of throwing, we should make it a separate method.
Comment #22
mondrake#21 I agree, but it's difficult while we have #3189680: Deprecate the 'throw_exception' option in the Database API I think... we can deprecate that and in D10 be strict.
Comment #23
mondrakeA mixture of https://git.drupalcode.org/project/drupal/-/merge_requests/68#note_5800 (which is from #3189680: Deprecate the 'throw_exception' option in the Database API) and https://git.drupalcode.org/project/drupal/-/merge_requests/114#note_7235
Comment #24
donquixote CreditAttribution: donquixote as a volunteer commentedActually, this option is currently only used for
->query()
/->handleQueryException()
.Which is somewhat ok, because ->query() does not have a native return type hint, and already advertises that it can return NULL if 'throw_exception' is false.
The try/catch in the
->query()
method contains a call to$this->prepareStatement()
, but->prepareStatement()
itself does not have any behavior that would be conditioned upon the 'throw_exception' parameter.In fact I would argue that
->prepareStatement()
is currently broken if no statementWrapperClass was provided.This could probably be verified with a test.
Conclusion:
There is no BC reason to consider this parameter anywhere within
->prepareStatement()
.My proposal:
Fix ->prepareStatement():
Make sure that it will always return a value consistent with its native return type hint.
Agree on an exception type that ->prepareStatement() is allowed/expected to throw.
Throw an exception in ->prepareStatement(), if $this->wrapperClass is empty. This is not a BC break, because this case is already broken.
For #3177660: Remove public properties from StatementInterface implementations:
Use try/catch in calling code, e.g., the ->execute() methods of the wrapper classes.
Make Connection->handleQueryException public, and call it from the wrapper classes, OR inject an exception handler into the wrapper classes.
Comment #25
mondrakeThanks for reviews @donquixote @alexpott.
I added a check to ensure no false|null can be returned by prepareStatement. Again, IMHO this is really edge case, it won't happen in core. prepareStatement() would only get to an exception in MySql or PgSql in case of disable emulated statement preparation when opening the connection.
Comment #26
daffie CreditAttribution: daffie commentedThe only unresolved thread has been addressed.
Back to RTBC.
Comment #27
mondrakeRerolled.
Comment #28
alexpottDiscussed with @catch - both of us have looked at the code and didn't haven't anymore to add.
Committed 5478a95 and pushed to 9.2.x. Thanks!