boutdecode/etl-core-bundle
Symfony bundle for building configurable ETL pipelines with reusable Workflow templates and time-bound Pipeline runs. Chains Extract, Transform, and Load steps via Symfony Messenger, Workflow, and Flow-PHP, with DDD/CQS structure, status tracking, and per-run configuration overrides.
enum support in configurationSchemaFields declared in a step's configurationSchema (see v0.11.0) can now restrict the value to a fixed list of allowed values:
configurationSchema: [
'mode' => ['type' => 'string', 'enum' => ['append', 'overwrite']],
],
in_array($value, $enum, true)).type — both constraints must pass.type.InvalidStepConfigurationException with the list of allowed values in the message.This is purely additive — no existing public signatures change. See the README for the full DSL.
configurationSchema on #[AsExecutableStep]Steps can now declare the expected shape of their configuration, in addition to the existing human-readable configurationDescription:
#[AsExecutableStep(
code: 'etl.loader.csv_file',
configurationSchema: [
'destination' => ['type' => 'string'],
'withHeader' => ['type' => 'boolean'],
],
)]
getConfigurationSchema() returns it verbatim, and it is now enforced at runtime: whenever setConfiguration() is called (e.g. by the pipeline runner), the given configuration is validated against the schema and an InvalidStepConfigurationException is thrown on mismatch.
Schema DSL, per field:
| Key | Description |
|---|---|
type |
One or more of string, number, integer, boolean, array, object, null, separated by | |
required |
(optional, bool) throws if the field is absent from the configuration |
properties |
(optional, with type: 'array') nested schema validated against each element |
schema |
(optional, with type: 'object') nested schema validated against the value itself |
All 9 built-in steps (CSV/JSON/XML extractors and loaders, filter, data mapping, console command executor) now declare their configurationSchema.
This is purely additive — no existing public signatures change. See the README for the full DSL and an example.
EmailNotificationProvider now sends HTML instead of plain text. The email body includes:
<pre> block (JSON pretty-printed for arrays/objects, HTML-escaped)NotificationMessage gains a new optional field:
| Field | Type | Description |
|---|---|---|
result |
mixed |
The pipeline's final result (Context::getResult()), or null if none was produced |
This is purely additive — no existing public signatures change. See the README for the full field list and a custom-provider example.
Old Pipeline runs can now be purged automatically so storage doesn't grow unbounded. A daily scheduled job (PurgeOldPipelines, registered in ETLScheduler) deletes pipelines in a terminal state (COMPLETED/FAILED) whose finishedAt is older than a configurable threshold, together with their PipelineHistory and StepHistory records. WorkflowStatistic/WorkflowExecutionStatistic records are intentionally left untouched so historical reporting survives purges.
Disabled by default.
# config/packages/boutdecode_etl_core.yaml
boutdecode_etl_core:
purge:
enabled: true # default: false
retention_days: 30 # default: 30 — minimum: 1
cron_expression: '0 3 * * *' # default: '0 3 * * *' — validated at compile time
See the README for the full behavior and a Doctrine implementation example.
PipelinePersister, PipelineHistoryPersister and StepHistoryPersister each gain a new delete() method, and PipelineProvider gains findPurgeablePipelines(\DateTimeImmutable $before): array. Consuming applications implementing these interfaces must add the corresponding methods.
Follow-up to v0.7.0: notification preferences are now dedicated fields on Workflow instead of being read from the generic getConfiguration() array.
Workflow gains three new methods: isNotifyOnSuccess(): bool, isNotifyOnFailure(): bool, getNotificationProviders(): ?array.AbstractWorkflow provides default implementations (false, false, null).WorkflowFactory::create() gains matching optional parameters: notifyOnSuccess, notifyOnFailure, notificationProviders.$workflow = $workflowFactory->create(
name: 'daily-import',
notifyOnSuccess: false,
notifyOnFailure: true,
notificationProviders: ['email'], // optional — null defaults to every registered provider
);
Consuming applications implementing Workflow (or extending AbstractWorkflow) should add the corresponding columns/getters — see the README for a complete Doctrine entity example.
(Note: v0.8.0 was published and then deleted shortly after due to a tagging process issue — no functional difference between v0.8.0 and v0.8.1.)
Get notified when a Workflow run succeeds and/or fails. After each pipeline run, NotificationMiddleware (priority -70, right after PipelineStatisticMiddleware) dispatches to every configured NotificationProvider. Sending a notification never fails the pipeline — errors are logged and swallowed.
Workflow$workflow = $workflowFactory->create(
name: 'daily-import',
configuration: [
'notifications' => [
'on_success' => false,
'on_failure' => true,
'providers' => ['email'], // optional — defaults to every registered provider
],
],
);
Ships with EmailNotificationProvider (via symfony/mailer). Recipients and sender are configured globally:
boutdecode_etl_core:
notifications:
email:
from: '[email protected]'
to: ['[email protected]']
subject_prefix: '[ETL]'
Implement NotificationProvider and tag the class with #[AsNotificationProvider(code: '...')] — it is automatically registered (same pattern as ExecutableStep). See the README for a complete example.
Full Changelog: https://github.com/boutdecode/etl-core/compare/v0.5.0...v0.6.0
Full Changelog: https://github.com/boutdecode/etl-core/compare/v0.4.0...v0.5.0
Automatic pre-aggregated execution metrics per pipeline, updated after every run.
| Interface | Role |
|---|---|
PipelineStatisticFactory |
Creates a fresh PipelineStatistic for a given pipeline |
PipelineStatisticProvider |
findByPipeline() and findAll() |
PipelineStatisticPersister |
create() (first save) and save() (update) |
PipelineHistoryProvider |
findByPipeline() and findByPipelineBetween() |
getSuccessRate(): float)GetPipelineStatisticQuery($pipelineId) — returns the aggregated PipelineStatisticGetPipelineHistoriesQuery($pipelineId, $from, $to) — returns raw PipelineHistory records over a period (for temporal trend)Extend AbstractPipelineStatistic and add Doctrine mappings — see the README for a complete example.
scheduled state in the Pipeline state machine, inserted between pending and in_progressschedule() transition: pending → scheduled (triggered by ExecutePipelineHandler when a pipeline is picked up)reset transition now also accepts scheduled as a source stateschedule() method on Pipeline interface and AbstractPipelineschedule() method on PipelineWorkflow interface and PipelineStateMachineIsPipelineExecutable now checks for SCHEDULED status instead of PENDINGExecutePipelineHandler transitions each pipeline to scheduled before dispatching ExecuteWorkflowCommandpending → (schedule) → scheduled → (start) → in_progress → (complete/fail) → completed/failed
completed/failed → (reset) → pending
failed → (restart) → in_progress
name property to Pipeline interface and AbstractPipeline?string $name = null parameter to PipelineFactory::create() and PipelineFactory::createFromWorkflow()DefaultPlannedTaskRunner now names pipelines after their workflow: "<workflow> pipeline"Add a new Executor step category alongside Extractor, Transformer and Loader.
ExecutorStep — interface extending ExecutableStep, exposes execute(string $command, array $arguments, Context $context, array $configuration): mixedAbstractExecutorStep — base class resolving command and arguments from the Context configuration, with fallback to the step's default configurationConsoleCommandExecutorStep — built-in implementation running a Symfony console command via Application::run(), returns array{ exitCode: int, output: string }ConsoleCommandExecutorStep (etl.executor.console_command)| Key | Description |
|---|---|
command |
Symfony console command name (e.g. app:my-command) |
arguments |
Associative array of arguments and options |
catchExceptions |
Whether to catch exceptions (default: false) |
symfony/console ^6.4\|\|^7.0 added as require-devFirst stable release of ETLCoreBundle — a reusable Symfony Bundle providing a configurable ETL (Extract / Transform / Load) pipeline engine.
Core
Context DTO — the central pass-through object between stepsAbstractExtractorStep, AbstractTransformerStep, AbstractLoaderStep)PipelineStatus enum and pipeline lifecycle managementBuilt-in ETL steps
| Step | Type |
|---|---|
CsvFileExtractStep |
Extractor |
JsonFileExtractStep |
Extractor |
XmlFileExtractStep |
Extractor |
FilterTransformStep |
Transformer |
DataMappingTransformStep |
Transformer |
CsvFileLoadStep |
Loader |
JsonFileLoadStep |
Loader |
DatabaseLoadStep |
Loader |
Execution engine
DefaultPipelineRunner with middleware chains (pipeline-level & step-level)CycleLife middlewares for pipeline & step lifecycle trackingpipeline_lifecycle state machine)PipelineScheduler with #[AsCronTask])CQS layer
CommandBus / QueryBus interfaces backed by Symfony MessengerAsyncCommand / AsyncQuery for opt-in async transportcommand.bus, query.bus, event.bus>= 8.2^6.4 || ^7.0composer require boutdecode/etl-core-bundle
How can I help you explore Laravel packages today?