-
Notifications
You must be signed in to change notification settings - Fork 2k
Description
Introduction
I want to begin a "big picture" discussion about something that has caused numerous issues in the past: framework interfaces. Let me also point out up front that this is currently a problem. We have done a handful of patch jobs to "make things work" but they have not addressed the underlying issues. A few references for example:
- Bug: red line on model by setPrefix & prefixTable #4329
- [DocBlock] Add property $request with @var IncomingRequest in BaseController #4280
- Add CLIRequest to request property in BaseController #4285
- Handling requests sent back from filters #3900
Interfaces
An interface is a contract between the code and our developers:
- A class that implements an interface must have all the defined methods.
- An interface cannot be changed without a major release.
- Anything that uses an interface in a typehint or docblock must use only that interface's methods.
Problem
Number three is where we get in trouble. Due to the nature of frameworks we need to move classes around a lot. In order to allow extending/replacing core components we use interfaces. In our current code base, sometimes this has been done well and maintained and sometimes it has not. Two examples...
Examples
CacheInterface
The CacheInterface is an example of interfacing handled well. The interface has 10 public methods, all the handlers implement those methods without embellishment, and our Cache Service returns the interface:
CodeIgniter4/system/Config/Services.php
Lines 90 to 99 in ecb2507
| /** | |
| * The cache class provides a simple way to store and retrieve | |
| * complex data for later. | |
| * | |
| * @param Cache|null $config | |
| * @param boolean $getShared | |
| * | |
| * @return CacheInterface | |
| */ | |
| public static function cache(Cache $config = null, bool $getShared = true) |
This means that other components like Throttler can use CacheInterface safely:
CodeIgniter4/system/Throttle/Throttler.php
Lines 30 to 35 in ecb2507
| /** | |
| * Container for throttle counters. | |
| * | |
| * @var \CodeIgniter\Cache\CacheInterface | |
| */ | |
| protected $cache; |
... and any developer who wished to use custom caching can define their own.
ConnectionInterface
I'm going to pick on the database layer because of this recent issue (#4329) and because it is a bit of a mess generally. ConnectionInterface has 16 public methods but they are not nearly comprehensive enough to support all our required database methods so we use BaseConnection implements ConnectionInterface and then have handlers extend BaseConnection. This way when MigrationRunner needs to check for the "migrations" table is can use the embellished method provided by BaseConnection:
CodeIgniter4/system/Database/MigrationRunner.php
Lines 935 to 944 in ecb2507
| /** | |
| * Ensures that we have created our migrations table | |
| * in the database. | |
| */ | |
| public function ensureTable() | |
| { | |
| if ($this->tableChecked || $this->db->tableExists($this->table)) | |
| { | |
| return; | |
| } |
This works because MigrationRunner knows its protected $db property is a BaseConnection:
CodeIgniter4/system/Database/MigrationRunner.php
Lines 70 to 76 in ecb2507
| /** | |
| * The main database connection. Used to store | |
| * migration information in. | |
| * | |
| * @var BaseConnection | |
| */ | |
| protected $db; |
That looks fine, but OOPS! our migration service expects a ConnectionInterface:
CodeIgniter4/system/Config/Services.php
Lines 425 to 434 in ecb2507
| /** | |
| * Return the appropriate Migration runner. | |
| * | |
| * @param Migrations|null $config | |
| * @param ConnectionInterface|null $db | |
| * @param boolean $getShared | |
| * | |
| * @return MigrationRunner | |
| */ | |
| public static function migrations(Migrations $config = null, ConnectionInterface $db = null, bool $getShared = true) |
Now we have a conundrum. Do we rework dependent classes so they only use the interface methods? Or do we redefine all services to use BaseConnection so we have access to these embellished methods? If we do the latter, then why have ConnectionInterface at all?
Road Forward
Unfortunately ConnectionInterface and MigrationRunner aren't an isolate incident. We have numerous infractions of this time throughout the framework, the ones I am most aware of being in the database and HTTP layers. Given the current "broken" state of things I think we have some leeway in the changes we can make, but since we are not ready for version 5 yet these will have to be handled delicately. Here's my proposal but I'd like to hear ideas from others as well:
- Identify comprehensive and intact interfaces (like Cache) and standardize their references through the code
- Identify abstract and base classes that should be used instead of interfaces, update all references and deprecated the interfaces
- Identify interfaces we want to keep and push intermediate implementations back as far as possible (e.g. no "MyClass extends MyImplementationOfInterface")
- Identify non-comprehensive interfaces that cannot be replaced and make new interfaces (extensions) to be used instead
What are your thoughts? criticisms? solutions?