PHPackages                             shaelz/stage-gate - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. shaelz/stage-gate

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

shaelz/stage-gate
=================

A typed import pipeline: proof, stage, review, approve, publish.

v0.1.1(1mo ago)07MITPHPPHP ^8.2CI passing

Since Jul 3Pushed 1mo agoCompare

[ Source](https://github.com/Shaelz/stage-gate)[ Packagist](https://packagist.org/packages/shaelz/stage-gate)[ Docs](https://github.com/Shaelz/stage-gate)[ RSS](/packages/shaelz-stage-gate/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (10)Versions (3)Used By (0)

stage-gate
==========

[](#stage-gate)

[![tests](https://github.com/Shaelz/stage-gate/actions/workflows/tests.yml/badge.svg)](https://github.com/Shaelz/stage-gate/actions/workflows/tests.yml)[![Packagist](https://camo.githubusercontent.com/88a9f6b46c0c46d32d941ca73320216f1d8ef0a447c78241aec7d0cd0d75f9ea/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f736861656c7a2f73746167652d67617465)](https://packagist.org/packages/shaelz/stage-gate)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)

A typed import pipeline for PHP: **proof, stage, review, approve, publish.** The safety layer between "someone uploaded a file" and "the database changed."

Why
---

[](#why)

Most import features skip straight from "parse the file" to "write the rows." That works until someone re-uploads a file that overlaps with data already in the system, and something gets silently overwritten. The fix isn't a bigger try/catch — it's a pipeline with named stages, an explicit diff-classification step before any write, and a publish step that's all-or-nothing.

This pattern was built once inside a real Laravel app, for importing competition fixtures from Excel. It's been running in production since the 2025/2026 season, and has since been swapped in to replace that app's own hand-rolled diff and publish logic — verified against a real production database dump before the swap went live (see [ROADMAP.md](ROADMAP.md) for the details). stage-gate is that pattern, pulled out and made generic over what's being imported.

What it does
------------

[](#what-it-does)

Five stages, each with an explicit outcome:

1. **Proof** — validate the parsed rows against a typed schema. Malformed rows fail here, before anything else runs.
2. **Stage** — hold the valid rows in a pending state, not yet visible to the rest of the system.
3. **Review** — classify every staged row against what already exists: `new`, `unchanged`, `updated`, `overwrite_risk`, or `removed`.
4. **Approve** — a human (or a rule) explicitly acknowledges overwrite-risk rows. Nothing classified as overwrite-risk can publish without that acknowledgment.
5. **Publish** — returns a plan of writes and an audit entry. Your app executes the plan inside its own transaction: either every approved row lands, or none do.

What it isn't
-------------

[](#what-it-isnt)

Not a general ETL framework. It doesn't parse your source files, move data between systems, or schedule jobs — bring your own parser and your own storage. stage-gate isn't a storage layer either: the core never touches a database. `Publish::plan()` hands back a list of writes and deletes plus an audit entry; your app is what executes them, inside its own transaction. This is deliberate — it's what keeps the core usable outside Laravel, and it's why "the host owns storage" runs through every stage.

Installation
------------

[](#installation)

```
composer require shaelz/stage-gate
```

The core (`Proof`, `Stage`, `Classifier`, `Approve`, `Publish`) has no dependencies beyond PHP 8.2. The optional Laravel wrapper (`StageGate\Laravel\*` — a service provider, migrations, Eloquent models, queueable jobs) needs `spatie/laravel-package-tools`, `illuminate/support`, and `illuminate/database` in your app; see the `suggest` entries in [composer.json](composer.json).

Quick example
-------------

[](#quick-example)

```
use StageGate\{Field, FieldGroup, Schema, Proof, Stage, Classifier, Approve, Publish};

// 1. Define what a valid row looks like, and which fields make a change risky.
$schema = new Schema('sku', [
    new Field('sku'),
    new Field('price', validate: fn ($v) => is_numeric($v)),
]);

$fieldGroups = [
    new FieldGroup('metadata', ['name', 'category']),
    new FieldGroup('price', ['price'], isRisk: true),
];

// 2. Proof: validate raw rows against the schema.
$proof = Proof::analyze($rawRows, $schema);
if (! $proof->isValid()) {
    // handle $proof->errors and stop here
}

// 3. Stage: hold the valid rows as a pending batch.
$batch = Stage::stage('import-2026-07-03', $proof->rows);

// 4. Review: classify staged rows against what your app already has.
$existingRows = /* your own query, e.g. WHERE category IN (...) */ [];
$classified = Classifier::classifyAll($batch->rows, $existingRows, $fieldGroups);

// 5. Approve: acknowledge overwrite-risk rows explicitly (or none, to block them).
$approval = Approve::approve($batch, $classified, approvedRowKeys: [], approvedBy: 'jane@example.com');

// 6. Publish: get a plan back, then execute it yourself, inside your own transaction.
$plan = Publish::plan($classified, $approval, source: 'products-2026-07.csv');

DB::transaction(function () use ($plan) {
    foreach ($plan->writes as $write) {
        // $write->changeClass tells you upsert vs. delete (ChangeClass::Removed)
        MyModel::query()->updateOrCreate(['sku' => $write->row->key], $write->row->data);
    }

    AuditLog::create([
        'source' => $plan->audit->source,
        'approved_by' => $plan->audit->approvedBy,
        'change_counts' => $plan->audit->changeCounts,
    ]);
});
```

Using Laravel? The wrapper gives you an `ImportDefinition` interface to bundle the schema/field groups/existing-row query/write logic per import type, plus queueable `ProofAndStageJob`/`PublishJob` classes — see `src/Laravel/`.

Status
------

[](#status)

`v0.1.0`. The core and Laravel wrapper are built, fully tested, and proven against a real Laravel app's production fixture-import pipeline — both its diff and publish stages are swapped over to this package. See [ROADMAP.md](ROADMAP.md) for what's done, what's left, and the extraction notes in [docs/biljartv2-seams.md](docs/biljartv2-seams.md) for the design decisions behind the core (why publish returns a plan instead of touching storage, how overwrite-risk generalizes past one field-group split, and so on).

A written case study, covering that migration in a real production app, is planned once it's run through a real import cycle on this path.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance91

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Total

2

Last Release

43d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/7167970?v=4)[Gerben van der Velde](/maintainers/Shaelz)[@Shaelz](https://github.com/Shaelz)

---

Top Contributors

[![Shaelz](https://avatars.githubusercontent.com/u/7167970?v=4)](https://github.com/Shaelz "Shaelz (23 commits)")

---

Tags

difflaravelimportetlaudit-trailoverwrite-protection

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/shaelz-stage-gate/health.svg)

```
[![Health](https://phpackages.com/badges/shaelz-stage-gate/health.svg)](https://phpackages.com/packages/shaelz-stage-gate)
```

###  Alternatives

[konnco/filament-import

242253.5k2](/packages/konnco-filament-import)[marquine/php-etl

Extract, Transform and Load data using PHP.

182138.5k](/packages/marquine-php-etl)[firefly-iii/data-importer

Firefly III Data Import Tool.

8165.8k](/packages/firefly-iii-data-importer)[fab2s/yaetl

Widely Extended Nodal Extract-Transform-Load ETL Workflow AKA NEJQTL or Nodal-Extract-Join-Qualify-Tranform-Load

64195.1k](/packages/fab2s-yaetl)[cleverage/process-bundle

Process, import/export, transform and validate data with a simple API with Symfony3

2255.2k15](/packages/cleverage-process-bundle)[konnco/filament-safely-delete

344.1k](/packages/konnco-filament-safely-delete)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
