GeneratorPlus
is a wrapper around the native Generator
class that provides additional methods for more control over the iteration process.
In native PHP, calling the send
method inside a foreach
loop is problematic because it advances the generator to the next yield, disrupting the loop's iteration. GeneratorPlus
solves this issue with the sendInForeach
method, which ensures you can send values into the generator without moving to the next yield, maintaining the integrity of the loop.
Problem Example:
In this example, calling send
inside the foreach
loop causes the generator to advance to the next yield
statement, disrupting the loop and skipping iterations, leading to unintended behavior.
function getGenerator(): \Generator {
$sent = [];
$sent[] = yield 1;
$sent[] = yield 2;
$sent[] = yield 3;
$sent[] = yield 4;
$sent[] = yield 5;
return $sent;
}
$generator = getGenerator();
$items = [];
foreach ($generator as $item) {
$items[] = $item;
$generator->send(9); // This advances the generator, skipping iterations
}
var_dump($items); // Outputs [1, 3, 5];
var_dump($generator->getReturn()); // Outputs [9, null, 9, null, 9];
Unlike the native getReturn
method, which only allows communication at the end of the generator's execution, GeneratorPlus
provides a mechanism to communicate with the generator caller during the loop. This reverse communication allows for more dynamic interactions and event handling within the generator lifecycle.
You can install GeneratorPlus
via Composer:
composer require mano/generator-plus
To create an instance of GeneratorPlus
, use the createFromCallable
method, which takes a closure that returns a generator. There are two reasons why a closure must be used instead of creating it directly from the generator:
- Generators cannot be cloned, so using a closure ensures that the original generator is not modified.
- The EventDispatcher must be passed to the generator.
use Mano\GeneratorPlus\GeneratorPlus;
use Mano\GeneratorPlus\EventDispatcher\GeneratorEventDispatcher;
$generatorPlus = GeneratorPlus::createFromCallable(function(GeneratorEventDispatcher $eventDispatcher) {
yield 1;
$sent = yield 2;
if ($sent === 'foo') {
yield 7;
}
$eventDispatcher->dispatch(new MyCustomEvent('There is just one item left!'));
yield 3;
return 'bar';
});
You can attach events to the generator lifecycle using the attachEvent
method:
use Mano\GeneratorPlus\EventDispatcher\GeneratorPlusEvent;
$generatorPlus->attachEvent(MyCustomEvent::class, function(MyCustomEvent $event) {
echo "Oh my! The generator wants something! " . $event->getMessage();
});
The sendInForeach
method allows you to send values into the generator within a foreach
loop without disrupting the loop's iteration:
function getGenerator(): \Generator {
$sent = [];
$sent[] = yield 1;
$sent[] = yield 2;
$sent[] = yield 3;
$sent[] = yield 4;
$sent[] = yield 5;
return $sent;
}
$generator = getGenerator();
$items = [];
foreach ($generator as $item) {
$items[] = $item;
$generator->sendInForeach(9); // This does not advance the generator
}
var_dump($items); // Outputs [1, 2, 3, 4, 5];
var_dump($generator->getReturn()); // Outputs [9, 9, 9, 9, 9];
When dealing with batch processing in Doctrine, GeneratorPlus
can come in handy. Usually, you need to flush and clear the entity manager after some batch size to prevent memory issues. Employing the generator's event dispatcher can convey the message that one chunk has been processed to fire an event that clears the entity manager. If you clear the entity manager in the middle of a batch, residual objects would be detached from the manager, leading to errors.
class SomeRepository
{
public function getSomeEntityBasedOnCondition(GeneratorEventDispatcher $eventDispatcher = null)
{
$counter = 0;
while (true) {
$qb = $this->entityManager->createQueryBuilder()
->where('...') // select all entities that have not been updated yet
->setMaxResults(100);
$result = $qb->getQuery()->getResult();
if (count($result) === 0) {
break;
}
foreach ($result as $item) {
yield $item;
$counter++;
}
if($eventDispatcher !== null) {
// let the client code know about reaching the end of the batch
$eventDispatcher->dispatch(new MyCustomFlushEvent($counter));
}
}
}
}
class SomeController
{
public function __construct(private SomeRepository $repository)
{
}
public function someAction()
{
$generatorPlus = GeneratorPlus::createFromCallable(function (EventDispatcher $eventDispatcher) {
return $this->repository->getSomeEntityBasedOnCondition($eventDispatcher);
});
// Or use a different style if not any other arguments needed
// $generatorPlus = GeneratorPlus::createFromCallable([$this->repository, 'getSomeEntityBasedOnCondition']);
$generatorPlus->attachEvent(MyCustomFlushEvent::class, function (MyCustomFlushEvent $event) {
// flush at the end of the batch
$this->entityManager->flush();
if ($event->getCount() % 500 === 0) {
// clear the entity manager
$this->entityManager->clear();
}
});
foreach ($generatorPlus as $item) {
// some batch action
$item->setFoo(...);
}
}
}
This project is licensed under the MIT License. See the LICENSE file for details.
Contributions are welcome! Please submit a pull request or open an issue to discuss any changes.