PHPackages                             hasel/aphpsurd-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. hasel/aphpsurd-bundle

ActiveSymfony-bundle[Queues &amp; Workers](/categories/queues)

hasel/aphpsurd-bundle
=====================

Symfony bundle for Absurd, a Postgres-native durable execution engine

0.2.0(1mo ago)01MITPHPPHP &gt;=8.4CI passing

Since Apr 19Pushed 1mo agoCompare

[ Source](https://github.com/hasel-at/aphpsurd-bundle)[ Packagist](https://packagist.org/packages/hasel/aphpsurd-bundle)[ RSS](/packages/hasel-aphpsurd-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (14)Versions (3)Used By (0)

[![aphpsurd bundle](aphpsurd-banner.svg)](aphpsurd-banner.svg)

hasel/aphpsurd-bundle
=====================

[](#haselaphpsurd-bundle)

Symfony bundle wrapping [ruudk/absurd-php-sdk](https://github.com/ruudk/absurd-php-sdk), the PHP client for [Absurd](https://github.com/earendil-works/absurd), a Postgres-native durable execution engine built by [earendil-works](https://github.com/earendil-works/absurd).

Installation
------------

[](#installation)

```
composer require hasel/aphpsurd-bundle
```

Symfony Flex registers the bundle automatically. Without Flex, add it manually:

```
// config/bundles.php
return [
    Hasel\AphpsurdBundle\AphpsurdBundle::class => ['all' => true],
];
```

Configuration
-------------

[](#configuration)

The bundle picks up the default Doctrine DBAL connection automatically. Everything else is opt-in.

```
# config/packages/aphpsurd.yaml
aphpsurd:
    doctrine: true             # true = default connection, string = named connection, false = use dsn
    dsn: '%env(ABSURD_DSN)%'   # only when doctrine: false
    default_queue: default
    claim_timeout: 120         # seconds
    poll_interval: 250         # milliseconds
    default_max_attempts: 5
    queues:
        notifications:
            max_attempts: 3
            rate_limiter: notifications  # optional, requires symfony/rate-limiter
            retry_strategy:
                kind: exponential  # exponential | linear | fixed | none
                base_seconds: 2
                factor: 2.0
                max_seconds: 60
            cancellation:
                max_duration: 300
                max_delay: 30
```

Per-queue settings are applied as defaults when spawning tasks on that queue. Per-call `SpawnOptions` always take precedence.

Tasks
-----

[](#tasks)

Tasks are invokable classes tagged with `#[AsAbsurdTask]`, auto-discovered via Symfony's autoconfiguration.

```
use Hasel\AphpsurdBundle\Attribute\AsAbsurdTask;
use Ruudk\Absurd\Task\Context;

#[AsAbsurdTask('send-invitations', queue: 'notifications')]
final class SendInvitationsTask
{
    public function __invoke(SendInvitationsPayload $params, Context $ctx): mixed
    {
        // ...
    }
}
```

Typed payload deserialization requires `symfony/serializer`.

Spawning tasks
--------------

[](#spawning-tasks)

Inject `AbsurdClientInterface` wherever you need to produce work — controllers, services, event listeners. It resolves task class names to their configured task name and queue automatically, applies per-queue defaults from config, and in debug mode integrates with the Symfony profiler.

When a handler's `__invoke` has a typed payload parameter, you can pass the payload object directly and the bundle resolves the task from its type:

```
use Hasel\AphpsurdBundle\AbsurdClientInterface;

final class OrderController
{
    public function __construct(private AbsurdClientInterface $absurd) {}

    public function checkout(): Response
    {
        $this->absurd->spawn(new SendInvitationsPayload($orderId));
        $this->absurd->emitEvent('order.confirmed', ['orderId' => $id]);
        // ...
    }
}
```

Each payload type may only be used in a single handler's `__invoke` — the bundle throws a `LogicException` at container compile time otherwise.

Alternatively, pass the handler class name explicitly:

```
$this->absurd->spawn(SendInvitationsTask::class, $payload);
```

Override defaults per call:

```
use Ruudk\Absurd\Task\SpawnOptions;

$this->absurd->spawn(new SendInvitationsPayload($orderId), options: new SpawnOptions(maxAttempts: 1));
```

Workers
-------

[](#workers)

Run one worker process per queue. Graceful shutdown on `SIGTERM`/`SIGINT` requires `ext-pcntl`.

```
php bin/console absurd:consume
php bin/console absurd:consume --queue=notifications
php bin/console absurd:consume --limit=100 --time-limit=3600 --memory-limit=128M
```

Rate limiting
-------------

[](#rate-limiting)

Per-queue rate limiting is supported via `symfony/rate-limiter`. Install it first:

```
composer require symfony/rate-limiter
```

Configure a rate limiter in Symfony (e.g. using the `framework` bundle), then reference it by service ID in the queue config:

```
# config/packages/framework.yaml
framework:
    rate_limiter:
        notifications:
            policy: fixed_window
            limit: 10
            interval: '1 minute'
            storage_service: cache.app
```

```
# config/packages/aphpsurd.yaml
aphpsurd:
    queues:
        notifications:
            rate_limiter: notifications
```

After each batch of tasks the worker consumes one token per task processed. If the limit is exhausted it waits until the window resets before continuing. For multi-host deployments use a shared storage backend (e.g. Redis) for the limiter.

Service reset between tasks
---------------------------

[](#service-reset-between-tasks)

The bundle ships a `ResetServicesListener` that calls Symfony's `services_resetter` after every completed or failed task. This resets Doctrine entity managers, Monolog buffers, and anything else tagged `kernel.reset`.

Treat the worker like any long-running process: don't cache `EntityManagerInterface` results in properties, fetch them per-task.

Migration
---------

[](#migration)

The bundle vendors the Absurd SQL schema. The recommended approach is to keep the Absurd schema in your Doctrine migration history alongside the rest of your database.

To prevent Doctrine from picking up the `absurd` schema in its own migrations, add a schema filter to your DBAL config:

```
# config/packages/doctrine.yaml
doctrine:
    dbal:
        schema_filter: '~^(?!absurd)~'
```

It is known, that Doctrine might still create a `CREATE SCHEMA absurd` statement inside the `down`-step in an otherwise empty migration.

When you run `make:migration`, the bundle checks whether your installed Absurd schema is behind the version the bundle requires. If `doctrine/doctrine-migrations-bundle` is installed, it automatically generates a dedicated migration file with the pending SQL. Otherwise it prints the pending filenames for you to apply manually.

```
php bin/console make:migration
# with doctrine/migrations:    → generates DoctrineMigrations/VersionXXX.php
# without doctrine/migrations: → warning listing pending SQL files to apply manually
```

For non-Doctrine setups, apply the schema directly:

```
php bin/console absurd:migrate
php bin/console absurd:migrate --dry-run  # list pending files without applying
```

Commands
--------

[](#commands)

CommandDescription`absurd:migrate`Apply Absurd schema migrations`absurd:migrate --dry-run`List pending migration files without applying them`absurd:setup-queues`Create configured queues in the database`absurd:setup-queues --prune`Also remove queues not in config`absurd:consume`Start a worker`absurd:cleanup`Delete old completed tasks and events`absurd:list-tasks`List all registered task handlersProfiler
--------

[](#profiler)

In debug mode the bundle registers a Web Debug Toolbar panel showing all spawned tasks (payload, options, errors) and emitted events per request.

AI
--

[](#ai)

In the spirit of the original absurd code base, this bundle was written with support from Claude Code.

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance90

Actively maintained with recent releases

Popularity1

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

50d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3c1f121b5492c5dc7a30b3da54b567c4c1bef5d190e78bb6f8c51918f8cc2fa9?d=identicon)[wsl](/maintainers/wsl)

---

Top Contributors

[![crazy-weasel](https://avatars.githubusercontent.com/u/22397?v=4)](https://github.com/crazy-weasel "crazy-weasel (8 commits)")

---

Tags

postgresqueueworkflowSymfony Bundleabsurddurable-execution

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/hasel-aphpsurd-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/hasel-aphpsurd-bundle/health.svg)](https://phpackages.com/packages/hasel-aphpsurd-bundle)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
