Skip to content

Transformer Contract

Reference for the pluggable transformation and storage pipeline introduced in CFX-46. This covers the interface every platform transformer must implement, the result value objects, and the unified Eloquent models.

Interface: TransformerContract

Namespace: App\Contracts\TransformerContract

public function transform(Collection $data, SystemConnection $connection): TransformationResult;

Accepts a Collection<int, CustomerData|OrderData|ProductData> (the output of an integration's requestBulkExport()) and a SystemConnection. Persists each item to the unified Eloquent models. Returns a TransformationResult summarising successes and failures.

The method never throws — any exception raised while storing an individual item is captured and included in the result.

Abstract Base Class: AbstractTransformer

Namespace: App\Services\AbstractTransformer

Implements TransformerContract. Concrete transformers extend this class and implement store().

transform() is declared final. It iterates over the data collection, calls store() for each item, and catches any Throwable per item — meaning a failure on one record does not abort the rest of the batch. The result carries the full failure list so callers can log or retry specific items.

abstract protected function store(CustomerData|OrderData|ProductData $item, SystemConnection $connection): void;

This is the only method a concrete transformer must implement.

Result Value Objects

TransformationResult

Namespace: App\Data\TransformationResult

Property Type Description
succeeded int Number of items stored successfully
failed int Number of items that raised an exception
failures TransformationFailure[] One entry per failed item

TransformationFailure

Namespace: App\Data\TransformationFailure

Property Type Description
item CustomerData\|OrderData\|ProductData The DTO that could not be stored
reason Throwable The exception raised during store()

Unified Eloquent Models

These models are the persistent representation of normalised platform data inside Conflux. All use HasUuids and SoftDeletes.

Customer

Namespace: App\Models\Customer
Table: customers

Column Type Description
id uuid Conflux-assigned primary key
system_connection_id uuid FK The system connection that produced this record
source_id string System-native customer ID (dedup key)
source_custom_id ?string Secondary system identifier (e.g. internal CRM ID)
source_platform string Cast to SystemDriver enum
email string
first_name ?string
last_name ?string
phone ?string
gender ?string Cast to Gender enum
title ?string
zip ?string
date_of_birth ?string ISO 8601 date string
address_billing ?json Serialised AddressData
address_shipping ?json Serialised AddressData
tags ?json Array of tag strings
order_count ?int Total number of orders on the source platform
total_revenue ?int Lifetime revenue in minor currency units
external_data ?json Platform-specific key/value pairs from customAttributes

Unique constraint: (system_connection_id, source_id)

Relations: systemConnection()BelongsTo<SystemConnection>, consents()HasMany<CustomerConsent>, orders()HasMany<Order>

CustomerConsent

Namespace: App\Models\CustomerConsent Table: customer_consents

Column Type Description
id uuid Primary key
customer_id uuid FK Owning customer
channel string Cast to ConsentChannel enum (email, sms, general)
subscribed bool Whether the customer is subscribed
confirmed bool Whether the subscription is confirmed (double opt-in)
subscribed_at ?string ISO 8601 datetime string
confirmed_at ?string ISO 8601 datetime string
unsubscribed_at ?string ISO 8601 datetime string
source ?string Cast to ConsentSource enum
ip_address ?string

Unique constraint: (customer_id, channel) — one consent record per channel per customer.

Relations: customer()BelongsTo<Customer>

Order

Namespace: App\Models\Order
Table: orders

Column Type Description
id uuid Conflux-assigned primary key
system_connection_id uuid FK The system connection that produced this record
customer_id ?uuid FK Linked Customer (nullable)
source_id string System-native order ID (dedup key)
source_order_number ?string Human-readable order number on the system
source_platform string Cast to SystemDriver enum
customer_source_id ?string System-native ID of the associated customer
status string System-native status string (not normalised)
currency string ISO 4217 currency code
total int Grand total in minor currency units
total_tax ?int Tax portion in minor currency units
total_shipping ?int Shipping portion in minor currency units
address_billing ?json Serialised AddressData
address_shipping ?json Serialised AddressData
tags ?json Array of tag strings
date_paid ?string ISO 8601 datetime string
external_data ?json System-specific key/value pairs from customAttributes

Unique constraint: (system_connection_id, source_id)

Relations: systemConnection()BelongsTo<SystemConnection>, customer()BelongsTo<Customer>, lineItems()HasMany<LineItem>

LineItem

Namespace: App\Models\LineItem Table: line_items

Column Type Description
id uuid Primary key
order_id uuid FK Owning order
product_id ?uuid FK Resolved Product — null if the product has not been synced
source_id string System-native line item ID (dedup key within the order)
product_source_id string System-native product ID used to resolve product_id
name string Product name at time of order
quantity int
unit_price int Price per unit in minor currency units
total int Line total in minor currency units
product_custom_id ?string Secondary system product identifier
sku ?string
currency ?string ISO 4217 currency code

Unique constraint: (order_id, source_id)

Relations: order()BelongsTo<Order>, product()BelongsTo<Product>

product_id is resolved at import time by looking up Product with (system_connection_id, product_source_id). If the product has not been synced yet, product_id remains null until the order is re-imported after the product sync.

Product

Namespace: App\Models\Product
Table: products

Column Type Description
id uuid Conflux-assigned primary key
system_connection_id uuid FK The system connection that produced this record
source_id string System-native product ID (dedup key)
source_custom_id ?string Secondary system identifier
source_platform string Cast to SystemDriver enum
name string
description ?text
url ?string Product page URL on the system
image_url ?string Primary product image URL
status string Cast to ProductStatus enum
sku ?string
price ?int Current price in minor currency units
original_price ?int Pre-discount price in minor currency units
currency ?string ISO 4217 currency code
brand_name ?string
target_group ?string
categories ?json Array of category strings
tags ?json Array of tag strings
order_count ?int Number of times this product has been ordered
external_data ?json System-specific key/value pairs from customAttributes

Unique constraint: (system_connection_id, source_id)

Relations: systemConnection()BelongsTo<SystemConnection>

Default store() Implementation

AbstractTransformer provides a concrete store() method that handles all three entity types via a match expression. It uses updateOrCreate keyed on (system_connection_id, source_id) so that re-running a bulk export updates existing records rather than creating duplicates.

Platform-specific transformers extend AbstractTransformer and override store() only when they need behaviour that differs from the default. Until that need arises, a minimal subclass with no overrides is sufficient.

TransformerRegistry

Namespace: App\Services\TransformerRegistry

A singleton that maps each SystemDriver to a concrete transformer class. It mirrors IntegrationRegistry and is the only mechanism by which Conflux locates the correct transformer at runtime.

public function register(SystemDriver $driver, string $transformer): void
public function resolve(SystemDriver $driver): AbstractTransformer
  • register() — associates a SystemDriver case with a class-string<AbstractTransformer>. Called once per platform in AppServiceProvider::boot().
  • resolve() — returns an instance of the registered transformer, resolved via the service container (app($class)). Throws RuntimeException if no transformer has been registered for the given driver.

The registry is bound as a singleton in AppServiceProvider::register():

$this->app->singleton(TransformerRegistry::class);

PersistBulkDataJob receives the registry via constructor injection and calls resolve() at job execution time to obtain the transformer for the connection's system driver.

Registration is required

A transformer that is not registered in TransformerRegistry cannot be resolved. Any PersistBulkDataJob dispatched for its system driver will throw a RuntimeException at execution time. Every transformer implementation must be registered — see Implement a System Transformer.

PersistBulkDataJob

Namespace: App\Jobs\PersistBulkDataJob
Queue connection: rabbitmq

The job that bridges the bulk-fetch stage and the persistence stage. BulkSyncJob dispatches it after each successful requestBulkExport() call when the result is non-empty.

public function __construct(
    public readonly Collection $data,           // Collection<int, CustomerData|OrderData|ProductData>
    public readonly SystemConnection $systemConnection,
) {}

On execution, it resolves the correct transformer from TransformerRegistry using the system driver of the associated SystemConnection, then calls transform(). If the connection has no associated system, the job exits silently.

The job runs on the rabbitmq connection, which places it on a different queue from BulkSyncJob (which runs on the default redis-backed queue). This separates the fetch stage from the persistence stage and allows them to scale independently.