With the release of php 5.5 we got ourself a cool new language feature: generators. If you’re new to the concept I suggest you read the excellent blog post from Anthony Ferrara about this subject. In this post I’ll show an example of how I use generators in my every day work.
The other day I was working on a import of a large batch of vacancies from a remote system via a SOAP webservice (yes, C#.NET on the other side). The service returned a container containing the current resultset and a boolean value whether there where more results. Consider the following the classical approach without generators:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?php class RefreshVacanciesCommand extends ContainerAwareCommand { // ... protected function execute(InputInterface $input, OutputInterface $output) { $service = $this->getContainer()->get('soap_service'); $batchSize = 100; $startId = 0; do { $response = $service->getAllVacancies($batchSize, $startId); foreach ($response->GetAllVacanciesResult->Vacancies->Vacancy as $vacancy) { // Do actual stuff with $vacancy } $startId++; $more = $response->GetAllVacanciesResult->More; } while ($more); } } |
There’s fundamentally not much wrong with this approach. It works. But let me show a more elegant way of solving this issue which also brings more benefits to the table as you’ll see.
The GetAllVacanciesGenerator class
Take a look at this GetAllVacanciesGenerator
who’s responsibility it is to do the cumbersome fetching of the results and notice the yield
keyword turning it into a Generator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?php class GetAllVacanciesGenerator { /** * @var Service */ private $service; public function __construct(Service $service) { $this->service = $service; } /** * @param $batchSize * @param $startId * @return \Generator */ public function getAllVacancies($batchSize, $startId) { do { $response = $this->service->getAllVacancies($batchSize, $startId); foreach ($response->GetAllVacanciesResult->Vacancies->Vacancy as $vacancy) { $startId = $vacancy->Id; yield $vacancy; } $startId++; $more = $response->GetAllVacanciesResult->More; } while ($more); } } |
The command is much cleaner now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php class RefreshVacanciesCommand extends ContainerAwareCommand { // ... protected function execute(InputInterface $input, OutputInterface $output) { $service = $this->getContainer()->get('soap_service'); $batchSize = 100; $startId = 0; $generator = new GetAllVacanciesGenerator($service); foreach ($generator->getAllVacancies($batchSize, 0, null) as $vacancy) { // Do stuff } } } |
As you can see the command is looking pretty again. I love this kind of improvements. It’s also easy to test this seperate generator class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?php class GetAllVacanciesGeneratorTest extends \PHPUnit_Framework_TestCase { public function test_it_should_fetch_all_vacancies() { $mock = $this->getMockBuilder('Service') ->disableOriginalConstructor() ->getMock(); $mock->expects($this->at(0)) ->method('getAllVacancies') ->with(5, 0, 'test') ->will($this->returnValue($this->createResponse(true, [1,2,3,4,5]))); $mock->expects($this->at(1)) ->method('getAllVacancies') ->with(5, 6, 'test') ->will($this->returnValue($this->createResponse(false, [6,7]))); $mock->expects($this->exactly(2)) ->method('getAllVacancies'); $generator = new GetAllVacanciesGenerator($mock); foreach ($generator->getAllVacancies(5, 0) as $result) { $this->assertInstanceOf('Vacancy', $result); } } private function createResponse($more, $vacancyIds = []) { // Helper to ease building of fake Soap response ... } } |
This could also be done with implementing a Iterator
but I think the generator is much more readable.