xotelia/fetch-bundle
Xotelia Fetch Bundle simplifies fetching and handling remote data in PHP applications. It offers a clean, configurable API for making requests, processing responses, and integrating reusable fetch workflows with minimal setup.
composer require xotelia/fetch-bundle. It is a Symfony bundle, so it expects Symfony's kernel/bundle registration; there is no Laravel service provider shipped.Xotelia\AbimusFetchBundle\AbimusFetchBundle in config/bundles.php, then look at the bundle's DependencyInjection/Configuration.php and Resources/config/services.* to see the exact config keys and service IDs it exposes. That is your ground truth — the README is thin and the API surface is small.Fetch/IMAP library it depends on). Read the underlying library's docs first; the bundle mostly gives you configuration, a configured client service, and Symfony wiring.config/packages/abimus_fetch.yaml (or whatever the extension alias resolves to — check getAlias()). Keep credentials in env vars, not the YAML.INBOX, fetches UNSEEN messages, and dumps subject/from/date to stdout. Get that working before you add persistence.// src/Command/PeekMailboxCommand.php (Symfony)
#[AsCommand(name: 'mail:peek')]
class PeekMailboxCommand extends Command
{
public function __construct(private MailFetcherInterface $fetcher) { parent::__construct(); }
protected function execute(InputInterface $in, OutputInterface $out): int
{
foreach ($this->fetcher->fetchUnseen() as $message) {
$out->writeln(sprintf('%s | %s', $message->getSubject(), $message->getFrom()));
}
return Command::SUCCESS;
}
}
Poll-and-persist command (the bread and butter). A scheduled console command is the natural shape: connect, select folder, fetch by criteria (UNSEEN, SINCE <date>, or a UID range), hand each message to a handler, then flag or move it.
foreach ($fetcher->fetchByCriteria('UNSEEN') as $msg) {
$this->handler->handle($msg); // persist / dispatch / store attachments
$msg->markAsSeen(); // or move to "Processed"
}
Idempotency via UIDVALIDITY + UID. Store both, not just the UID. Use them as a unique key on your messages table so re-runs and crashes don't duplicate rows.
Message::firstOrCreate(
['mailbox' => $folder, 'uid_validity' => $uidValidity, 'uid' => $msg->getUid()],
['subject' => $msg->getSubject(), 'from' => $msg->getFrom(), 'received_at' => $msg->getDate()]
);
Don't process inline — dispatch. Fetch in the command, persist a lightweight record plus raw message (or attachment paths), then dispatch a job/event for parsing. This keeps the IMAP connection short-lived and lets you retry parsing without re-hitting the server.
Laravel adaptation. You cannot composer require a Symfony bundle into Laravel and expect it to boot. Two options:
MailboxServiceProvider:// app/Providers/MailboxServiceProvider.php
public function register(): void
{
$this->app->singleton(MailClient::class, fn () => new MailClient(
config('mailbox.host'),
config('mailbox.port'),
config('mailbox.username'),
config('mailbox.password'),
config('mailbox.encryption'),
));
}
Queue + scheduler in Laravel:
// app/Console/Kernel.php
$schedule->job(new FetchMailboxJob('INBOX'))->everyFiveMinutes()->withoutOverlapping();
// app/Jobs/FetchMailboxJob.php
public function handle(MailClient $client): void
{
$client->connect();
foreach ($client->fetchUnseen() as $msg) {
ProcessMessageJob::dispatch($msg->getUid(), $msg->getRawBody(), $msg->getAttachments());
}
$client->disconnect();
}
Folder strategy. Use a dedicated folder (INBOX/ToProcess) rather than INBOX directly, and move messages to Processed/Failed after handling. This gives you a real queue with replay semantics and keeps the mailbox tidy.
Testing. Abstract the client behind a small interface (fetchUnseen(): iterable) and fake it in tests. For integration tests, point at a disposable Dovecot/GreenMail container and seed .eml fixtures rather than hitting a real server.
config/fetch.php, no facade. Expect to write the wiring yourself or use the underlying library directly. Budget for that.composer.json for the Symfony version constraint — it likely won't support Symfony 6/7 without a fork.UIDVALIDITY changes are silent breakage. When the server changes UIDVALIDITY, all stored UIDs become invalid. Detect the change and re-sync rather than treating it as duplicates.UNSEEN on a 100k-message mailbox will exhaust memory and hit server timeouts. Use SINCE, UID ranges, or BODY.PEEK[] with limits. Fetch headers first, then bodies on demand.utf8_decode your way through it.novalidate-cert is a common requirement for internal mail servers; exposing that as an env toggle saves you an incident. Also set connection/read timeouts — the default can hang a worker indefinitely.memory_limit. Stream to disk if the library supports it; otherwise cap message size server-side and skip oversized ones explicitly.markAsSeen() mutates state even if processing later throws. Prefer moving to a Processed folder only after successful handling, or track your own processed state and leave server flags alone.retry(3, 1000) on connect and logs fetch counts.expunge()/disconnect() in a finally block.How can I help you explore Laravel packages today?