Skip to content

Use embed=body mode for stream feed iterators to support $all and system streams#77

Merged
dbellettini merged 5 commits into
mainfrom
copilot/support-all-and-system-streams
Jul 2, 2026
Merged

Use embed=body mode for stream feed iterators to support $all and system streams#77
dbellettini merged 5 commits into
mainfrom
copilot/support-all-and-system-streams

Conversation

Copilot AI commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

StreamFeedIterator used readEventBatch() to fetch events individually by URL. System events (e.g. $ProjectionCreated) return null content at their individual URLs, causing silent skips. The $all stream was similarly affected.

The fix leverages EntryEmbedMode::BODY when opening feeds, embedding event data directly in feed entries — eliminating the N+1 request pattern and working for all stream types.

Changes

  • StreamFeedIterator — opens feeds with EntryEmbedMode::BODY, reads embedded events from entries instead of calling readEventBatch()
  • Entry — added ?Event $embeddedEvent constructor param and getEmbeddedEvent() accessor
  • EntryDenormalizer — accepts EventDenormalizer, extracts embedded event during entry denormalization
  • EventDenormalizer — relaxed supportsDenormalization (no longer requires data key), handles null data (system events) and JSON-string data (embed=body format) via new decodeField() helper
  • EventStoreFactory / SerializerFactory — adjusted instantiation order since EntryDenormalizer now depends on EventDenormalizer

Before/After

// Before: silently skips system events and $all entries
$iterator = $eventStore->forwardStreamFeedIterator(rawurlencode('$all'));
iterator_count($iterator); // Undercounts — many events skipped

// After: all events including system events are yielded
$iterator = $eventStore->forwardStreamFeedIterator(rawurlencode('$all'));
foreach ($iterator as $key => $entryWithEvent) {
    $entryWithEvent->getEvent()->getType(); // e.g. "$ProjectionCreated"
    $entryWithEvent->getEvent()->getData(); // [] for system events without data
}

- Modify StreamFeedIterator to open feeds with EntryEmbedMode::BODY
  instead of fetching events individually via readEventBatch()
- Add getEmbeddedEvent() to Entry class for accessing embedded event data
- Update EntryDenormalizer to extract Event from embedded entry data
- Update EventDenormalizer to handle null data (system events) and
  JSON string data (embed=body format)
- Remove ClientExceptionInterface from iterator throws since
  readEventBatch is no longer called

This resolves the issue where iterating over $all and system streams
silently skipped events that couldn't be deserialized from individual
event URLs. Using embed=body works for all stream types including
$all and system streams with events that have no data body.

Closes #71
Copilot AI changed the title [WIP] Fix $all and system streams deserialization in StreamFeedIterator Use embed=body mode for stream feed iterators to support $all and system streams Jun 27, 2026
Copilot AI requested a review from dbellettini June 27, 2026 18:54
- EventDenormalizer: read both metadata and metaData keys so embedded
  feed entries (which use capital-D) no longer silently drop metadata
- EventDenormalizer: map empty decoded metadata ([] from {}) back to null
  to restore null-for-empty semantics required by integration tests
- EntryDenormalizer: guard embedded-event extraction behind BODY mode so
  RICH feeds no longer fabricate Events with data=[]
- StreamFeedDenormalizer: forward $format and $context when calling
  entryDenormalizer->denormalize so the embedMode context reaches
  EntryDenormalizer
- StreamFeedIteratorTest: fix system-event test to account for forward
  iterator's array_reverse ordering; use array_values + integer indexing
  to avoid EntryWithEvent|false PHPStan error from reset(); remove
  redundant assertContainsOnlyInstancesOf; use array_key_exists in
  createStreamFeedWithEmbeddedEvents so explicit data:null is preserved
  rather than coalesced to the default string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pprMw2pSRdHxxLBUpE4zb
@dbellettini dbellettini marked this pull request as ready for review July 2, 2026 21:02
dbellettini and others added 2 commits July 2, 2026 23:40
EventDenormalizer::decodeField() now normalizes empty arrays to null,
restoring the pre-existing contract that events without metadata expose
null from getMetadata() (broken by the embed=body refactor).

Fix the null-data system event test: feed entries are provided
newest-first to match the forward iterator's array_reverse, and the
mock helper honours an explicit null data value instead of substituting
the default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PHPUnit 13 warns when a mock object has no configured expectations and
suggests a stub instead. Convert value-provider collaborators from
createMock() to createStub() across the unit tests, and where a shared
mock property was used with expects() only in some tests, build a local
mock in those tests and keep the shared property as a stub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dbellettini dbellettini merged commit 2831b43 into main Jul 2, 2026
4 checks passed
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.

3 participants