Browse documentationOpen

byrcsc/laravel-data-sync · 1.x

Sync definitions.

The anatomy of a SyncDefinition class, how its name and registration work, what the container resolves, and the validation applied before a definition ever runs.

A sync definition is a class extending SyncDefinition. It answers five required questions and adjusts a dozen optional ones. Behavior lives here; config/data-sync.php holds only infrastructure.

The required surface

use ByRcsc\LaravelDataSync\Contracts\Reader as ReaderContract;
use ByRcsc\LaravelDataSync\Definitions\Source;
use ByRcsc\LaravelDataSync\Definitions\SyncDefinition;

final class ProductsSync extends SyncDefinition
{
    public function source(): Source;      // where files come from
    public function format(): ReaderContract; // how to read them
    public function model(): string;       // the destination Eloquent model
    public function fields(): array;       // the mapping, as Field objects
    public function matchOn(): array;      // the columns that identify a row
}

Everything else has a default:

MethodDefaultDocumented in
name()kebab-case class basename minus Syncbelow
rules()[]Validation and hooks
writeMode()WriteMode::UpsertMatching and writing
onDuplicate()DuplicatePolicy::LastWinsMatching and writing
onUnknownColumns()UnknownColumnPolicy::IgnoreField mapping
captureUnmappedInto()nullField mapping
chunkSize()1000Matching and writing
maxErrors()nullMatching and writing
atomic()falseMatching and writing
captureFailedPayloads()trueSensitive data
prepare()returns the row unchangedValidation and hooks
withValidator()no-opValidation and hooks
beforeSave()not declaredValidation and hooks
afterSave()not declaredValidation and hooks
queueConnection()nullQueues and workers
queue()nullQueues and workers
schedule()not declaredScheduling

Names

The name is what you type on the command line, what the ledger and runs record, and what data-sync.overrides keys on. It is derived from the class basename with a trailing Sync removed, then kebab-cased:

ClassName
ProductsSyncproducts
SupplierPricingSyncsupplier-pricing
Inventoryinventory

Override it when the class name is wrong for operators:

public function name(): string
{
    return 'supplier-catalog';
}

Two definitions may not share a name. A collision throws an InvalidConfigurationException at boot.

Registration

// config/data-sync.php
'syncs' => [
    App\Syncs\ProductsSync::class,
    App\Syncs\StatementsTransfer::class,
],

Order does not matter. sync:run with no argument runs every enabled definition, in registration order.

Disable one without removing it:

'overrides' => [
    'products' => ['enabled' => false],
],

A disabled definition is skipped by sync:run and is not scheduled. It is still validated, and still addressable by name if you run it explicitly.

Definitions are resolved from the container

Every definition is built with app()->make(), so constructor injection works:

final class ProductsSync extends SyncDefinition
{
    public function __construct(
        private readonly CurrencyConverter $currency,
    ) {}

    public function fields(): array
    {
        return [
            Field::make('price_cents', from: 'Price')
                ->transform(fn (string $value): int => $this->currency->toCents($value)),
        ];
    }
}

Definitions are resolved fresh on each use, including inside queued jobs, so they must not hold per-run state. The one piece of mutable state the base class carries is the payload-capture flag set by withoutPayloadCapture().

Validation happens before anything runs

Resolving a definition validates it. sync:run, sync:status, sync:doctor, and the queued jobs all go through the same path, so a broken definition fails loudly and early rather than half way through a file.

A SyncDefinition must satisfy all of the following:

  • fields() is non-empty and contains only Field instances, with no duplicate attribute names.
  • model() names an existing class extending Illuminate\Database\Eloquent\Model.
  • matchOn() is non-empty, and every entry names a mapped field.
  • chunkSize() is at least 1, and maxErrors(), when set, is not negative.
  • Every mapped attribute is a real column on the model's table, or has a set mutator. This check is skipped when the table does not exist yet.
  • When onUnknownColumns() is Capture, captureUnmappedInto() names an attribute cast to array, json, object, collection, encrypted:array, or encrypted:collection.

Failures throw an InvalidDefinitionException naming the class and the problem.

Transfers

A TransferDefinition shares source(), name(), the queue methods, and schedule(), and replaces the row-mapping surface with matchFilename() and destination(). Both kinds go in the same syncs array and run through the same commands. See file transfers.