byrcsc/laravel-data-sync · 1.x
Quick start.
Scaffold a products sync, map a supplier CSV onto an Eloquent model, run it inline and queued, and read the run history.
This walkthrough takes a supplier CSV into a Product model, runs it both
inline and through the queue, and reads the result back out of the run history.
It assumes the package is installed and migrated.
1. Scaffold the definition
php artisan make:sync ProductsSyncThat writes app/Syncs/ProductsSync.php. The Sync suffix is appended when you
leave it off, so make:sync Products generates the same class.
2. Describe the feed
Given products/products-2026-07-31.csv on the incoming disk:
SKU,Description,Price,Currency
DS-1001,Anglepoise desk lamp,89.00,PHP
DS-1002,Mechanical keyboard,149.50,PHPfill in the source, the format, the model, the mapping, and the match columns:
namespace App\Syncs;
use App\Models\Product;
use ByRcsc\LaravelDataSync\Contracts\Reader as ReaderContract;
use ByRcsc\LaravelDataSync\Definitions\Field;
use ByRcsc\LaravelDataSync\Definitions\Reader;
use ByRcsc\LaravelDataSync\Definitions\Source;
use ByRcsc\LaravelDataSync\Definitions\SyncDefinition;
final class ProductsSync extends SyncDefinition
{
public function source(): Source
{
return Source::disk('incoming')
->path('products')
->minFileAge(minutes: 2);
}
public function format(): ReaderContract
{
return Reader::csv();
}
public function model(): string
{
return Product::class;
}
public function fields(): array
{
return [
Field::make('sku', from: 'SKU'),
Field::make('name', from: 'Description'),
Field::make('price_cents', from: 'Price')
->transform(fn (string $value): int => (int) round((float) $value * 100)),
Field::make('currency')->default('PHP'),
Field::make('discontinued_at')->optional(),
];
}
public function matchOn(): array
{
return ['sku'];
}
public function rules(): array
{
return [
'sku' => ['required', 'string', 'max:32'],
'price_cents' => ['required', 'integer', 'min:0'],
];
}
}Three things are worth noticing.
minFileAge() defers files whose modification time is too recent, which is how
you avoid reading a file the supplier is still uploading. Deferred files are
counted separately and picked up on a later run.
Field::make('discontinued_at')->optional() says the column may be absent. A
field that is neither optional nor defaulted must be present in the file — a
missing one is a schema mismatch and the run fails before any row is written,
rather than importing half a feed.
matchOn() names the columns that identify a row. Back them with a unique index
on products; sync:doctor warns when they are not.
3. Register it
// config/data-sync.php
'syncs' => [
App\Syncs\ProductsSync::class,
],The name is derived from the class — ProductsSync becomes products. Override
name() on the definition when you want something else.
4. Run it
Start in-process, where you can see everything happen:
php artisan sync:run products --nowRunning [products]...
File run [01JQ8Z3M0K2N4P6R8T0V2X4Y6A] (bulk) ... 2 created, 0 updated, 0 skipped, 0 failed
[products] finished: 1 discovered, 0 skipped, 0 deferred.Run it again and nothing happens: the checksum is in the ledger, and the file is
counted as skipped. Edit the CSV so its contents change — or pass --force — to
process it again.
Now try the queued path, which is what sync:run does by default:
php artisan queue:work # terminal 1
php artisan sync:run products # terminal 2The command discovers the file, stages it, records a run, and dispatches a job.
That job plans the file and fans its chunks out through Bus::batch(), so
several workers can share one file. Nothing is written until a worker picks the
jobs up.
5. Check before you commit to it
php artisan sync:run products --dry-run--dry-run validates and counts without writing models, running write hooks,
archiving the file, cleaning up the source, or marking the checksum processed.
It always runs in-process. The run is still recorded, and sync:status marks it
as a dry run.
Treat the counts as a preview rather than a guarantee: dry runs skip
prepare(), so a definition that reshapes rows there can report slightly
different numbers than the real run.
6. Read the history
php artisan sync:status
php artisan sync:status products --limit=25
php artisan sync:status --failuresThe table shows the run ULID, the definition, the status, the write path, the
created/updated/skipped/failed counters, and — for runs still in flight —
queued batch progress from the batch record.
Run Sync Status · path C/U/S/F Progress
01JQ8Z3M0K2N4P6R8T0V2X4Y6A products completed · bulk 2/0/0/0 2 rows--failures prints one line per failed row: the row number, the validation
errors, and the captured payload. See failures and
retries.
7. React to a finished run
namespace App\Listeners;
use ByRcsc\LaravelDataSync\Events\SyncCompleted;
use Illuminate\Contracts\Queue\ShouldQueue;
final class NotifyMerchandising implements ShouldQueue
{
public function handle(SyncCompleted $event): void
{
if ($event->run->rows_failed === 0) {
return;
}
// $event->run->ulid, ->rows_created, ->rows_failed,
// ->error_summary, ->write_path, ->archive_path
}
}Package events are dispatched synchronously inside the job doing the work, so queue the listener, not the event. See events and listeners.
What to read next
- Sync definitions for everything a definition can declare.
- Matching and writing for write modes, duplicates, chunking, and the bulk versus Eloquent write path.
- Running a sync for
--now,--force, and the discovery lock. - File transfers for feeds that move files rather than rows.