Skip to content

M5 — Channel breadth + CSV + multi-source + LookupTable enrichment - #115

Merged
loevgaard merged 9 commits into
1.xfrom
feature/m5-channels-csv-lookup
Jul 2, 2026
Merged

M5 — Channel breadth + CSV + multi-source + LookupTable enrichment#115
loevgaard merged 9 commits into
1.xfrom
feature/m5-channels-csv-lookup

Conversation

@loevgaard

Copy link
Copy Markdown
Member

M5 — Channel breadth + CSV + multi-source + external enrichment

Broadens the engine to more destinations and adds the LookupTable enrichment resource. Four
chunks (7 commits); 503 tests; the admin LookupTable flow is browser-verified.

CSV writer + format presets

  • CsvWriter (league/csv, the csv writer family): a header row that is the union of the
    feed's output fields
    (computed by the generator, injected into the config) then one row per
    item keyed by header — so heterogeneous multi-source rows line up; missing → empty cell, list →
    joined, optional UTF-8 BOM. csv format preset.
  • generic_xml + partner_ads XmlWriter format presets — new destinations are format
    presets, no new writer class.

Channel mapping presets

Meta (csv, space-separated availability via value_map), Bing + Pinterest (google_rss),
TikTok (csv, sku_id/product_page_url), Partner-ads (partner_ads, Danish element names). They
show up in the admin target picker. Acceptance: a valid Meta CSV and a valid Partner-ads XML are
produced from the same catalog purely by choosing the preset — no code changes.

product FeedType + multi-source

  • Shared resolvers (id, title, description, link, main_image, additional_images,
    is_configurable) now support both ProductVariant and Product (ProductAwareTrait);
    variant behaviour is unchanged. New product-level resolvers: variant_count, from_price,
    product_availability.
  • ProductDataSource + ProductFeedType. Multi-source proven end to end: one CSV feed with a
    product source and a product_variant source emits both product-level and variant-level rows
    under a union header.

External enrichment — the LookupTable resource (§10.1)

  • LookupTable Sylius resource (entity/mapping/repository/admin grid + CRUD + Refresh
    action + menu). The rows property maps to a lookup_rows json column (rows is a MySQL
    reserved word).
  • Pluggable LookupSourceInterface with built-in csv (uploaded file) and url
    (downloads CSV/TSV — covers a published Google Sheet …/export?format=csv).
  • LookupTableRefresher imports rows keyed by keyColumn, stamping refreshedAt; on any
    failure it keeps the last-good rows and logs a warning — a broken source never blanks the
    serving data.
  • DatabaseLookup (the production LookupInterface) resolves lookup(table, key, column) in
    expressions/twig against the tables (loaded once + cached per run).
  • lookup:{code}:{column} source references resolve in the generator via the table's
    joinField — so concat(["value", "lookup:badges:badge"]) (the exact §10.1 case) and GTIN
    backfill (g:gtin ← lookup:…) work end to end.
  • Browser-verified: create a lookup table → Refresh fetches a CSV URL and imports the keyed
    rows (refreshedAt stamped) → the flash shows.

CI note

Verified locally against the updated PHPStan/Rector/ECS (CI runs composer update), so the
substantive gate (Unit / Integration / Static Code Analysis / Coding Standards) should be green.
The Backwards Compatibility Check carries intentional breaks (the FieldMappingEvaluator
constructor gained a lookup dependency; Configuration gained the resource) — left visible per the
rewrite policy — and Dependency Analysis + Mutation remain the standing rewrite reds.

loevgaard added 7 commits July 2, 2026 12:56
- CsvWriter (league/csv, 'csv' writer family): streams a header row (union of the
  feed's output fields) then one row per item, cells keyed by header column so
  heterogeneous multi-source rows line up; missing field -> empty cell, list ->
  joined. Optional UTF-8 BOM written directly (league/csv only BOMs its own
  output methods, not an external stream). Cross-version league/csv 9.0-9.28 API.
- CsvWriterConfig (delimiter/enclosure/header/bom) + withHeader().
- csv FormatInterface preset (XmlWriter unchanged).
- FeedGenerator computes the union header from all sources' mappings and injects
  it for CsvWriterConfig before opening the writer.

+6 tests (442 total); PHPStan max + ECS + Rector + container lint green.
- generic_xml + partner_ads XmlWriter format presets (no new writer class):
  partner_ads roots produkter/produkt.
- Mapping presets targeting product_variant: Meta (csv, space-separated
  availability via value_map), Bing + Pinterest (google_rss, = Google), TikTok
  (csv, sku_id/product_page_url), Partner-ads (partner_ads, Danish element names
  nypris/glpris/VareURL/BilledURL/koen). Registered + translated; they appear in
  the admin target picker.
- Acceptance: a valid Meta CSV and a valid Partner-ads XML are produced from the
  same catalog purely by choosing the preset -- no code changes.

+27 tests (469 total); PHPStan max + ECS + container/yaml lint green.
- ProductAwareTrait: shared resolvers (id, title, description, link, main_image,
  additional_images, is_configurable) now support both ProductVariant and Product
  entities, deriving the product from either (variant -> getProduct()); variant
  behaviour is unchanged. id resolves to the variant/product code respectively.
- New product-level resolvers: variant_count (both entities), from_price
  (cheapest enabled variant price), product_availability (in_stock if any variant
  is). Variant-only resolvers (item_group_id, channel_price, availability, ...)
  untouched.
- ProductDataSource (iterate enabled, channel-assigned Product) + ProductFeedType
  ('product', scope channel x locale x currency); wired + translated.
- Multi-source proven end to end: one CSV feed with a product source and a
  product_variant source emits both product-level and variant-level rows under a
  union header.

+24 tests (493 total); PHPStan max + ECS + Rector + container lint green.
- LookupTable resource entity/interface + Doctrine mapping
  (setono_sylius_feed__lookup_table; the 'rows' property maps to a lookup_rows
  json column since rows is a MySQL reserved word) + repository; registered as a
  Sylius resource in Configuration.
- DatabaseLookup implements LookupInterface against the LookupTable repository
  (each table loaded once and cached per instance); LookupInterface now aliases
  to DatabaseLookup in production. InMemoryLookup remains for tests/programmatic use.
- ScriptingTest now persists a LookupTable and proves expression + sandboxed twig
  lookup() resolves through the database.

493 tests green; PHPStan max + ECS + Rector + container lint green.
(sources/refresh, lookup: source-ref, and the admin UI follow.)
- CsvLookupSource ('csv'): parses an uploaded CSV from the feed filesystem by
  header. UrlLookupSource ('url'): downloads CSV/TSV from a URL (covers a
  published Google Sheet export). LookupRow normalizes records to column=>scalar.
- LookupTableRefresher: imports rows via the source registered for the table's
  sourceType, keyed by keyColumn; stamps refreshedAt on success. On any failure
  it keeps the last-good rows + refreshedAt and logs a warning -- a broken source
  never blanks the serving data.
- Wired: both sources tagged; refresher + interface alias.

+7 tests (500 total); PHPStan max + ECS + Rector + container lint green.
- LookupReferenceResolver: parses a lookup:{code}:{column} reference, reads the
  table's joinField value off the item (via the source resolvers), and returns
  the requested column. Wired into FieldMappingEvaluator's source resolver, so a
  mapping like concat(['value','lookup:badges:badge']) enriches each row per item
  (the exact 10.1 case) and GTIN backfill (g:gtin <- lookup:...) works.

+1 test (501 total); PHPStan max + ECS + Rector + container lint green.
- Resource CRUD grid (code/sourceType/refreshedAt) + routes; LookupTableType form
  (code/name/sourceType/location/keyColumn/joinField/refreshPolicy) that packs the
  single location into sourceConfig as path (csv) or url (url) and unpacks it on
  edit; RefreshLookupTableAction (re-import + flash); a 'Lookup tables' admin menu
  item; translations (incl. the flashes-domain refresh flash + page titles).
- Configuration uses LookupTableType as the resource form.

Browser-verified: grid loads, create persists, Refresh fetches a CSV URL and
imports the keyed rows (refreshedAt stamped), keep-last-good on failure.

+2 tests (503 total); PHPStan max + ECS + Rector + container/yaml lint green.
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.45865% with 88 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.32%. Comparing base (da728d9) to head (d22303b).

Files with missing lines Patch % Lines
src/Form/Type/LookupTableType.php 0.00% 56 Missing ⚠️
src/Model/LookupTable.php 66.66% 14 Missing ⚠️
src/Lookup/LookupReferenceResolver.php 75.00% 5 Missing ⚠️
src/Lookup/LookupTableRefresher.php 80.00% 5 Missing ⚠️
src/FeedType/ProductFeedType.php 86.36% 3 Missing ⚠️
src/Lookup/LookupRow.php 77.77% 2 Missing ⚠️
src/Lookup/DatabaseLookup.php 88.88% 1 Missing ⚠️
...ueResolver/Product/ProductAvailabilityResolver.php 94.73% 1 Missing ⚠️
src/Writer/CsvWriter.php 96.96% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                1.x     #115      +/-   ##
============================================
- Coverage     98.48%   95.32%   -3.17%     
- Complexity      822      998     +176     
============================================
  Files           108      134      +26     
  Lines          1918     2417     +499     
============================================
+ Hits           1889     2304     +415     
- Misses           29      113      +84     

☔ 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 15:07
- league/csv 9.0-9.5 ignore setDelimiter() after createFromStream() (insertOne
  read the delimiter from the document, unsynced) -> the custom-delimiter CSV
  test failed on Deps:lowest. Raise the constraint to ^9.6 (verified: 9.6 honours
  the delimiter and supports PHP 8.1).
- league/csv <9.28 types getRecords() loosely (mixed), tripping PHPStan on
  lowest; LookupRow::normalize now accepts mixed + guards is_iterable/is_scalar,
  and the generator tests read records through a version-robust
  array_filter(is_array(...)) helper. Verified clean on both 9.6 and 9.28.

503 tests; PHPStan max + ECS + Rector + validate/normalize green.
On PHP 8.1/8.2 PHPStan types iterator_to_array's param as Traversable, but
league/csv getRecords() and the lookup sources' fetch() are declared iterable,
so Static Code Analysis failed on those legs. Spread ([...iterable]) accepts any
Traversable and avoids the stub mismatch. Verified clean under PHPStan phpVersion
8.1 and league/csv 9.6 + 9.28.
@loevgaard
loevgaard merged commit f76b70b into 1.x Jul 2, 2026
20 of 30 checks passed
@loevgaard
loevgaard deleted the feature/m5-channels-csv-lookup branch July 2, 2026 13:17
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