Introduce a lightweight collection class
- Dominant language
- PHP
- Stars
- 3.4k
- Forks
- 1.2k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 433
Description
# Technical TODO
## Problem Description
The `Collection` base class inherits from `Struct`, pulling in API serialization, plugin extension storage, reflection-based hydration, and roughly 20 public methods that internal collections never use. The codebase has no intermediate abstraction between raw typed arrays (`list`) and the full `Collection` class, forcing a binary choice between untyped array boilerplate and the complete Struct machinery. This results in repeated manual iteration patterns across consumers and unnecessary runtime overhead for purely internal data structures.
## Current State
Four classes (`KernelPluginCollection`, `StockDataCollection`, `TransportCollection`, `BufferedFlowQueue`) already implement lightweight collection semantics without extending `Collection`, but each reinvents the API independently with inconsistent method signatures.
Among the `@internal` Collection subclasses, none reference `getApiAlias()`, `getExtension()`, `addExtension()`, `getVars()`, or the Struct-level `jsonSerialize()`. Their method bodies are limited to `add()`, `set()`, `filter()`, `filterInstance()`, and iteration.
The `array_values(array_filter(...))` combination appears frequently across the codebase as a workaround to restore sequential indexing after filtering, a problem that a collection's `filter()` method returning `static` would eliminate entirely.
## Migration Candidates
### Migration Candidates
- `list` in the DAL Write Pipeline:
Over 20 subscribers iterate the commands from `PreWriteValidationEvent` and repeat the same pattern: loop through all commands, skip those that are not the right command subclass (e.g. not `InsertCommand` or `UpdateCommand`), then skip those that do not match the target entity name. A `WriteCommandCollection` with `filterByEntity()` and `filterInstance()` would replace this two-level guard clause with a single call. Purely internal, never API-serialized.
- `EntityWriteEvent::getCommandsForEntity()` return value:
This method filters the internal command array by entity name and wraps the result in `array_values()` to restore sequential indexing. A collection's `filter()` method handles re-indexing inherently, making the `array_values()` call unnecessary.
- `list` in WebhookManager:
The WebhookManager applies multiple filter and extract operations on webhook lists: extracting ACL role IDs via map-then-filter, and filtering webhooks by live version status. Each operation chains `array_values(array_filter(...))` or `array_values(array_filter(array_map(...)))` to maintain list semantics. A webhook collection would reduce these to `filter()` and `map()` calls. Internal only; `Webhook` is a DTO, not API-facing.
- `DatadogPayloadCollection`:
The class body is completely empty. It exists only to type the collection element. Inherits the full Struct hierarchy with zero usage. Drop-in migration candidate with no behavioral change.
- `RequirementsCheckCollection`:
Uses only `filterInstance()`, `filter()`, and `first()` to partition requirement checks by type (path checks vs. system checks) and detect error states. None of the Struct capabilities are referenced. All methods would work identically on a lightweight base.
- `ErrorCollection` (App Validation):
Uses only `add()`, `set()`, and iteration. Elements are keyed by their message key. All referenced methods would be available on a lightweight base class.
- `Criteria` internal list properties:
The `Criteria` class manages four list-typed properties for sorting, post-filters, score queries, and field groupings as raw arrays. These are embedded properties in one of the most central DAL classes rather than standalone types, which makes this a lower-priority candidate compared to the others.
## Desgin Suggestion
### Implementation Design
Introduce an abstract base class that implements `\IteratorAggregate` and `\Countable` without extending `Struct`. It sits beside `Collection` in the type hierarchy without replacing it.
The class provides the subset of `Collection` methods that internal code actually uses: `add()`, `set()`, `get()`, `has()`, `remove()`, `filter()`, `filterInstance()`, `map()`, `first()`, `count()`, `isEmpty()`, and `getElements()`. Its `filter()` returns a new instance of `static` with sequential keys, eliminating the need for `array_values()` wrappers. An optional `getExpectedClass()` method enables runtime type checking for subclasses that need it.
Excluded from Struct: extension storage (10 methods), reflection-based hydration (`assign()`, `assignRecursive()`), recursive deep cloning, `createFrom()`, `getApiAlias()`, `getVars()`, and the `\JsonSerializable`, `ExtendableInterface`, `AssignArrayInterface` interfaces.
Domain-specific subclasses extend this base and add typed convenience methods that encode the filtering logic currently duplicated across consumers.
`Collection` and `EntityCollection` remain unchanged. All API-facing, DAL-managed, and public collection classes continue to use the existing hierarchy.
Contributor guide
Assessment
This issue has not been assessed yet.