shopware / shopware/shopware

Introduce a lightweight collection class

Open
#14,932 0 comments 0 reactions 0 assignees View on GitHub
component/core domain/framework priority/low
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.