Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Syncro Sheet Laravel Package

zuko/syncro-sheet

Laravel package for syncing Eloquent models with Google Sheets. Export records, import spreadsheet rows, and keep data in sync through configurable mappings and queued jobs—ideal for lightweight admin exports and editable data workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

The 1.0 release is deliberately minimal — "basically sync features" — so treat it as a working core rather than a finished product. There is no ecosystem of docs to lean on yet, which changes the onboarding order: read the source before you read a README.

  1. Install and let discovery do its job:
    composer require <vendor>/<package>:^1.0
    php artisan vendor:publish --provider="Vendor\Package\PackageServiceProvider"
    
  2. Immediately pin the constraint. ^1.0 is fine, but avoid * or dev-main until the API stabilises — a 1.x minor release in a young package can still move method signatures.
  3. Where to look first, in this order:
    • src/*ServiceProvider.php — what's registered, whether a facade/singleton is bound, whether commands exist (php artisan list | grep <package>).
    • config/<package>.php — connection/auth settings. This is your contract with the remote side.
    • The main client/syncer class — look for the public methods on it; that's your entire API surface right now.
    • CHANGELOG.md / tagged diffs between 0.x and 1.0 if the package existed pre-release.
  4. First use case: pick one small, one-way sync of a single model — e.g. push local Product rows to the remote, or pull them down. Do it in tinker before you write a job:
    $syncer = app(\Vendor\Package\Syncer::class);
    $syncer->sync(Product::query()->limit(1)->get());
    
    Confirm the payload shape and the response before scaling up. If sync() accepts a Collection, you now have a chunkById() story; if it accepts a single model, you have a loop and a job.

The first useful deliverable is almost never "sync everything" — it's "sync one model, idempotently, with a unique index in place so a second run is a no-op."

Implementation Patterns

Wrap the sync in an idempotent job. Sync features fail halfway. Make re-running safe by keying on a stable external identifier and using upsert() with a unique index rather than updateOrCreate() in a loop.

final class SyncProducts implements ShouldQueue, ShouldBeUnique
{
    use InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = [10, 60, 300];

    public function handle(Syncer $syncer): void
    {
        Product::query()->chunkById(200, function ($products) use ($syncer) {
            $syncer->sync($products);
        });
    }

    public function uniqueId(): string { return 'sync-products'; }
}

Always pass IDs, not models, across the queue boundary. SerializesModels re-queries on unserialize; a model deleted between dispatch and handle becomes a ModelNotFoundException in the middle of a batch. chunkById inside the job re-reads fresh state and avoids stale hydration.

One direction per job. Bidirectional sync in a single job produces unresolvable merge logic and infinite echo loops once you add model events. Two jobs, SyncDown and SyncUp, each with its own cursor/timestamp watermark.

Use watermarks, not full scans. Track last_synced_at (per-entity, in a small state table or cache) and query where('updated_at', '>', $watermark). Overlap the window by a minute to cover clock skew, and dedupe on the unique key — cheap insurance against drift.

Deletion semantics are a design decision you must make explicitly. Most young sync packages handle create/update and leave deletes to you. Pick one:

  • Soft-delete locally, sync the tombstone, hard-delete on a schedule after confirmation.
  • Or maintain a remote_deleted_at column and filter reads.

Never let the package's absence of a delete story silently become "deletes never propagate" without documenting it.

Compose with Laravel primitives rather than fighting them:

Http::retry(3, 200)->timeout(10)   // if you're wrapping the transport
Bus::batch([...])->allowFailures()->dispatch();

and schedule on a dedicated queue so a slow sync can't starve user-facing jobs:

$schedule->job(new SyncProducts)->everyFiveMinutes()->onOneServer();

Test against fakes, not the network. Http::fake() / Queue::fake() plus a fixture of the remote payload shape. Assert on the database after handle(), not on call counts — call counts break the moment you add chunking.

Gotchas and Tips

  • 1.0 is a floor, not a contract. With "basically sync features," expect the public API to grow methods and possibly rename them. Isolate the package behind your own thin service class (App\Sync\ProductSyncer) so a 1.1 rename is a one-file change.
  • Cross-system sync is not transactional. DB::transaction() protects your side only. If the remote write succeeds and your commit fails, you're inconsistent. Mitigate with idempotency keys sent to the remote, and a sync_log table recording intent before the call.
  • Config caching bites young packages. If the config file references env() outside the config file itself, php artisan config:cache will silently null it. Check the published config; if it calls env() in a service provider, that's a bug — read the config key instead.
  • Timezone and format drift. Carbon in app timezone vs remote UTC is the single most common cause of "it keeps re-syncing the same rows." Store and compare in UTC; cast explicitly (->utc()->toIso8601String()).
  • Idempotency under retry. A 500 after the remote committed means your retry duplicates. Require a unique constraint on the external ID at both ends before you turn on $tries > 1.
  • Unique indexes are mandatory, not optional. upsert() without a unique index is a silent duplicator. Add the migration before the first sync run, not after.
  • Memory on large payloads. ->get() on a 100k-row table plus a JSON encode will hit memory_limit. chunkById(200) and stream where possible.
  • Debugging: log with context — Log::info('sync', ['entity' => 'product', 'id' => $id, 'http_status' => ...]). If the package dispatches events (check for SyncStarted/SyncFailed), attach a listener rather than editing vendor code. php artisan about confirms whether the provider actually discovered; a missing facade binding is usually a cached config from a previous install.
  • Extension points to look for: a bound interface (Contracts\Syncer) you can rebind in your own provider, Macroable traits on the client, and any sync.* config array of per-entity handlers. Those are the supported seams — prefer them over subclassing internals.
  • Missing features to budget for in 1.0: conflict resolution, batched upserts, webhook receivers, and rate-limit handling are commonly absent at this stage. Plan to build or wait; don't assume.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin