-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Recently, I tried to enable checking exceptions in PHPStan and, besides a few other issues with the DBAL API, stumbled upon the one that shows what an awfully overcomplicated piece of work the Events API is:
AbstractPlatform::getDropTableSQL()dispatches anonSchemaDropTableevent which can override the SQL used to drop the table.- The return type of
SchemaDropTableEventArgs::getSql()is?string. If it returnsNULL, the platform will throw anUnexpectedValueException. - There's no way to tell that a given method is an event handler unless it's documented as such or whoever reads the code knows that the methods with names starting with
onare event handlers. Those methods are not invoked from anywhere in the code explicitly.
Here's a chain of questions that leads to the above conclusion:
- Why is
SchemaDropTableEventArgsallowed to returnNULLif it's not even a valid value? - We could require that it returns a
stringbut then what would it return if the handler of the event didn't callsetSql()? - Putting aside the event-handling scenario, what should the
SchemaDropTableEventArgsclass do if it was instantiated and then got itssetSql()invoked? Maybe throw a logical exception? - Then, should it throw this exception unconditionally or only if it got a call to
preventDefault()first?
Problems with the API design and implementation
What this API does, most likely could be solved just by extending the platform class and overriding the corresponding methods. Besides this awfully implicitly stateful logic, a few more concerns about DDL events:
- It looks like it's an overengineered solution to what could be easily solved by extending the corresponding platform methods.
- Unlike extending the platform, events don't allow invoking the default implementation (the method of the
parentclass). - Unlike class extension, events don't provide access to the original logic like a call to the
parentmethod would. - The DDL events API is incomplete (e.g. one can override
DROP TABLEbut cannot overrideDROP FOREIGN KEY). - In event-driven systems and DDD, an event is an immutable object representing a fact that happened in the past. Which fact does an
onSchemaDropTableevent represent? Let's assume it's a fact that somebody intended to drop a table. Why is it mutable? Why does it carry any information about processing the event (prevent default) and even implementation of that processing (SQL)? - For 800 lines of production code, there are three unit tests with mocks and not a single integration test which would demonstrate how this API can be used and if it can be used at all.
- The documentation shows only how to register handlers but not what they should to.
Problems with the EventManager API as such
This API is unsafe from the standpoint of the types:
- There's no way to enforce the fact that a given event (
onSomething) is dispatched only with a specific type of arguments (SomethingArgs). - There's no way to guarantee that the given listener supports handling the given type of event. The dispatcher calls the method corresponding to the event name dynamically:
$listener->$eventName($eventArgs)
The mistakes in the code that implements the handling of events will lead to a type error at runtime.
Types of events
- Connection and transaction events:
Line 21 in a85d913
public const postConnect = 'postConnect'; The logic implemented on top of these events can be implemented using driver middlewares.Lines 33 to 35 in a85d913
public const onTransactionBegin = 'onTransactionBegin'; public const onTransactionCommit = 'onTransactionCommit'; public const onTransactionRollBack = 'onTransactionRollBack'; - Schema manipulation events: Depending on the use case, it should be advised to either extend the corresponding platform method, or modify the arguments being passed to the method before calling it.
Lines 23 to 30 in a85d913
public const onSchemaCreateTable = 'onSchemaCreateTable'; public const onSchemaCreateTableColumn = 'onSchemaCreateTableColumn'; public const onSchemaDropTable = 'onSchemaDropTable'; public const onSchemaAlterTable = 'onSchemaAlterTable'; public const onSchemaAlterTableAddColumn = 'onSchemaAlterTableAddColumn'; public const onSchemaAlterTableRemoveColumn = 'onSchemaAlterTableRemoveColumn'; public const onSchemaAlterTableChangeColumn = 'onSchemaAlterTableChangeColumn'; public const onSchemaAlterTableRenameColumn = 'onSchemaAlterTableRenameColumn'; - Schema introspection events: This is the hardest part: we allow users to hook into the very internals of the schema introspection, e.g. the API consumer is exposed to the SQL results representing raw introspected data. By definition, this API is very brittle since the raw results is an implementation detail that can and will change. The fact that we allow this extension complicates the refactoring of the Schema Manager and Platform API.
Lines 31 to 32 in a85d913
public const onSchemaColumnDefinition = 'onSchemaColumnDefinition'; public const onSchemaIndexDefinition = 'onSchemaIndexDefinition';