Skip to content

Commit 3d94ebd

Browse files
Merge pull request #80 from ghostwriter/feature/support-empty-collections-in-embedded-section
Support empty collections in `_embedded` section
2 parents 07dff20 + b432dfd commit 3d94ebd

12 files changed

+260
-31
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
[![Build Status](https://github.com/mezzio/mezzio-hal/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/mezzio/mezzio-hal/actions/workflows/continuous-integration.yml)
44

55
> ## 🇷🇺 Русским гражданам
6-
>
6+
>
77
> Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
8-
>
8+
>
99
> У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
10-
>
10+
>
1111
> Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
12-
>
12+
>
1313
> ## 🇺🇸 To Citizens of Russia
14-
>
14+
>
1515
> We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
16-
>
16+
>
1717
> One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
18-
>
18+
>
1919
> You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
2020
2121
This library provides utilities for modeling HAL resources with links and generating [PSR-7](https://www.php-fig.org/psr/psr-7/) responses representing both JSON and XML serializations of them.

docs/book/v2/links-and-resources.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,43 @@ $resource = $resource->withLink($link);
198198
$resource = $resource->withoutLink($link);
199199
```
200200

201+
### Embed Empty Collection
202+
203+
> INFO: **New Feature**
204+
> Available since version 2.7.0.
205+
206+
To maintain consistency in the structure of the response, you may choose to embed both non-empty and empty collections within the `_embedded` section. This can be achieved by enabling the `embed-empty-collections` configuration option.
207+
208+
To enable this feature, modify the configuration file `config/autoload/hal.global.php` as follows:
209+
210+
```php
211+
return [
212+
'mezzio-hal' => [
213+
'embed-empty-collections' => false, // (default: false for compatibility reasons)
214+
'metadata-factories' => [...],
215+
'resource-generator' => [...],
216+
],
217+
];
218+
```
219+
220+
The default setting of `false` ensures compatibility with existing API endpoints and prevents potential test failures.
221+
222+
When `embed-empty-collections` is set to `false`, the representation will be as follows:
223+
224+
```json
225+
{
226+
"contacts": []
227+
}
228+
```
229+
230+
However, when `embed-empty-collections` is set to `true`, the representation will be as follows:
231+
232+
```json
233+
{
234+
"_embedded": {
235+
"contacts": []
236+
}
237+
}
238+
```
239+
201240
With these tools, you can describe any resource you want to represent.

psalm-baseline.xml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,9 +448,6 @@
448448
</MixedArrayOffset>
449449
</file>
450450
<file src="src/ResourceGenerator/UrlBasedCollectionStrategy.php">
451-
<InvalidArgument>
452-
<code>$page</code>
453-
</InvalidArgument>
454451
<MethodSignatureMismatch>
455452
<code>protected function generateSelfLink(</code>
456453
</MethodSignatureMismatch>

src/ConfigProvider.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,16 @@ public function getDependencies(): array
6969
public function getHalConfig(): array
7070
{
7171
return [
72-
'resource-generator' => [
72+
'embed-empty-collections' => false,
73+
'resource-generator' => [
7374
'strategies' => [ // The registered strategies and their metadata types
7475
RouteBasedCollectionMetadata::class => RouteBasedCollectionStrategy::class,
7576
RouteBasedResourceMetadata::class => RouteBasedResourceStrategy::class,
7677
UrlBasedCollectionMetadata::class => UrlBasedCollectionStrategy::class,
7778
UrlBasedResourceMetadata::class => UrlBasedResourceStrategy::class,
7879
],
7980
],
80-
'metadata-factories' => [ // The factories for the metadata types
81+
'metadata-factories' => [ // The factories for the metadata types
8182
RouteBasedCollectionMetadata::class => RouteBasedCollectionMetadataFactory::class,
8283
RouteBasedResourceMetadata::class => RouteBasedResourceMetadataFactory::class,
8384
UrlBasedCollectionMetadata::class => UrlBasedCollectionMetadataFactory::class,

src/HalResource.php

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use function array_shift;
2020
use function array_walk;
2121
use function count;
22+
use function get_debug_type;
2223
use function gettype;
2324
use function in_array;
2425
use function is_array;
@@ -47,32 +48,46 @@ class HalResource implements EvolvableLinkProviderInterface, JsonSerializable
4748
* @param LinkInterface[] $links
4849
* @param HalResource[][] $embedded
4950
*/
50-
public function __construct(array $data = [], array $links = [], array $embedded = [])
51-
{
51+
public function __construct(
52+
array $data = [],
53+
array $links = [],
54+
array $embedded = [],
55+
private bool $embedEmptyCollections = false
56+
) {
57+
$this->embedEmptyCollections = $embedEmptyCollections;
58+
5259
$context = self::class;
60+
5361
array_walk($data, function ($value, $name) use ($context) {
5462
$this->validateElementName($name, $context);
55-
if (
56-
! empty($value)
57-
&& ($value instanceof self || $this->isResourceCollection($value))
58-
) {
63+
64+
if ($value instanceof self || $this->isResourceCollection($value)) {
5965
$this->embedded[$name] = $value;
6066
return;
6167
}
68+
6269
$this->data[$name] = $value;
6370
});
6471

6572
array_walk($embedded, function ($resource, $name) use ($context) {
6673
$this->validateElementName($name, $context);
6774
$this->detectCollisionWithData($name, $context);
68-
if (! ($resource instanceof self || $this->isResourceCollection($resource))) {
69-
throw new InvalidArgumentException(sprintf(
70-
'Invalid embedded resource provided to %s constructor with name "%s"',
71-
$context,
72-
$name
73-
));
75+
76+
if (
77+
$resource instanceof self ||
78+
$resource === [] ||
79+
$this->isResourceCollection($resource)
80+
) {
81+
$this->embedded[$name] = $resource;
82+
return;
7483
}
75-
$this->embedded[$name] = $resource;
84+
85+
throw new InvalidArgumentException(sprintf(
86+
'Invalid embedded resource provided to %s constructor with name "%s":"%s"',
87+
$context,
88+
$name,
89+
get_debug_type($resource)
90+
));
7691
});
7792

7893
if (
@@ -142,8 +157,7 @@ public function withElement(string $name, $value): HalResource
142157
$this->validateElementName($name, __METHOD__);
143158

144159
if (
145-
! empty($value)
146-
&& ($value instanceof self || $this->isResourceCollection($value))
160+
$value instanceof self || $this->isResourceCollection($value)
147161
) {
148162
return $this->embed($name, $value);
149163
}
@@ -395,6 +409,10 @@ private function isResourceCollection($value): bool
395409
return false;
396410
}
397411

412+
if ($value === []) {
413+
return $this->embedEmptyCollections;
414+
}
415+
398416
return array_reduce($value, static function ($isResource, $item) {
399417
return $isResource && $item instanceof self;
400418
}, true);

src/ResourceGenerator.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ public function getStrategies(): array
9494

9595
public function fromArray(array $data, ?string $uri = null): HalResource
9696
{
97-
$resource = new HalResource($data);
97+
/** @psalm-suppress MixedArrayAccess */
98+
$embedEmptyCollections =
99+
$this->hydrators->has('config')
100+
&& $this->hydrators->get('config')['mezzio-hal']['embed-empty-collections'] ?? false;
101+
102+
$resource = new HalResource($data, [], [], $embedEmptyCollections);
98103

99104
if (null !== $uri) {
100105
return $resource->withLink(new Link('self', $uri));

src/ResourceGenerator/UrlBasedCollectionStrategy.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ protected function generateLinkForPage(
7373

7474
switch ($paginationType) {
7575
case Metadata\AbstractCollectionMetadata::TYPE_PLACEHOLDER:
76-
$url = str_replace($url, $paginationParam, $page);
76+
$url = str_replace($url, $paginationParam, (string) $page);
7777
break;
7878
case Metadata\AbstractCollectionMetadata::TYPE_QUERY:
7979
// fall-through
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"_links": {
3+
"self": {
4+
"href": "/api/contacts"
5+
}
6+
},
7+
"_embedded": {
8+
"contacts": []
9+
}
10+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"_links": {
3+
"self": {
4+
"href": "/api/contacts"
5+
}
6+
},
7+
"_embedded": {
8+
"contacts": [
9+
{
10+
"id": 1,
11+
"name": "John",
12+
"email": "[email protected]"
13+
},
14+
{
15+
"id": 2,
16+
"name": "Jane",
17+
"email": "[email protected]"
18+
}
19+
]
20+
}
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"contacts": null,
3+
"_links": {
4+
"self": {
5+
"href": "/api/contacts"
6+
}
7+
}
8+
}

0 commit comments

Comments
 (0)