Skip to content

M7 — Delivery, scale & more FeedTypes (final milestone) - #117

Merged
loevgaard merged 7 commits into
1.xfrom
feature/m7-delivery-scale-feedtypes
Jul 2, 2026
Merged

M7 — Delivery, scale & more FeedTypes (final milestone)#117
loevgaard merged 7 commits into
1.xfrom
feature/m7-delivery-scale-feedtypes

Conversation

@loevgaard

Copy link
Copy Markdown
Member

M7 — Delivery, scale & more FeedTypes (§6.3, §8.2–8.6, §12) — the final milestone

Five chunks; 747 tests; the delivery-targets form + all 7 feed types are browser-verified.

gzip + splitting + SplitManifest

  • OutputWriter rotates a context into {contextKey}-{n}.{ext} parts when a maxItems/maxBytes
    limit (format default merged with Feed.formatConfig['split']) is hit, reusing the same
    writer/config (CSV re-emits its header per part). Single-part output stays byte-identical.
  • SplitManifest strategy: none (self-describing parts) / supplemental (google_rss → a
    <manifest> naming the parts at the canonical path).
  • Optional gzip (Feed.formatConfig['gzip']) via a bounded zlib.deflate filter, per part.

Multi-chunk fan-out + resumability

  • A large single-source plain feed is partitioned into id-ranges; GenerateFeedChunk messages
    render body-only partials; a FeedChunk barrier table with an atomic idempotent counter lets
    exactly one chunk trigger FinalizeFeedContext, which concatenates the ordered partials.
  • Byte-identical to the inline single-chunk path (asserted unit + functional); resumes after a
    simulated mid-run chunk failure
    — the finalize claim (atomic barrier-row delete) fires once, a
    stray finalize is a no-op. A shared FeedContextFinalizer (record → gate → counter → complete) is
    used by both paths. Fan-out engages only for single-source, non-split, non-gzip feeds (threshold
    formatConfig['chunk']['size'], default 10k); split/gzip stay inline.

Per-context multi-target delivery

  • DeliveryTarget (transport / transportConfig / pathTemplate / match); pluggable transports —
    local always, ftp/sftp/s3 gated on class_exists (Flysystem adapters are the integrator's
    optional install, listed under composer suggest).
  • DeliveryService pushes the whole file set (parts + manifest) to every matching target,
    best-effort + isolated (never throws), recording {target,path,status,error?} on
    FeedContextResult.deliveries. Hooked into finalize: a context is delivered only once promoted to
    canonical and published — a gate-blocked context is never delivered.

More FeedTypes (resource-agnosticism)

  • order, customer, taxon, product_review, promotion feed types + data sources + resolvers.
    An Order CSV export and a Taxon feed generate via the real engine (functional tests). 7 feed
    types now show in the admin source picker.

Cron

  • setono:feed:process --all (explicit form) + a README Scheduling section with a crontab example;
    fixed the README's stale command name.

CI note

Verified against the updated PHPStan/Rector/ECS and PHP-8.1-pinned PHPStan + league/csv 9.6, so
the gate (Unit / Integration / Static Code Analysis / Coding Standards) should be green. BC,
Dependency Analysis and Mutation remain the standing rewrite reds.

This is the last milestone — it completes the M0–M7 resource-agnostic rewrite.

loevgaard added 5 commits July 2, 2026 17:49
- OutputWriter orchestrates per-context output: buffers one part at a time and
  rotates to {contextKey}-{n}.{ext} when maxItems/maxBytes (format default merged
  with Feed.formatConfig['split']) is exceeded, reusing the same writer/config
  (CSV re-emits its header per part). Single-part output stays byte-identical to
  before (un-suffixed path).
- SplitManifest strategy (registry): none (self-describing parts) / supplemental
  (google_rss -> a <manifest> naming the parts at the canonical path).
- Optional gzip (Feed.formatConfig['gzip']) via a bounded zlib.deflate filter,
  .gz extension, per part.
- GenerationResult carries all written paths (parts + manifest) for delivery.

+7 test files (594 total); all gates (incl. PHP 8.1) green.
Five thinner feed types (SS8.2-8.6) with their DataSources + resolvers, proving
the engine is resource-agnostic:
- order [CHANNEL] (completed orders), customer [], taxon [LOCALE], product_review
  [CHANNEL,LOCALE] (accepted), promotion [CHANNEL] (non-archived).
- Resolver names namespaced where they collide in the global resolver registry
  (promotion_*, review_created_at).
- Acceptance: an Order CSV export and a Taxon feed generate via the real
  FeedGenerator + Doctrine data sources against the database.

+116 tests (710 total); 7 feed types registered; all gates (incl. PHP 8.1) green.
- DeliveryTarget (child of Feed): transport, transportConfig, pathTemplate
  (placeholders), match {channel?,locale?,currency?}. Pluggable
  DeliveryTransport registry: local (always) + ftp/sftp/s3 gated on class_exists
  (Flysystem adapters are the integrator's optional install; listed under
  composer suggest, not required).
- DeliveryMatcher (wildcard per dimension, empty=all) + PathTemplateRenderer.
- DeliveryService pushes the whole file set (parts + manifest) to every matching
  target, best-effort + isolated (per-target/per-file try-catch, never throws),
  recording {target,path,status,error?} on FeedContextResult.deliveries.
- Hooked into finalize: MoveGeneratedFeedSubscriber delivers a context only after
  it is promoted to canonical AND published; a gate-blocked context is never
  delivered. FeedContextResult carries channel/locale/currency codes + paths.
- DeliveryTargetType admin form + a deliveryTargets collection on the feed form.

+24 tests (734 total); all gates (incl. PHP 8.1) green.
setono:feed:process already processes every enabled feed by default; add an
explicit --all flag (the documented cron form) and a Scheduling section in the
README with a crontab example. Fix the README's stale command name
(setono:sylius-feed:process -> setono:feed:process).
For a large single-source plain feed, GenerateFeedContextHandler partitions the
context into id-ranges (ChunkPartitioner) and fans out GenerateFeedChunk messages
instead of generating inline. Each chunk renders a body-only partial
({contextKey}.chunk-{i}); a per-context FeedChunk barrier table tracks completion
with an atomic idempotent counter, so exactly one chunk triggers
FinalizeFeedContext, which concatenates the ordered partials (preamble + bodies +
epilogue) via OutputWriter::finalizeFromPartials.

- Byte-identical to the inline single-chunk path (asserted at unit + functional
  level over the same catalog).
- Chunk-level resumability: a failed chunk is retried by Messenger without failing
  the feed; the finalize claim (atomic DELETE of the barrier rows) fires exactly
  once; a stray re-dispatched finalize is a no-op.
- Shared FeedContextFinalizer (record -> gate -> counter -> complete) used by both
  the inline and fan-out paths so they finalize identically.
- Fan-out engages only for single-source, non-split, non-gzip feeds (threshold
  formatConfig['chunk']['size'], default 10k); split/gzip stay on the inline path.
- CsvWriter BOM moved to writePreamble so body-only partials omit it.

+4 test files (747 total); all gates (incl. PHP 8.1 + byte-identical + resume) green.
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.65217% with 198 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.17%. Comparing base (ab10d4f) to head (0f3a961).

Files with missing lines Patch % Lines
src/DataSource/ProductReviewDataSource.php 7.40% 25 Missing ⚠️
src/DataSource/PromotionDataSource.php 7.69% 24 Missing ⚠️
src/DataSource/CustomerDataSource.php 8.00% 23 Missing ⚠️
src/DataSource/OrderDataSource.php 57.57% 14 Missing ⚠️
src/DataSource/TaxonDataSource.php 46.15% 14 Missing ⚠️
src/Delivery/DeliveryService.php 84.61% 10 Missing ⚠️
src/Model/FeedChunk.php 66.66% 10 Missing ⚠️
src/Delivery/S3DeliveryTransport.php 43.75% 9 Missing ⚠️
src/Form/Type/DeliveryTargetType.php 84.31% 8 Missing ⚠️
src/DataSource/ProductDataSource.php 0.00% 7 Missing ⚠️
... and 19 more
Additional details and impacted files
@@             Coverage Diff              @@
##                1.x     #117      +/-   ##
============================================
- Coverage     95.25%   92.17%   -3.09%     
- Complexity     1231     1727     +496     
============================================
  Files           161      228      +67     
  Lines          3057     4382    +1325     
============================================
+ Hits           2912     4039    +1127     
- Misses          145      343     +198     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

loevgaard added 2 commits July 2, 2026 20:01
On --prefer-lowest, league/flysystem-local resolves to 3.15 (its LocalFilesystemAdapter
implements League\Flysystem\ChecksumProvider, added in flysystem 3.14) while
league/flysystem could still resolve to 3.0, whose interface set lacks
ChecksumProvider -> 'Interface ChecksumProvider not found' broke kernel boot +
PHPStan on every lowest-deps CI leg once the delivery transports (Chunk 3) began
instantiating a Filesystem. Raising league/flysystem + -local to ^3.15 keeps the
pair matched; flysystem-bundle stays ^2.4 || ^3.0 (Sylius pins ^2.4). Highest
(flysystem 3.35) unchanged; 747 tests green.
…sv typing)

Two lowest-deps issues the CI matrix surfaced:
1. symfony/translation's PhpAstExtractor (booted by PHPStan's Symfony container
   loader) is version-coupled to nikic/php-parser: <=6.4.2 call ParserFactory::
   create() (php-parser 4), >=6.4.3 call createForHostVersion() (php-parser 5).
   --prefer-lowest resolved a mismatched pair -> 'Call to undefined method'.
   Pin require-dev to the coherent forward combo: nikic/php-parser ^5.0 +
   symfony/translation ^6.4.3.
2. The fan-out byte-identity tests did array_map(fn(array $row)...) over
   league/csv records, which are typed mixed on the 9.6 floor (param too narrow)
   yet array on newer (guard redundant). Extract a static rowId(mixed): mixed
   helper -- a hard type boundary clean on both.

Highest unchanged (php-parser 5.7 + translation 6.4.42); 747 tests + PHPStan max
(incl. PHP 8.1) green on both highest and the lowest resolve.
@loevgaard
loevgaard merged commit 930d362 into 1.x Jul 2, 2026
20 of 30 checks passed
@loevgaard
loevgaard deleted the feature/m7-delivery-scale-feedtypes branch July 2, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant