What is in array?
- ordered sequence
- of values
- of the same type
- identified by their offset
PHP has associative arrays
- Arbitrary key-value index
- "Dictionary"/"Map"
- Numeric arrays via a hack
- No type guarantees (on value or key)
Arrays are a hack
| $a[] = 'A'; |
| $a[] = 'B'; |
| print_r($a); |
Array
(
[0] => A
[1] => B
)
unset($a[1]);
$a[] = 'C';
print_r($a);
Array
(
[0] => A
[2] => C
)
Arrays are a buggy hack
| for ($i = 0; $i < count($a); ++$i) { |
| print $a[$i]; |
| } |
Notice: Undefined offset: 1 in /in/fQ0UB on line 13
What's wrong with this picture?
| protected function expandArguments(&$query, &$args) { |
| foreach ($args as $key => $data) { |
| |
| $key_name = str_replace('[]', '__', $key); |
| $new_keys = []; |
| foreach ($data as $i => $value) { |
| $new_keys[$key_name . $i] = $value; |
| } |
| |
| |
| $query = str_replace($key, implode(', ', array_keys($new_keys)), $query); |
| |
| |
| unset($args[$key]); |
| $args += $new_keys; |
| } |
| } |
| protected function expandArguments(&$query, &$args) { |
| foreach ($args as $key => $data) { |
| |
| $key_name = str_replace('[]', '__', $key); |
| $new_keys = []; |
| foreach ($data as $i => $value) { |
| $new_keys[$key_name . $i] = $value; |
| } |
| |
| |
| $query = str_replace($key, implode(', ', array_keys($new_keys)), $query); |
| |
| |
| unset($args[$key]); |
| $args += $new_keys; |
| } |
| } |
Hey, look, an SQL injection!
Drupageddon, 2014
Stupid simple fix
| protected function expandArguments(&$query, &$args) { |
| foreach ($args as $key => $data) { |
| |
| $key_name = str_replace('[]', '__', $key); |
| $new_keys = []; |
| foreach (array_values($data) as $i => $value) { |
| $new_keys[$key_name . $i] = $value; |
| } |
| |
| |
| $query = str_replace($key, implode(', ', array_keys($new_keys)), $query); |
| |
| |
| unset($args[$key]); |
| $args += $new_keys; |
| } |
| } |
PHP uses arrays in place of purpose-built data structures.
That is almost never the right answer.
Lists/Sequences
\ArrayObject
class TypedArray extends \ArrayObject
A type-specific "array"
| class TypedArray extends \ArrayObject { |
| protected string $type; |
| |
| public static function forType(string $type): static { |
| $ret = new static(); |
| $ret->type = $type; |
| return $ret; |
| } |
| |
| protected function __construct(...$args) { |
| parent::__construct(...$args); |
| } |
| |
| public function offsetSet($index, $newval) { |
| if (! $newval instanceof $this->type) { |
| throw new \TypeError( |
| sprintf('Only values of type %s are supported', $this->type) |
| ); |
| } |
| parent::offsetSet($index, $newval); |
| } |
| } |
A type-specific "array"
| $a = TypedArray::forType(Point::class); |
| |
| $a[0] = new Point(2, 4); |
| $a[1] = new Point(6, 9); |
| $a['foobar'] = new Point(5, 2); |
| |
| foreach ($a as $point) { ... } |
| |
| $a['bad'] = new Carrot(); |
| $a['also_bad'] = 'a point'; |
Force a sequence through the API
| class TypedSequence implements \IteratorAggregate, \Countable { |
| protected string $type; |
| protected array $values = []; |
| |
| public static function forType(string $type): static {...} |
| protected function __construct() {} |
| |
| public function getIterator() { |
| return new ArrayIterator($this->values); |
| } |
| |
| public function count(): int { |
| return count($this->values); |
| } |
| |
| public function add($newval): static { |
| if (! $newval instanceof $this->type) { |
| throw new \TypeError(...); |
| } |
| $this->values[] = $newval; |
| return $this; |
| } |
| } |
But arrays pass by value, objects by reference!
No, objects pass by value. You've been lied to.
But they're modifiable!
Their handle passes by value, not the object.
OMGWTFBBQ?
Sigh.
Can't touch $this
| class ImmutableTypedSequence implements IteratorAggregate, Countable { |
| |
| |
| public function add($newval): static { |
| if (!$newval instanceof $this->type) { throw new \TypeError(...);} |
| |
| $new = clone($this); |
| $values = [...$this->values, $newval]; |
| $new->values = $values; |
| return $new; |
| } |
| |
| public function remove($val): static { |
| if ($key = array_search($val, $this->values, true)) { |
| $values = $this->values; |
| unset($values[$key]); |
| $new = clone($this); |
| $new->values = array_values($values); |
| return $new; |
| } |
| return $this; |
| } |
| } |
Type-specific
| class PointSequence implements IteratorAggregate, Countable { |
| |
| |
| public function add(Point $newval): static { |
| $new = clone($this); |
| $values = [...$this->values, $newval]; |
| $new->values = $values; |
| return $new; |
| } |
| |
| public function remove(Point $val): static { |
| if ($key = array_search($val, $this->values, true)) { |
| $values = $this->values; |
| unset($values[$key]); |
| $new = clone($this); |
| $new->values = array_values($values); |
| return $new; |
| } |
| return $this; |
| } |
| } |
Uniqueness ("Set")
| class Set extends ImmutableTypedSequence { |
| |
| public function has($val): bool { |
| return array_search($val, $this->values, true) !== false; |
| } |
| |
| public function add($newval): static { |
| return $this->has($newval) ? $this : parent::add($newval); |
| } |
| } |
Ordering
| class OrderedSet extends Set { |
| protected $compare; |
| |
| public static function forType(string $type, callable $compare = null): static { |
| $ret = new static(); |
| $ret->type = $type; |
| $ret->compare = $compare; |
| return $ret; |
| } |
| |
| public function getIterator() { |
| if ($this->compare) { |
| usort($this->values, $this->compare); |
| } |
| return new ArrayIterator($this->values); |
| } |
| } |
| $compare = fn(Point $a, Point $b): int => [$a->x, $a->y] <=> [$b->x, $b->y]; |
| |
| $s = OrderedSet::forType(Point::class, $compare) |
| ->add(new Point(3, 4)) |
| ->add(new Point(2, 5)) |
| ->add(new Point(2, 3)); |
| |
| foreach ($s as $point) { |
| var_dump($point); |
| } |
| Point Object |
| ( |
| [x] => 2 |
| [y] => 3 |
| ) |
| Point Object |
| ( |
| [x] => 2 |
| [y] => 5 |
| ) |
| Point Object |
| ( |
| [x] => 3 |
| [y] => 4 |
| ) |
Purpose-built ordering
| class PointSet implements IteratorAggregate { |
| protected array $values = []; |
| |
| public function has(Point $val): bool { |
| return array_search($val, $this->values, true) !== false; |
| } |
| |
| public function add(Point $newval): static { |
| $new = clone($this); |
| $new->values = [...$this->values, $newval]; |
| return $new; |
| } |
| |
| public function remove(Point $oldval): static { ... } |
| |
| public function compare(Point $, Point $b): int { |
| return [$a->x, $a->y] <=> [$b->x, $b->y]; |
| } |
| |
| public function getIterator() { |
| usort($this->values, [$this, 'compare']); |
| return new ArrayIterator($this->values); |
| } |
| } |
What have we gained?
- Type-safe lists
- Sequence-guaranteed lists
- Immutability guarantee
- Self-documenting API
- Encapsulation of functionality
Can't type hint on array
anymore
:-(
Do you care about the type of the objects?
ProductList implements \Traversable
Do you just want to be able to foreach()
iterable
iterable :: array|\Traversable
\Traversable :: \Iterator|\IteratorAggregate
\Generator implements \Iterator
Generators
- Lazy stream of values
- Lazy value creation
- Structure flattening
- Simpler code
A PSR-14 example
| interface ListenerProviderInterface { |
| |
| |
| |
| |
| |
| |
| |
| public function getListenersForEvent(object $event): iterable; |
| } |
A PSR-14 example
| class Dispatcher implements DispatcherInterface { |
| |
| |
| public function dispatch(object $event) { |
| |
| foreach ($this->provider->getListenersForEvent($event) as $listener) { |
| if ($event instanceof StoppableEventInterface |
| && $event->isPropagationStopped()) { |
| break; |
| } |
| $listener($event); |
| } |
| |
| return $event; |
| } |
| } |
Tukio (PSR-14 reference implementation)
| class CallbackProvider implements ListenerProviderInterface { |
| |
| |
| public function getListenersForEvent(object $event): iterable { |
| if (!$event instanceof CallbackEventInterface) { |
| return []; |
| } |
| $subject = $event->getSubject(); |
| |
| foreach ($this->callbacks as $type => $callbacks) { |
| if ($event instanceof $type) { |
| foreach ($callbacks as $callback) { |
| if (method_exists($subject, $callback)) { |
| yield [$subject, $callback]; |
| } |
| } |
| } |
| } |
| } |
| } |
Trivial to concatenate
| class AggregateProvider implements ListenerProviderInterface { |
| |
| protected $providers = []; |
| |
| public function getListenersForEvent(object $event): iterable { |
| |
| foreach ($this->providers as $provider) { |
| yield from $provider->getListenersForEvent($event); |
| } |
| } |
| |
| } |
From fig/event-dispatcher-util
But what about array_*
functions?
What about them?
array_*
functions
- Rarely used
- Internal implementation detail
- Easy to reimplement
The most common
| function iterable_map(iterable $list, callable $operation): iterable { |
| foreach ($list as $k => $v) { |
| yield $operation($k, $v); |
| } |
| } |
| |
| function iterable_filter(iterable $list, callable $filter): iterable { |
| foreach ($list as $k => $v) { |
| if ($filter($v)) { |
| yield $k => $v; |
| } |
| } |
| } |
Collection objects
| class Collection implements \IteratorAggregate { |
| protected $valuesGenerator; |
| |
| protected function __construct(){} |
| |
| public static function fromGenerator(callable $callback): static { |
| $new = new static(); |
| $new->valuesGenerator = $callback; |
| return $new; |
| } |
| |
| public static function fromIterable(iterable $values = []) { |
| return static::fromGenerator(function () use ($values) { |
| yield from $values; |
| }); |
| } |
| |
| public function getIterator(): iterable { |
| return ($this->valuesGenerator)(); |
| } |
| } |
Collection objects
| class Collection implements \IteratorAggregate { |
| |
| public function append(iterable ...$collections): static { |
| return static::fromGenerator(function() use ($collections) { |
| yield from ($this->valuesGenerator)(); |
| foreach ($collections as $col) { |
| yield from $col; |
| } |
| }); |
| } |
| |
| public function add(...$items): static { |
| return $this->append($items); |
| } |
| } |
Collection objects
| class Collection implements \IteratorAggregate { |
| |
| public function map(callable $fn): static { |
| return static::fromGenerator(function () use ($fn) { |
| foreach (($this->valuesGenerator)() as $key => $val) { |
| yield $key => $fn($val); |
| } |
| }); |
| } |
| |
| public function toArray(): array { |
| return iterator_to_array((function() { |
| foreach ($this as $value) { |
| yield $value; |
| } |
| })()); |
| } |
Collection objects
| $c = Collection::fromIterable([1, 2, 3]); |
| |
| $c = $c->add(4, 5, 6); |
| $c = $c->map(fn($v) => $v * 10); |
| |
| print_r($c->toArray()); |
| Array |
| ( |
| [0] => 10 |
| [1] => 20 |
| [2] => 30 |
| [3] => 40 |
| [4] => 50 |
| [5] => 60 |
| ) |
Can you add map/filter to OrderedSet?
Lookup tables
- Map from arbitrary value to arbitrary value
- Keys are int or string only
- Good for user/config-supplied keys
A common pattern
| $builders = []; |
| foreach (getRegisteredBuilderPlugins() as $builder) { |
| $builders[$builder->format()] = $builder; |
| } |
| |
| |
| $output = $lookup[$request->getHeaderLine('Accept')]->buildOutput($stuff); |
- Error handling?
- Missing keys?
- Type safety?
Build your own lookup utility
| class Lookup implements ArrayAccess { |
| protected string $type; |
| protected array $values; |
| protected mixed $default; |
| |
| public static function forType(string $type, $default = null): static { |
| |
| } |
| |
| public function offsetSet($offset, $value) { |
| if (! is_string($offset)) { throw new \TypeError(...); } |
| if (! $value instanceof $this->type) { throw new \TypeError(...);} |
| |
| $this->values[$offset] = $value; |
| } |
| |
| public function offsetGet($offset) { |
| return $this->values[$offset] ?? $this->default; |
| } |
| |
| public function offsetExists($offset) { ... } |
| public function offsetUnset($offset) { ... } |
| } |
Error handling is internalized
| $builders = Lookup::forType(OutputBuilder::class, new DefaultBuilder()); |
| |
| foreach (getRegisteredBuilderPlugins() as $builder) { |
| $builders[$builder->format()] = $builder; |
| } |
| |
| |
| $output = $lookup[$request->getHeaderLine('Accept')]->buildOutput($stuff); |
Or without ArrayAccess
| class Lookup { |
| protected string $type; |
| protected array $values; |
| |
| public static function forType(string $type): static { |
| |
| } |
| |
| public function add(string $key, $value): void { |
| if (! $value instanceof $this->type) { throw new \TypeError(...);} |
| |
| $this->values[$key] = $value; |
| } |
| |
| public function lookup(string $key, $default = null): mixed { |
| return $this->values[$offset] ?? $default; |
| } |
| } |
Or a purpose-built service
| class BuilderMap { |
| protected array $builders = []; |
| |
| public function __construct( |
| protected BuilderProvider $provider, |
| protected string $defaultFmt, |
| ) { } |
| |
| protected function buildLookup(): void { |
| foreach ($this->provider->getRegisteredBuilderPlugins() as $b) { |
| $this->builders[$b->format()] = $b; |
| } |
| } |
| |
| public function lookup(string $key): OutputBuilder { |
| if (!$this->builders) $this->buildLookup(); |
| return $this->values[$key] ?? $this->values[$this->defaultFmt]; |
| } |
| } |
| $builder = $builders->lookup($req->getHeaderLine('Accept')); |
| $output = $builder->buildOutput($stuff); |
Anonymous structs
Anonymous structs
| $order = [ |
| 'id' => 345, |
| 'total' => 100.45, |
| 'skus' => [ 123, '5B3', '987'], |
| 'canceled' => false, |
| 'usr' => 8, |
| ]; |
- Is cancelled spelled right?
- Is usr correct or a typo?
- Is user an ID or User object?
- Are skus numeric or strings?
- Can skus be a single value?
- Are there other properties?
- What currency is total in?
Named structs
| class Order { |
| public string $id; |
| public Money $total; |
| public User $user; |
| public bool $cancelled = false; |
| public array $skus = []; |
| } |
In PHP 8.0
| class Order { |
| public function __construct( |
| public string $id, |
| public Money $total, |
| public bool $cancelled = false, |
| public User $user, |
| public array $skus = [], |
| ) {} |
| } |
$options
is a code smell
| function formatProduct(Product $product, array $options): string { |
| |
| } |
- What are the values for
$options
?
- Which are required
- How are they spelled
- What are their legal types? How strict?
¯\_(ツ)_/¯
So, docs?
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function formatProduct(Product $product, array $options): string { ... } |
But good code is self documenting!
Objects are self-documenting
| class FormatterOptions { |
| public function __construct( |
| public string $label = 'Your product', |
| public string $bgcolor = '#FFF', |
| public string $color = '#000', |
| public int $fontSize = 14, |
| public string $layout, |
| ) {} |
| } |
| |
| function formatProduct(Product $p, FormatterOptions $options): string {...} |
Objects have methods
| class FormatterOptions { |
| public string $label = 'Your product'; |
| public string $bgcolor = '#FFF'; |
| public string $color = '#000'; |
| public int $fontSize = 14; |
| public string $layout; |
| |
| |
| public static function forLayout(string $layout, mixed ...$extra) { ... } |
| |
| public function darkMode(bool $dark): static { |
| $this->bgcolor = $dark ? '#000' : '#FFF'; |
| $this->color = $dark ? '#FFF' : '#000'; |
| return $this; |
| } |
| } |
Turn it around
| class ProductFormatter { |
| |
| |
| public function __construct(string $layout) { |
| $this->layout = $layout; |
| } |
| |
| public function darkMode(bool $dark): static { ... } |
| |
| public function setColors(string $color, string $bgcolor): static { |
| $this->color = $color; |
| $this->bgcolor = $bgcolor; |
| return $this; |
| } |
| |
| public function setFontInPoints(int $size): static { |
| $this->fontSize = $size; |
| return $this; |
| } |
| |
| public function format(Product $product): string { ... } |
| } |
Easy to use
| $formatter = new ProductFormatter('standard'); |
| |
| $formatter->darkMode(true)->format($product); |
If has dependencies
| $formatter = $themeSystem->getFormatter('standard'); |
| |
| $formatter->darkMode(true)->format($product); |
But aren't public properties eeeevil?
- Private object => Public properties
- Public API => protected properties with meaningful methods