How to encapsulate behavior using PHP iterator to comply SRP
OOP is still alive and some of us are still trying to encapsulate (more details here) proprieties and behavior. During my work on a legacy project surfing on pure PHP with no framework, and no built-in data collection (except the arrays), I often tried to follow the OOP standards, including encapsulation. Most of the time I managed to use it when I had to build a new class for a certain model. However, we can also encapsulate behavior when it comes to building a service class.
For the purpose of this article, we can think about the following scenario: We are working on a legacy project where refactoring possibilities are very restricted. In a service class, we have to fetch some data (an array of objects) from an API, then we need to filter based on some conditions, then we need to clear some data, save some logs and process elements in the foreach loop. It does not seem to be a hard task except for the fact that we also need to meet the criteria for SRP (Single Responsibility Principle).
Initial code
public function storeInvoices(Vendor $vendor, string $dateFrom, string $dateTo)
{
$loggerService = new LoggerService();
$invoiceService = new InvoiceService();
$invoices = $invoiceService->fetchInvoices($vendor, $dateFrom, $dateTo);
$loggerService->log("Fetched invoices: " . json_encode($invoices));
foreach ($invoices as $invoice) {
if ($invoiceService->isDue($invoice)) {
$this->store($invoice);
}
}
}
As you can see, we broke the SRP rule because we have more than one action related to its purpose. This function should do one thing: store invoices. Instead, it fetches invoices, logs something and filters based on some condition and calls the store function.
Now let`s do some refactoring to meet our purpose. First of all, we need to establish which parts should not stand inside this function. There are three extra parts: fetching, logging and filtering. So our function should receive in a way, already fetched and filtered invoices. We can not change the parameter list because we are working in a legacy code but we can reach that by using an Iterator (more about iterators here).
New code
public function storeInvoices(Vendor $vendor, string $dateFrom, string $dateTo)
{
$invoiceIterator = new InvoiceIterator($vendor, $dateFrom, $dateTo);
foreach ($invoiceIterator as $invoice) {
$this->store($invoice);
}
}
Now, our function looks much better and it is doing one thing. Maybe you are wondering what is happening behind scenes. Let’s take a look:
Iterator implementation
class InvoiceIterator implements Iterator
{
private $invoiceService = null;
private $invoices = [];
private $index = 0;
private $vendor;
private $loggerService;
private $dateFrom;
private $dateTo;
public function __construct(Vendor $vendor, string $dateFrom, string $dateTo)
{
$this->loggerService = new LoggerService();
$this->invoiceService = new InvoiceService();
$this->vendor = $vendor;
$this->dateFrom = $dateFrom;
$this->dateTo = $dateTo;
}
public function current(): mixed
{
return $this->invoices[$this->index];
}
public function next(): void
{
$this->index++;
}
public function key(): mixed
{
return $this->index;
}
public function valid(): bool
{
return !empty($this->invoices[$this->key()]) && $this->invoiceService->isDue($this->current());
}
public function rewind(): void
{
$this->invoices = $this->invoiceService->fetchInvoices($this->vendor, $this->dateFrom, $this->dateTo);
$this->loggerService->log("Fetched invoices: " . json_encode($this->invoices));
}
}
Inside the constructor, we are instantiating and setting some variables. The rewind function is dealing with fetching and logging. Also, logging could be encapsulated inside the fetchInvoices function. The others function are just standard for iterating over an array. The key function returns the current key, next increments the index and current returns the current item(invoice).
Of course, you could see some improvements in our code but our main purpose was to delegate some responsibilities to an iterator object in order to reach SRP.
Thank you!