PHP-ETL - Rule Engine

Rule Engine - Custom Rules

Adding a Custom Rule

The six built-in rules cover most cases, but you can register your own. A rule has two parts: a RuleConfigInterface value object (the typed arguments) and a RuleInterface implementation (the logic).

1. Define the config:

use Oliverde8\Component\RuleEngine\RuleConfig\RuleConfigInterface;

final class UppercaseFirstRuleConfig implements RuleConfigInterface
{
    public function __construct(public readonly RuleConfigInterface $value) {}
}

2. Implement the rule. Extend AbstractRule and implement ConfigurableRuleInterface β€” getConfigClass() tells RuleApplier which config class this rule handles:

use Oliverde8\Component\RuleEngine\Rules\AbstractRule;
use Oliverde8\Component\RuleEngine\Rules\ConfigurableRuleInterface;
use Oliverde8\Component\RuleEngine\RuleConfig\RuleConfigInterface;

final class UppercaseFirstRule extends AbstractRule implements ConfigurableRuleInterface
{
    public function getConfigClass(): string
    {
        return UppercaseFirstRuleConfig::class;
    }

    public function applyConfig(array $rowData, array &$transformedData, RuleConfigInterface $config): mixed
    {
        assert($config instanceof UppercaseFirstRuleConfig);
        $value = $this->ruleApplier->applyConfig($rowData, $transformedData, $config->value);
        return ucfirst((string) $value);
    }

    // Only needed if you also want the deprecated array syntax to support this rule.
    public function apply(array $rowData, array &$transformedData, array $options = [])
    {
        return ucfirst((string) $options['value']);
    }

    public function validate(array $options): void
    {
        $this->requireOption('value', $options);
    }

    public function getRuleCode(): string
    {
        return 'uppercase_first';
    }
}

3. Register it on a RuleApplier and inject that instance wherever RuleTransformConfig, FilterDataConfig, IfConfig, or SwitchConfig are used β€” they all take a RuleApplier as a constructor dependency, resolved through GenericChainFactory’s injections:

🐘 Standalone

use Oliverde8\Component\PhpEtl\ChainBuilderV2;
use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\RuleTransformOperation;
use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\RuleTransformConfig;
use Oliverde8\Component\PhpEtl\GenericChainFactory;
use Oliverde8\Component\RuleEngine\RuleApplier;
use Oliverde8\Component\RuleEngine\Rules\Get;
// ... plus any other built-in rules you need (Implode, StrToLower, ...)

$ruleApplier = new RuleApplier($logger, [
    new Get($logger),
    new UppercaseFirstRule(),
]);

$chainBuilder = new ChainBuilderV2($executionContextFactory, [
    new GenericChainFactory(
        RuleTransformOperation::class,
        RuleTransformConfig::class,
        injections: ['ruleApplier' => $ruleApplier],
    ),
    // ... other factories, passing the same $ruleApplier to FilterData/If/Switch as needed
]);

🎡 Symfony

Tag your rule so it’s collected automatically, then reference the tagged RuleApplier service when wiring the GenericChainFactory for each operation:

services:
  App\Etl\Rules\UppercaseFirstRule:
    autowire: true
    tags: [{ name: 'etl.rule' }]

Once registered, UppercaseFirstRuleConfig works anywhere a RuleConfigInterface is accepted, exactly like a built-in rule:

(new RuleTransformConfig(add: false))
    ->addColumn('FirstName', new UppercaseFirstRuleConfig(new GetRuleConfig('first_name')));