New to CakePHP 5 and I have some questions regarding the conventions (I suppose):
I have a class Model/InvoicesTable which currently reads rows from an Excel spreadsheet. My boss communicated to me that the datasets will be read from a CRM system in the future.
So my first intention was to rename InvoicesTable to InvoicesExcelTable or similar and create an interface InvoicesTable. Then my controller would request the InvoicesTable(because the controller shouldn’t care about the current implementation) via Dependency Injection and all would be fine.
But unfortunately, the Table classes are not resolved using the service container, as far as I understand. So how could I build this in CakePhp?
Thanks in advance.
PS: Copilot came up with this, but this doesn’t really go along the CakePhp conventions:
<?php
// src/Model/InvoicesRepositoryInterface.php
interface InvoicesRepositoryInterface
{
public function getItems(string $filePath): array;
}
// src/Model/InvoicesRepository.php
class InvoicesRepository implements InvoicesRepositoryInterface
{
public function getItems(string $filePath): array
{
$invoicesTable = TableRegistry::getTableLocator()->get('Invoices');
return $invoicesTable->getItems($filePath);
}
}
// In Application.php
public function services(ContainerInterface $container): void
{
$container->add(InvoicesRepositoryInterface::class, InvoicesRepository::class);
}
I assume that the Excel sheet will be replaced by some other interface, like a REST API or even a shared database table/layer. So it is basically replacing an implementation with another.
If it will be replaced, then I’d say that any efforts at abstraction are superfluous. Just replace the one implementation with the other when the time comes. Abstraction is for when the implementation needs to be determined on the fly based on some piece of information.
Not sure if I agree. The spreadsheet file needs to be read from different sheets, while typically in a DB operation this could be done with a single SQL JOIN or something, or either one or two REST requests. So the API of the model table classes would be different for each implementation, and replacing the implementation would mean to refactor all the classes (like service classes) that call these methods. This is one of the reasons why interfaces exist IMHO.
Controllers automatically create the table instance according to their name.
Therefore, if you rename your table class you can either
rename your controller class to InvoicesExcelController as well to automatically get a $this→InvoicesExcel object populated, or
add the following to your controller to manually populate the property:
public InvoicesExcelTable $InvoicesExcel;
public function initialize(): void {
parent::initialize();
$this->InvoicesExcel = $this->fetchTable('InvoicesExcel');
}
All Controllers have the LocatorAwareTrait present in their base class, so you can fetch any table object via Controllers - 5.x
You can’t get table objects injected via the dependency injection container, because we use our own Locator pattern to get instances (this has historic reasons, as the DI container was added in CakePHP 4.2, way past the time, when table objects were already a thing)
You’re going to have to refactor all those classes anyway, to make them use whatever generic interfaces you might come up with. Seems likely to me (not knowing all the intricacies of your project) that it would be easier to refactor to use a single new implementation than to add sufficiently generic interfaces and then refactor to use those.
At this point I would argue that this is a drawback of the whole Convention Over Configuration approach; while it can help to get an app up and running quickly, it seems to create a couple of obstacles down the road. Creating controllers that represent an implementation of a data source is something I’d rather not do, becaue it would mean that my implementation has crept into all layers. I don’t really agree that “You’re going to have to refactor all those classes anyway”, since this is one of the selling points of an interface in the first place - at least in theory. This is a new codebase so the question is not whether it is worth to do a large refactoring for interfaces, but how to prevent a near-complete rewrite of controller, tables, entities etc. just to switch the data source.
Also, I am confused about the whole dependency management - services should be injected via DI, but cannot be injected into ie. table classes, components have dataLoader, table classes have tableLocator or fetchTableetc. - but these are all just dependencies. Why not have a uniform way of handling/requesting/injecting them?