-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NEW NullDatabase #10016
NEW NullDatabase #10016
Conversation
So from my perspective, Option A solves the problem of "using the SS kernel when a database isn't available", while the other two options simply create a platform for solving the problem in a more elegant way. I think the ideal approach here is both. We want to provide a solution for this not-entirely-uncommon problem but also provide a way of hooking into database activity in a general sense to do other things (e.g. track content changes!) If we went with Option B:
Concerns:
If we went with Option C:
Concerns:
|
Discussed offline with Aaron, here's where we got to:
|
3b0e68d
to
d25e0fa
Compare
OK I tried Option B, see https://github.com/open-sausages/silverstripe-framework/tree/pulls/4/databaselesskernel, even done it with crazy anonymous classes for the single-method event handlers ;) The problem was that several methods are called before |
d25e0fa
to
625e724
Compare
Yup, that makes sense. Do you want to just remove those event emissions from the database class, though? Not sure why they're there anymore. |
625e724
to
e9f0d25
Compare
Of course, that's done now (removed the events from |
Can remove the composer dependency and the use statements as well then? |
00876c5
to
138655d
Compare
Yeah just realised that after the failing build ... doing too many things at once :) |
I like this NullDatabase approach here, thinking how we could live without it before. Will definitely speed up some functional tests for us as well where no database is needed. Will check the other issue. |
@michalkleiner Haven't thought of the test run use case before. Once this is merged you could look at |
138655d
to
9194e05
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems fine, main thing is to see if you can interfaces here rather than extending a base class.
src/Core/DatabaselessKernel.php
Outdated
* | ||
* @internal | ||
*/ | ||
class DatabaselessKernel extends CoreKernel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any hope of getting this to implement Kernel rather than extend CoreKernel (which itself implements Kernel)? Or would there be too much code you'd need to end up duplicating from CoreKernel?
Just seems really weird extending something then removing something (the database) from it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, but we can't add an interface in a minor release. That's why we're subclassing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's doable if you did this:
interface Kernel
|- abstract class BaseKernel implements Kernel (New class - move bunch of stuff from CoreKernel to here)
|- class CoreKernel extends BaseKernel (Include database connection methods)
|- class DatabaselessKernel extends BaseKernel
That would mean that CoreKernel keeps the same signature and functionality
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we're going from adding one new @internal
class to solve an edge case to three new classes and one new interface?
I'm just concerned that the motivation behind this was to keep it as simple and low-impact as possible, and now we're just expanding the maintenance footprint. I agree it's a better architecture, but I just don't want to lose sight over why were all here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're adding one new abstract class + one new class, as opposed to one new class. We'll have the same number of methods, so it's literally just one extra file to make things tidier which IMO will reduce the maintenance footprint.. Also as Michal mentioned above the DatabaselessKernel could be used for speedier functional testing as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, I didn't see that the interface already existed. OK, yeah this makes more sense now. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move bunch of stuff from CoreKernel to here
I think this is the only bit I'm not sold on. What "stuff" we move is kind of important. In this case, moving all the database-related methods out of the base class and into the CoreKernel would achieve what we need, but it's a bit arbitrary to draw the line at database concerns. If the point of this is abstraction and composability, then why not allow for configless or manifestless kernels as well? If the base class is going to be database agnostic, I see no reason why it can't be agnostic to all the other layers as well.
Here's one option:
-
BaseKernel
becomes a collection of services that can be used to boot the kitchen sink of application layers, e.g.setThemeResourceLoader
,setInjectorLoader
, etc.. -
boot()
method remains abstract -
All
bootXXX
protected methods, e.g.bootConfigs()
defined in the concretion -
To maintain a horizontal architecture, we move
bootXXX()
methods to traits -
Implementations can pick and choose which boots they want. That way CoreKernel and Databaseless kernel can both use the
ManifestBoot
andConfigBoot
trait, for example. -
bootPHP
would be the exception. A non-PHP boot isn't plausible. :-)
If we were to keep bootXXX()
methods in CoreKernel
, then DatabaselessKernel would have to extend CoreKernel to have access to them, and then we're right back where we started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Traits is probably overkill, though I know you love your composability :-) Maybe a middle ground is just leave all the stuff you'd move to traits in BaseKernel as method and just use the abstract boot() method to choose what methods get booted
I'm not overly fussed how you split it though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's too complicated. There's too much coupling in those boot methods, and it's probably not realistic to boot without a manifest.
A few leaky abstractions here and there, too, e.g. isFlushed()
not being part of the interface, yet expected in the public consumption of the API. Annoying.
Have refactored.
120aa94
to
2d5f990
Compare
2d5f990
to
db878a6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, some minor changes to make
I've double checked that all things removed from CoreKernel were copied over to BaseKernel
Running a website or normal unit test still works as it normally does
I can validate so much as the --no-database
option works and throws NullDatabaseExceptions e.g. running vendor/bin/sake --no-database dev/config
I'm not sure how to test something running successfully without a database @unclecheese presumably you've validated this does what you need it to for building GraphQL 4 schemas?
|
||
/** | ||
* Indicates whether the Kernel has been flushed on boot | ||
* Unitialized before boot | ||
* Uninitialised before boot | ||
* | ||
* @var bool | ||
*/ | ||
private $flush; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted on DatabaselessKernel, more $this->flush along with to BaseKernel as a protected field along with isFlushed()
I can't see really see any need to keep isFlushed abstract and open to different implementations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replied on the other comment.
src/ORM/Connect/NullDatabase.php
Outdated
/** | ||
* @var string | ||
*/ | ||
protected $queryErrorMessage = 'Using NullDatabase, cannot execute query: %s'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
protected $queryErrorMessage = 'Using NullDatabase, cannot execute query: %s'; | |
private $queryErrorMessage = 'Using NullDatabase, cannot execute query: %s'; |
src/ORM/Connect/NullDatabase.php
Outdated
/** | ||
* @var string | ||
*/ | ||
protected $errorMessage = 'Using NullDatabase, cannot interact with database'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
protected $errorMessage = 'Using NullDatabase, cannot interact with database'; | |
private $errorMessage = 'Using NullDatabase, cannot interact with database'; |
@@ -0,0 +1,10 @@ | |||
<?php | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/ORM/Connect/NullDatabase.php
Outdated
*/ | ||
class NullDatabase extends Database | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/Core/DatabaselessKernel.php
Outdated
*/ | ||
class DatabaselessKernel extends BaseKernel | ||
{ | ||
protected $queryErrorMessage = 'Booted with DatabaseLessKernel, cannot execute query: %s'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
protected $queryErrorMessage = 'Booted with DatabaseLessKernel, cannot execute query: %s'; | |
private $queryErrorMessage = 'Booted with DatabaseLessKernel, cannot execute query: %s'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this even used for? It can probably just be removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/Core/BaseKernel.php
Outdated
return []; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
abstract public function boot($flush = false); | ||
|
||
abstract public function isFlushed(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned elsewhere, no need for this to be abstract, the implementation is the same in both CoreKernel + DatabaselessKernel, so may as well just do it here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My instinct would be to keep it as is.
Looking at the implementation of isFlushed
, it seems like it's tied in to the implementation of boot
. Since boot
is abstract, it would seem to make some sense to say that isFlushed
should also be abstract.
Otherwise, we're putting an implicit requirement on the sub classes to update the isFlushed
variable in the boot method. Defining isFlushed
as abstract makes that requirement explicit.
src/Core/BaseKernel.php
Outdated
} | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove 2 empty lines here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
This is required for GraphQL code generation in CI (without a working runtime database/webserver environment). Context: silverstripe/silverstripe-graphql#388
db878a6
to
32b19f8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased this bad boy + made some tweaks.
|
||
abstract public function boot($flush = false); | ||
|
||
abstract public function isFlushed(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My instinct would be to keep it as is.
Looking at the implementation of isFlushed
, it seems like it's tied in to the implementation of boot
. Since boot
is abstract, it would seem to make some sense to say that isFlushed
should also be abstract.
Otherwise, we're putting an implicit requirement on the sub classes to update the isFlushed
variable in the boot method. Defining isFlushed
as abstract makes that requirement explicit.
src/Core/BaseKernel.php
Outdated
} | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
|
||
/** | ||
* Indicates whether the Kernel has been flushed on boot | ||
* Unitialized before boot | ||
* Uninitialised before boot | ||
* | ||
* @var bool | ||
*/ | ||
private $flush; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replied on the other comment.
src/Core/DatabaselessKernel.php
Outdated
*/ | ||
class DatabaselessKernel extends BaseKernel | ||
{ | ||
protected $queryErrorMessage = 'Booted with DatabaseLessKernel, cannot execute query: %s'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* NEW DatabaselessKernel to support operation without DB This is required for GraphQL code generation in CI (without a working runtime database/webserver environment). Context: silverstripe/silverstripe-graphql#388 * New --no-database option for sake * Refactor to abstract class * Apply feedback peer review Co-authored-by: Aaron Carlino <[email protected]> Co-authored-by: Maxime Rainville <[email protected]>
We need to throw exceptions in a specific context: When you run
composer install
orcomposer update
, we want to generate GraphQL code automatically through a composer plugin. These commands are also run on environments without a functional Silverstripe installation in CI, as part of automation testing as well as to create deployment packages sent to a separate runtime environment.Option A: NullDatabase
This pull request. Given the massive API surface of
Database
and its dependencies, that's a lot of code to shim in order to throw a bunch of exceptions. But its marked as@internal
, so we can pivot the approach at any time. One benefit here is that we avoid undesireable side effects by making other methods a no-op.Option B: Dispatching events in Database
We already use https://github.com/silverstripe/silverstripe-event-dispatcher as a dependency on
silverstripe/graphql
. By dispatching events fromDatabase
, we could avoid introducing new (internal) implementations. Thankfully the methods where events are most useful are implemented onabstract class Database
, rather than concrete implementations likeMySQLDatabase
, so we can achieve good coverage without worrying about modifications on other database adapters like PostgreSQL. This would also open up new ways of benchmarking queries (e.g. send the results to a monitoring service).Option C: Proxy DB
Damian has written https://github.com/tractorcow/silverstripe-proxy-db. It is a quite powerful way to create fakes, but would add a pretty substantial dependency to core: https://github.com/phpspec/prophecy. The library is pretty common and active, but also has no explicit statements about semver or ongoing commitments to PHP version support. So on the long term, this could lead to similar challenges that we currently face by hard-wiring
SapphireTest
to specificPHPUnit
features, which are now blocking PHP 8 compatibility in core. Note that we currently depend on this library insilverstripe/auditor
which is a supported module, but not in core. And this dependency could probably disappear if we implemented Option B anyway.