Skip to content

Commit 066dc98

Browse files
committed
Convert package to use immutable value objects
1 parent 3ead2c1 commit 066dc98

22 files changed

+490
-505
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All Notable changes to `bakame/http-strucured-fields` will be documented in this
1515
- `Dictionnary::create` named constructor to create a new instance without any parameter.
1616
- `Type` Enum to list all possible Item Type supported.
1717
- `Value` Interface is introduced with `Item` being the only available implementation.
18+
- `MemberOrderedMap::add` and `MemberOrderedMap::remove` methods
1819

1920
### Fixed
2021

@@ -23,6 +24,7 @@ All Notable changes to `bakame/http-strucured-fields` will be documented in this
2324
- **[BC Break]** `::fromAssociative`, `::fromList`, `::fromPairs` methods require iterable arguments without default value.
2425
- **[BC Break]** `Item::value` method returns the Item (returns value can be `float|int|string|bool|ByteSequence|DateTimeImmutable|Token`).
2526
- **[BC Break]** `InnerList::parameters` is no longer accessible as a public readonly property.
27+
- **[BC Break]** Modifying container instances with `ArrayAccess` modifying methods is forbidden and will trigger a `LogicException` exception.
2628

2729
### Deprecated
2830

@@ -32,6 +34,8 @@ All Notable changes to `bakame/http-strucured-fields` will be documented in this
3234

3335
- **[BC Break]** `ForbiddenStateError` exception is removed, the `InvalidArgument` exception is used instead.
3436
- **[BC Break]** `Item::is*` methods are removed, the enum `Type` is used instead.
37+
- **[BC Break]** `MemberContainer::clear` method is removed without replacement.
38+
- **[BC Break]** - `MemberOrderedMap::set` and `MemberOrderedMap::delete` methods; use `MemberOrderedMap::add` and `MemberOrderedMap::remove` instead
3539

3640
## [0.6.0] - 2022-11-12
3741

docs/containers.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ At any given time it is possible with each of these objects to:
2626

2727
- iterate over its members using the `IteratorAggregate` interface;
2828
- know the number of members it contains via the `Countable` interface;
29-
- access container members via the `ArrayAccess` interface;
3029
- tell whether the container contains or not members with the `hasMembers` methods from the `Container` interface;
31-
- clear its content using the `clear` method from the `Container` interface;
30+
- to access the members using the methods exposed by the `ArrayAccess`. **WARNING: Updating using `ArrayAccess` method is forbidden and will result in a `LogicException` being emitted.**;
3231

3332
getter methods:
3433

@@ -37,5 +36,5 @@ getter methods:
3736

3837
**Of note:**
3938

40-
- All setter methods are chainable
39+
- All setter methods are returns a new instance with the applied changes.
4140
- For setter methods, Item types are inferred using `Item::from` if a `Item` object is not provided.

docs/index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ The package can be used to:
2323
use Bakame\Http\StructuredFields\Item;
2424

2525
$field = Item::from("/terms", ['rel' => 'copyright', 'anchor' => '#foo']);
26-
echo $field->toHttpValue(); // display "/terms";rel="copyright";anchor="#foo"
27-
echo $field->value(); // display "/terms"
28-
echo $field->parameters['rel']->value(); // display "copyright"
26+
echo $field->toHttpValue(); // display "/terms";rel="copyright";anchor="#foo"
27+
echo $field->value(); // display "/terms"
28+
echo $field->parameters->get('rel')->value(); // display "copyright"
2929
```
3030

3131
System Requirements
@@ -53,7 +53,7 @@ require 'path/to/http-structured-fields/repo/autoload.php';
5353
use Bakame\Http\StructuredFields\OrderedList;
5454

5555
$list = OrderedList::fromHttpValue('"/member/*/author", "/member/*/comments"');
56-
echo $list[-1]->value(); // returns '/member/*/comments'
56+
echo $list->get(-1)->value(); // returns '/member/*/comments'
5757
~~~
5858

5959
Documentation

docs/lists.md

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,6 @@ $orderedList = OrderedList::from(
5353
echo $orderedList->toHttpValue(); //returns '"42";foo="bar", (42.0 forty-two);a'
5454
```
5555

56-
Same example using the `ArrayAccess` interface.
57-
58-
```php
59-
use Bakame\Http\StructuredFields\InnerList;
60-
use Bakame\Http\StructuredFields\Token;
61-
62-
$innerList = InnerList::fromList([42, 42.0, "42"], ["a" => true]);
63-
isset($innerList[2]); //return true
64-
isset($innerList[42]); //return false
65-
$innerList[] = Token::fromString('forty-two');
66-
unset($innerList[0], $innerList[2]);
67-
echo $innerList->toHttpValue(); //returns '(42.0 forty-two);a'
68-
```
69-
7056
**if you try to set a key which does not exist an exception will be
7157
thrown as both classes must remain valid lists with no empty
7258
keys. Be aware that re-indexation behaviour may affect
@@ -77,6 +63,6 @@ use Bakame\Http\StructuredFields\OrderedList;
7763
use Bakame\Http\StructuredFields\Token;
7864

7965
$innerList = OrderedList::fromList([42, 42.0]);
80-
$innerList[2] = Token::fromString('forty-two'); // will throw
66+
$innerList->insert(2, Token::fromString('forty-two')); // will throw
8167
echo $innerList->toHttpValue(), PHP_EOL;
8268
```

docs/ordered-maps.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ getter methods:
2626

2727
setter methods:
2828

29-
- `set` add an element at the end of the container if the key is new otherwise only the value is updated;
29+
- `add` add an element at the end of the container if the key is new otherwise only the value is updated;
3030
- `append` always add an element at the end of the container, if already present the previous value is removed;
3131
- `prepend` always add an element at the beginning of the container, if already present the previous value is removed;
32-
- `delete` to remove elements based on their associated keys;
32+
- `remove` to remove elements based on their associated keys;
3333
- `mergeAssociative` merge multiple instances of iterable structure as associative constructs;
3434
- `mergePairs` merge multiple instances of iterable structure as pairs constructs;
3535

@@ -53,24 +53,22 @@ $dictionary->hasPair(-1); //return true
5353

5454
echo $dictionary
5555
->append('z', 42.0)
56-
->delete('b', 'c')
56+
->remove('b', 'c')
5757
->toHttpValue(); // returns "a=?0, z=42.0"
5858
```
5959

6060
In addition to `StructuredField` specific interfaces, both classes implements:
6161

6262
- PHP `Countable` interface.
6363
- PHP `IteratorAggregate` interface.
64-
- PHP `ArrayAccess` interface.
6564

6665
```php
6766
use Bakame\Http\StructuredFields\Parameters;
6867

6968
$parameters = Parameters::fromAssociative(['b' => true, 'foo' => 'bar']);
70-
$parameters->keys(); // returns ['b', 'foo']
71-
$parameters->get('b'); // returns Item::from(true)
72-
$parameters['b']; // returns Item::from(true)
73-
$parameters['b']->value(); // returns true
69+
$parameters->keys(); // returns ['b', 'foo']
70+
$parameters->get('b'); // returns Item::from(true)
71+
$parameters->get('b')->value(); // returns true
7472
iterator_to_array($parameters->toPairs(), true); // returns [['b', Item::from(true)], ['foo', Item::from('bar')]]
7573
iterator_to_array($parameters, true); // returns ['b' => Item::from(true), 'foo' => Item::from('bar')]
7674
$parameters->mergeAssociative(

src/ByteSequence.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use function base64_encode;
1010
use function preg_match;
1111

12+
/**
13+
* @see https://www.rfc-editor.org/rfc/rfc8941.html#section-3.3.5
14+
*/
1215
final class ByteSequence
1316
{
1417
private function __construct(

src/Dictionary.php

Lines changed: 55 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace Bakame\Http\StructuredFields;
66

7+
use ArrayAccess;
78
use DateTimeInterface;
89
use Iterator;
10+
use LogicException;
911
use Stringable;
1012
use function array_key_exists;
1113
use function array_keys;
@@ -15,24 +17,39 @@
1517
use function is_array;
1618

1719
/**
20+
* @see https://www.rfc-editor.org/rfc/rfc8941.html#section-3.2
21+
*
1822
* @implements MemberOrderedMap<string, Value|InnerList<int, Value>>
23+
* @implements ArrayAccess<string, Value|InnerList<int, Value>>
1924
* @phpstan-import-type DataType from Item
2025
*/
21-
final class Dictionary implements MemberOrderedMap
26+
final class Dictionary implements MemberOrderedMap, ArrayAccess
2227
{
2328
/** @var array<string, Value|InnerList<int, Value>> */
2429
private array $members = [];
2530

2631
/**
27-
* @param iterable<string, InnerList<int, Value>|Value|DataType> $members
32+
* @param iterable<string, InnerList<int, Value>|iterable<Value|DataType>|Value|DataType> $members
2833
*/
2934
private function __construct(iterable $members = [])
3035
{
3136
foreach ($members as $key => $member) {
32-
$this->set($key, $member);
37+
$this->members[MapKey::fromString($key)->value] = self::filterMember($member);
3338
}
3439
}
3540

41+
/**
42+
* @param StructuredField|iterable<Value|DataType>|DataType $member
43+
*/
44+
private static function filterMember(iterable|StructuredField|Token|ByteSequence|DateTimeInterface|Stringable|string|int|float|bool $member): InnerList|Value
45+
{
46+
return match (true) {
47+
$member instanceof InnerList, $member instanceof Value => $member,
48+
is_iterable($member) => InnerList::fromList($member),
49+
default => Item::from($member),
50+
};
51+
}
52+
3653
/**
3754
* Returns a new instance.
3855
*/
@@ -47,7 +64,7 @@ public static function create(): self
4764
* its keys represent the dictionary entry key
4865
* its values represent the dictionary entry value
4966
*
50-
* @param iterable<string, InnerList<int, Value>|Value|DataType> $members
67+
* @param iterable<string, InnerList<int, Value>|list<Value|DataType>|Value|DataType> $members
5168
*/
5269
public static function fromAssociative(iterable $members): self
5370
{
@@ -61,20 +78,19 @@ public static function fromAssociative(iterable $members): self
6178
* the first member represents the instance entry key
6279
* the second member represents the instance entry value
6380
*
64-
* @param MemberOrderedMap<string, Value|InnerList<int, Value>>|iterable<array{0:string, 1:InnerList<int, Value>|Value|DataType}> $pairs
81+
* @param MemberOrderedMap<string, Value|InnerList<int, Value>>|iterable<array{0:string, 1:InnerList<int, Value>|list<Value|DataType>|Value|DataType}> $pairs
6582
*/
66-
public static function fromPairs(MemberOrderedMap|iterable $pairs): self
83+
public static function fromPairs(iterable $pairs): self
6784
{
6885
if ($pairs instanceof MemberOrderedMap) {
6986
$pairs = $pairs->toPairs();
7087
}
7188

72-
$instance = new self();
73-
foreach ($pairs as $pair) {
74-
$instance->set(...$pair);
75-
}
76-
77-
return $instance;
89+
return new self((function (iterable $pairs) {
90+
foreach ($pairs as [$key, $member]) {
91+
yield $key => $member;
92+
}
93+
})($pairs));
7894
}
7995

8096
/**
@@ -84,12 +100,11 @@ public static function fromPairs(MemberOrderedMap|iterable $pairs): self
84100
*/
85101
public static function fromHttpValue(Stringable|string $httpValue): self
86102
{
87-
$instance = new self();
88-
foreach (Parser::parseDictionary($httpValue) as $key => $value) {
89-
$instance->set($key, is_array($value) ? InnerList::fromList(...$value) : $value);
90-
}
91-
92-
return $instance;
103+
return new self((function (iterable $pairs) {
104+
foreach ($pairs as $key => $value) {
105+
yield $key => is_array($value) ? InnerList::fromList(...$value) : $value;
106+
}
107+
})(Parser::parseDictionary($httpValue)));
93108
}
94109

95110
public function toHttpValue(): string
@@ -206,79 +221,64 @@ public function pair(int $index): array
206221
// @codeCoverageIgnoreEnd
207222
}
208223

209-
/**
210-
* @throws SyntaxError If the string key is not a valid
211-
*/
212-
public function set(string $key, StructuredField|Token|ByteSequence|DateTimeInterface|Stringable|string|int|float|bool $member): static
224+
public function add(string $key, StructuredField|Token|ByteSequence|DateTimeInterface|Stringable|string|int|float|bool $member): static
213225
{
214-
$this->members[MapKey::fromString($key)->value] = self::filterMember($member);
226+
$members = $this->members;
227+
$members[MapKey::fromString($key)->value] = self::filterMember($member);
215228

216-
return $this;
217-
}
218-
219-
private static function filterMember(StructuredField|Token|ByteSequence|DateTimeInterface|Stringable|string|int|float|bool $member): InnerList|Value
220-
{
221-
return match (true) {
222-
$member instanceof InnerList, $member instanceof Value => $member,
223-
$member instanceof StructuredField => throw new InvalidArgument('Expecting a "'.Value::class.'" or a "'.InnerList::class.'" instance; received a "'.$member::class.'" instead.'),
224-
default => Item::from($member),
225-
};
229+
return new self($members);
226230
}
227231

228-
public function delete(string ...$keys): static
232+
public function remove(string ...$keys): static
229233
{
234+
$members = $this->members;
230235
foreach ($keys as $key) {
231-
unset($this->members[$key]);
236+
unset($members[$key]);
232237
}
233238

234-
return $this;
235-
}
236-
237-
public function clear(): static
238-
{
239-
$this->members = [];
240-
241-
return $this;
239+
return new self($members);
242240
}
243241

244242
public function append(string $key, StructuredField|Token|ByteSequence|DateTimeInterface|Stringable|string|int|float|bool $member): static
245243
{
246-
unset($this->members[$key]);
244+
$members = $this->members;
245+
unset($members[$key]);
247246

248-
return $this->set($key, $member);
247+
return new self([...$members, $key => self::filterMember($member)]);
249248
}
250249

251250
public function prepend(string $key, StructuredField|Token|ByteSequence|DateTimeInterface|Stringable|string|int|float|bool $member): static
252251
{
253-
unset($this->members[$key]);
252+
$members = $this->members;
253+
unset($members[$key]);
254254

255-
$this->members = [...[MapKey::fromString($key)->value => self::filterMember($member)], ...$this->members];
256-
257-
return $this;
255+
return new self([$key => self::filterMember($member), ...$members]);
258256
}
259257

260258
/**
261259
* @param iterable<string, InnerList<int, Value>|Value|DataType> ...$others
262260
*/
263261
public function mergeAssociative(iterable ...$others): static
264262
{
263+
$members = $this->members;
265264
foreach ($others as $other) {
266-
$this->members = [...$this->members, ...self::fromAssociative($other)->members];
265+
$members = [...$members, ...self::fromAssociative($other)->members];
267266
}
268267

269-
return $this;
268+
return new self($members);
270269
}
271270

272271
/**
273272
* @param MemberOrderedMap<string, Value|InnerList<int, Value>>|iterable<array{0:string, 1:InnerList<int, Value>|Value|DataType}> ...$others
274273
*/
275274
public function mergePairs(MemberOrderedMap|iterable ...$others): static
276275
{
276+
$members = $this->members;
277277
foreach ($others as $other) {
278-
$this->members = [...$this->members, ...self::fromPairs($other)->members];
278+
$members = [...$members, ...self::fromPairs($other)->members];
279279
}
280280

281-
return $this;
281+
return new self($members);
282282
}
283283

284284
/**
@@ -299,23 +299,13 @@ public function offsetGet(mixed $offset): InnerList|Value
299299
return $this->get($offset);
300300
}
301301

302-
/**
303-
* @param string $offset
304-
*/
305302
public function offsetUnset(mixed $offset): void
306303
{
307-
$this->delete($offset);
304+
throw new LogicException(self::class.' instance can not be updated using '.ArrayAccess::class.' methods.');
308305
}
309306

310-
/**
311-
* @param InnerList<int, Value>|Value|DataType $value
312-
*/
313307
public function offsetSet(mixed $offset, mixed $value): void
314308
{
315-
if (!is_string($offset)) {
316-
throw new SyntaxError('The offset for a dictionary member is expected to be a string; "'.gettype($offset).'" given.');
317-
}
318-
319-
$this->set($offset, $value);
309+
throw new LogicException(self::class.' instance can not be updated using '.ArrayAccess::class.' methods.');
320310
}
321311
}

0 commit comments

Comments
 (0)