setono/doctrine-orm-batcher-bundle
Symfony bundle that integrates Setono’s Doctrine ORM Batcher, making it easy to process large Doctrine queries in batches. Provides injectable services like BatcherFactoryInterface and QueryRebuilderInterface with autowiring support.
Install the Bundle:
composer require setono/doctrine-orm-batcher-bundle
Enable the Bundle:
Create a custom service provider in Laravel to register the bundle’s services (since Laravel lacks a bundles.php equivalent):
// app/Providers/DoctrineBatcherServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Setono\DoctrineORMBatcher\Factory\BatcherFactory;
class DoctrineBatcherServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(
\Setono\DoctrineORMBatcher\Factory\BatcherFactoryInterface::class,
function ($app) {
return new BatcherFactory(
$app->make(\Doctrine\ORM\EntityManagerInterface::class)
);
}
);
}
}
Register the provider in config/app.php:
'providers' => [
// ...
App\Providers\DoctrineBatcherServiceProvider::class,
],
Set Up Doctrine ORM:
Install Doctrine dependencies and configure it in config/doctrine.php:
composer require doctrine/dbal doctrine/orm
Example config/doctrine.php:
return [
'dbal' => [
'driver' => 'pdo_mysql',
'url' => env('DATABASE_URL'),
],
'orm' => [
'entity_managers' => [
'default' => [
'connection' => 'default',
'mappings' => [
['type' => 'annotation', 'namespace' => 'App\Entity', 'path' => 'app/Entities'],
],
],
],
],
];
First Use Case:
Batch delete inactive users (replace User with your Doctrine entity):
use Setono\DoctrineORMBatcher\Factory\BatcherFactoryInterface;
class UserService
{
public function __construct(
private BatcherFactoryInterface $batcherFactory,
private \Doctrine\ORM\EntityManagerInterface $em
) {}
public function deleteInactiveUsers()
{
$dql = 'DELETE FROM App\Entity\User u WHERE u.active = :active';
$batcher = $this->batcherFactory->createBatcher($this->em, $dql, ['active' => false]);
$batcher->setBatchSize(100); // Process 100 records at a time
$batcher->execute();
}
}
Use the batcher for SELECT, INSERT, UPDATE, or DELETE operations:
// SELECT batching (fetch in chunks)
$dql = 'SELECT u FROM App\Entity\User u WHERE u.role = :role';
$batcher = $this->batcherFactory->createBatcher($this->em, $dql, ['role' => 'admin']);
$batcher->setBatchSize(50);
foreach ($batcher as $batch) {
foreach ($batch as $user) {
// Process each user in the batch
}
}
// INSERT/UPDATE/DELETE batching
$dql = 'UPDATE App\Entity\User u SET u.active = :active WHERE u.id IN (:ids)';
$batcher = $this->batcherFactory->createBatcher($this->em, $dql, ['active' => true, 'ids' => []]);
$batcher->setBatchSize(200);
$ids = $this->em->createQuery('SELECT u.id FROM App\Entity\User u WHERE u.active = false')->getScalarResult();
$batcher->setParameter('ids', array_chunk($ids, 200));
$batcher->execute();
Optimize complex queries for batching using QueryRebuilderInterface:
use Setono\DoctrineORMBatcher\Query\QueryRebuilderInterface;
class UserQueryRebuilder implements QueryRebuilderInterface
{
public function rebuild(\Doctrine\ORM\QueryBuilder $qb, array $batch): \Doctrine\ORM\QueryBuilder
{
$ids = array_column($batch, 'id');
return $qb
->where('u.id IN (:ids)')
->setParameter('ids', $ids);
}
}
// Register the rebuilder (e.g., in a service)
$batcher = $this->batcherFactory->createBatcher($this->em, $dql);
$batcher->setQueryRebuilder(new UserQueryRebuilder());
Control transaction boundaries per batch:
$batcher = $this->batcherFactory->createBatcher($this->em, $dql);
$batcher->setBatchSize(100);
$batcher->setFlushMode(\Doctrine\ORM\UnitOfWork::FLUSH_AFTER); // Flush after each batch
$batcher->execute();
Hook into batch lifecycle events (e.g., pre/post batch processing):
use Setono\DoctrineORMBatcher\Event\BatchEvents;
$batcher = $this->batcherFactory->createBatcher($this->em, $dql);
$batcher->addListener(BatchEvents::PRE_BATCH, function ($event) {
// Log or validate before processing a batch
});
$batcher->addListener(BatchEvents::POST_BATCH, function ($event) {
// Cleanup or notify after a batch
});
Doctrine Entity Mapping: Ensure your entities are properly mapped for Doctrine. Example:
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class User
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column]
private bool $active = false;
}
Hybrid Eloquent/Doctrine: Use Doctrine only for batch operations while keeping Eloquent for CRUD:
// For CRUD (Eloquent):
User::where('active', true)->get();
// For batching (Doctrine):
$dql = 'SELECT u FROM App\Entity\User u WHERE u.active = true';
$batcher = $this->batcherFactory->createBatcher($this->em, $dql);
Command-Line Batching: Integrate with Laravel Artisan commands for bulk operations:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Setono\DoctrineORMBatcher\Factory\BatcherFactoryInterface;
class BatchUpdateCommand extends Command
{
protected $signature = 'batch:update-users';
protected $description = 'Batch update users';
public function __construct(
private BatcherFactoryInterface $batcherFactory,
private \Doctrine\ORM\EntityManagerInterface $em
) {
parent::__construct();
}
public function handle()
{
$dql = 'UPDATE App\Entity\User u SET u.last_login = :now WHERE u.last_login < :cutoff';
$batcher = $this->batcherFactory->createBatcher($this->em, $dql, [
'now' => new \DateTime(),
'cutoff' => new \DateTime('-30 days'),
]);
$batcher->setBatchSize(150);
$batcher->execute();
$this->info('Batch update completed!');
}
}
Queue Integration: Offload batch jobs to Laravel queues for async processing:
use Illuminate\Support\Facades\Queue;
Queue::push(function () {
$dql = 'SELECT u FROM App\Entity\User u WHERE u.role = :role';
$batcher = $this->batcherFactory->createBatcher($this->em, $dql, ['role' => 'admin']);
$batcher->setBatchSize(50);
foreach ($batcher as $batch) {
foreach ($batch as $user) {
// Process user (e.g., send email, update external system)
}
}
});
Doctrine vs. Eloquent Conflicts:
EntityManager and Eloquent’s Manager may conflict if not properly isolated.DoctrineEntity (e.g., DoctrineEntity\User) to avoid collisions.Query Rebuilding Complexity:
How can I help you explore Laravel packages today?