robertgdev / ds-vector-polyfill
Ds\Vector wrapper with native array fallback
Requires
- php: >=8.3
Requires (Dev)
- pestphp/pest: ^4.0
- phpunit/phpunit: ^12.5
- thecodingmachine/phpstan-safe-rule: ^1.4
Suggests
- ext-ds: For better performance with Ds\Vector
README
A Ds\Vector wrapper with native PHP array fallback — write against one API, run anywhere.
Use Case
Ds\Vector from the PHP Data Structures extension is a fast, memory-efficient
sequential container. But ext-ds isn't always available: shared hosts, CI
runners, and minimal Docker images often omit it.
VectorPolyfill provides the full Ds\Vector API (33 methods) through a
single interface backed by Ds\Vector when the extension is loaded, or a plain
PHP array when it isn't. Your code doesn't change — the backend is selected
once at factory time.
Tradeoffs: Vector vs Array
| Property | Ds\Vector backend |
Array fallback |
|---|---|---|
| get / set / push / pop | O(1) — direct buffer index | O(1) |
| shift / unshift / insert / remove | O(n) — C-level memmove | O(n) — PHP array_splice |
| Memory for large lists | ~25% less than PHP arrays | Standard PHP array overhead |
| Type safety | None (same as PHP) | None |
| sort / sorted | Native Ds implementation | sort() / usort() |
| foreach iteration | Native Traversable | ArrayIterator |
allocate() / capacity() |
Actual buffer hint (useful) | capacity() = count(); allocate() is a no-op |
| Installation requirement | pecl install ds |
Zero dependencies |
Benchmark: 1 million random ints
| Operation | Ds\Vector | Array |
|---|---|---|
| Memory (series ready) | 15.34 MB | 0.66 MB |
| Memory (peak total) | 30.68 MB | 40.66 MB |
| append | 0.002 ms | 0.002 ms |
| random access | 0.006 ms | 0.005 ms |
| first | 0.001 ms | 0.001 ms |
| last | 0.001 ms | 0.002 ms |
| remove (end) | 0.002 ms | 10.171 ms |
| sort | 216.958 ms | 231.515 ms |
| sum | 2.642 ms | 1.910 ms |
| filter (>50%) | 32.620 ms | 55.990 ms |
| map (*2) | 32.438 ms | 30.559 ms |
The array series ready appears small because the 1M-element $values array is
allocated outside the measurement window and passed to createForced, which
stores it directly — no copy. The Vector path is built element-by-element via
append and reports 15.34 MB. Peak memory tells the real story: Ds\Vector
uses ~25% less memory (30.68 MB vs 40.66 MB).
The most dramatic difference is remove (end): removing the last element from
a 1M-item Vector is O(1) (just decrements the internal size counter). On a PHP
array, removing the 999,999th element requires rebuilding the internal hash
table from scratch — over 10 ms vs 0.002 ms. Both backends are comparable for
read operations, sort, and map. Vector pulls ahead on filter thanks to
its more efficient iterator.
If you control the deployment environment, install ext-ds for the performance
and memory wins. If you distribute code to others or run on constrained
environments, the array fallback ensures your code works identically.
Installation
composer require robertgdev/ds-vector-polyfill
For best performance, also install the Ds extension:
pecl install ds
Quick Start
use RobertGDev\DsVectorPolyfill\VectorPolyfill; $series = VectorPolyfill::create(['a', 'b', 'c']); $series->append('d'); $series->first(); // 'a' $series->last(); // 'd' $series->get(1); // 'b' $series->remove(2); // 'c' $series->contains('b'); // true // Immutable operations return new instances $filtered = $series->filter(fn ($v) => $v !== 'b'); $mapped = $series->map(fn ($v) => strtoupper($v)); $merged = $series->merge(['x', 'y']); // Iteration foreach ($series as $value) { echo $value; } // Countable count($series); // 3 // JSON json_encode($series); // ["a","b","d"]
Forcing a Backend
Tests (or edge cases) can force a specific backend:
use RobertGDev\DsVectorPolyfill\VectorPolyfill; $vectorSeries = VectorPolyfill::createForced([1, 2, 3], VectorPolyfill::BACKEND_VECTOR); $arraySeries = VectorPolyfill::createForced([1, 2, 3], VectorPolyfill::BACKEND_ARRAY);
VectorPolyfill::create() auto-detects the best available backend.
API Reference
All methods match Ds\Vector semantics exactly. Return types, exception types,
and behavior are identical across both backends.
Factory
| Method | Signature |
|---|---|
create() |
VectorPolyfill::create(iterable $values = []): self |
createForced() |
VectorPolyfill::createForced(iterable $values, string $backend): self |
Capacity
| Method | Returns | Description |
|---|---|---|
allocate(int $capacity): void |
— | Pre-allocate memory (no-op on array) |
capacity(): int |
int |
Current capacity (count() on array) |
Mutation (in-place)
| Method | Returns | Description |
|---|---|---|
append(mixed $value): void |
— | Add value to end |
set(int $index, mixed $value): void |
— | Overwrite value at index |
insert(int $index, mixed ...$values): void |
— | Insert values at index |
remove(int $index): mixed |
Removed value | Remove and return by index |
pop(): mixed |
Last value | Remove and return last |
shift(): mixed |
First value | Remove and return first |
unshift(mixed ...$values): void |
— | Prepend values |
push(mixed ...$values): void |
— | Append values (see append) |
apply(callable $callback): void |
— | Update all values in place |
clear(): void |
— | Remove all values |
reverse(): void |
— | Reverse in place |
rotate(int $rotations): void |
— | Rotate by N positions |
sort(?callable $comparator = null): void |
— | Sort in place |
Read
| Method | Returns | Description |
|---|---|---|
get(int $index): mixed |
Value | Throws OutOfRangeException if OOB |
first(): mixed |
First value or null |
Returns null on empty |
last(): mixed |
Last value or null |
Returns null on empty |
count(): int |
Count | Also implements Countable |
isEmpty(): bool |
bool |
True when empty |
contains(mixed ...$values): bool |
bool |
True if all values present |
find(mixed $value): mixed |
Index or null |
Returns null when not found |
toArray(): array |
Plain PHP array | Returns a copy |
sum(): int|float |
Sum | Returns 0 on empty |
join(?string $glue = null): string |
Joined string | No glue = concatenation |
capacity(): int |
Capacity |
Immutable (return new instance)
All of these return a new VectorPolyfill; the original is unchanged.
| Method | Returns | Description |
|---|---|---|
copy(): self |
Shallow copy | Independent clone |
filter(?callable $callback = null): self |
Filtered series | Null callback removes falsy |
map(callable $callback): self |
Mapped series | |
slice(int $index, ?int $length = null): self |
Slice | |
sorted(?callable $comparator = null): self |
Sorted copy | |
reversed(): self |
Reversed copy | |
merge(iterable $values): self |
Merged series | Original unchanged |
Reduction
| Method | Signature |
|---|---|
reduce(callable $callback, mixed $initial = null): mixed |
Serialization
| Method | Description |
|---|---|
jsonSerialize(): array |
Implements JsonSerializable — use with json_encode() |
Iteration
Implements IteratorAggregate — supports foreach ($series as $value).
Interfaces implemented
VectorPolyfillInterface<TValue>— the full API contractIteratorAggregate— foreach iterationCountable—count()JsonSerializable—json_encode()
Exception Policy
| Exception | Thrown by |
|---|---|
OutOfRangeException |
get(), set(), remove(), insert() with invalid index |
RuntimeException |
pop(), shift() on empty (array backend) |
UnderflowException |
pop(), shift() on empty (vector backend; wrapped to match) |
PHP Version
Requires PHP >= 8.3.
License
MIT