Interface + events release. Consumer code can now type-hint against SheetsServiceInterface (the bundle wires the autowire alias next to the existing concrete one), and subscribers can listen for SheetsWriteEvent to react to writes. Also bundles a round of correctness fixes on the profiler tracer and the in-memory fake.
Added
SheetsServiceInterfacecaptures the consumer-facing read/write/discovery/metadata contract.SheetsService,TraceableSheetsService,CachedSheetsService, andInMemorySheetsServiceall implement it. The bundle registersSheetsServiceInterface $<name>autowire aliases alongside the existingSheetsService $<name>aliases, andSheetsRegistry::service()now returns the interface. Escape hatches (client(),driveService()) intentionally stay on the concreteSheetsServiceonly.SheetsWriteEvent(PSR-14) dispatched after every successfulappend,update,clear,addSheet, anddeleteSheet. Subscribe with#[AsEventListener(event: SheetsWriteEvent::class)]for cache invalidation, audit logging, or sync workflows. The event exposesoperation,spreadsheetId,sheetName,range, androwCount. Read events are deliberately not dispatched — they would fire on every row of streaming reads.- Optional
?EventDispatcherInterfaceconstructor argument onSheetsService(last position),TraceableSheetsService,CachedSheetsService, andInMemorySheetsService. Bundle wires it viaservice(EventDispatcherInterface::class)->nullOnInvalid()so existing apps withoutsymfony/event-dispatcherstill boot.
Fixed
TraceableSheetsService::readAssocIterableno longer drops the trace on earlybreak. The previous try/catch only recorded on full exhaustion or exception — a caller doingforeach (...) { if (...) break; }produced zero profiler entries. Switched to try/finally so the trace fires unconditionally.TraceableSheetsService::findSheetNameByIdis now wrapped. The parent's comment anticipated this override ("Call the private helper directly so a TraceableSheetsService subclass doesn't double-record"), but the wrapper was missing —findSheetNameByIdhit the parent silently with zero trace recorded.captureOriginbacktrace depth raised from 12 to 25 frames. In a real Symfony app, bundle-internal frames + the kernel + middleware can consume the first dozen slots, socaptureOriginsilently returnednullfor the call-site in most production paths.InMemorySheetsService::append()now enforces the homogeneous-row shape check that the realSheetsServiceruns. Tests using the fake no longer silently accept mixed positional/assoc rows or assoc rows with divergent keys — they throwMixedRowShapeExceptionmatching production.PeekCommandguardsrange(0, -1)so an empty leading row no longer emits a phantomAcolumn header. PHP returns[0]forrange(0, -1), which produced a misleading single-column table for empty range fetches.
Changed
TraceableSheetsService::trace()collapses its duplicated success/catch bodies into a singlefinallythat callsrecord()once with$error ?? null. Behaviour identical, half the code.SheetsCollector::getTotalDurationMs()is now lazy — it sumscalls[*].duration_mson demand instead of maintaining a running scalar. The running scalar was vulnerable to type drift after Symfony's profiler serialisation, which the previous defensiveis_float/is_intcast hinted at.SheetsRegistry::service()andmetadata()share a singlenotFoundMessage()helper instead of repeating thesprintftemplate.
Full changelog: https://github.com/gulaandrij/google-sheets-bundle/blob/v1.3.0/CHANGELOG.md