Skip to content

Commit 98ba8cb

Browse files
soyukaclaude
andcommitted
feat: add container listing with JSON-LD parsing via ml/json-ld
Adds getContainerContents() to parse ldp:contains from JSON-LD responses. Uses ML\JsonLD\JsonLD::expand() with base URL option to handle relative IRI resolution (CSS returns relative @id values in expanded JSON-LD). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 34512cf commit 98ba8cb

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

src/ContainerEntry.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Solid Client PHP project.
5+
* (c) Kévin Dunglas <kevin@dunglas.fr>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Dunglas\PhpSolidClient;
13+
14+
final class ContainerEntry
15+
{
16+
/**
17+
* @param list<string> $types RDF types of this entry
18+
*/
19+
public function __construct(
20+
public readonly string $url,
21+
public readonly bool $isContainer,
22+
public readonly array $types = [],
23+
) {
24+
}
25+
}

src/SolidClient.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Dunglas\PhpSolidClient;
1313

1414
use EasyRdf\Graph;
15+
use ML\JsonLD\JsonLD;
1516
use Symfony\Contracts\HttpClient\HttpClientInterface;
1617
use Symfony\Contracts\HttpClient\ResponseInterface;
1718

@@ -103,6 +104,41 @@ public function getResourceMetadata(string $url, array $options = []): ResourceM
103104
return ResourceMetadata::fromResponseHeaders($response->getHeaders(false));
104105
}
105106

107+
/**
108+
* Lists the contents of an LDP container by parsing ldp:contains from JSON-LD.
109+
*
110+
* @return list<ContainerEntry>
111+
*/
112+
public function getContainerContents(string $url, array $options = []): array
113+
{
114+
$options['headers']['Accept'] = 'application/ld+json';
115+
$response = $this->get($url, $options);
116+
$decoded = json_decode($response->getContent());
117+
$expanded = JsonLD::expand($decoded, ['base' => $url]);
118+
119+
$entries = [];
120+
foreach ($expanded as $node) {
121+
$contains = $node->{'http://www.w3.org/ns/ldp#contains'} ?? [];
122+
if ([] === $contains || !$contains) {
123+
continue;
124+
}
125+
126+
foreach ($contains as $entry) {
127+
$entryUrl = $entry->{'@id'} ?? null;
128+
if (null === $entryUrl) {
129+
continue;
130+
}
131+
$types = $entry->{'@type'} ?? [];
132+
$isContainer = [] !== array_filter($types, ResourceMetadata::isContainerType(...))
133+
|| str_ends_with($entryUrl, '/');
134+
135+
$entries[] = new ContainerEntry($entryUrl, $isContainer, $types);
136+
}
137+
}
138+
139+
return $entries;
140+
}
141+
106142
public function request(string $method, string $url, array $options = []): ResponseInterface
107143
{
108144
if ($accessToken = $this->oidcClient?->getAccessToken()) {

tests/ContainerOperationsTest.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Solid Client PHP project.
5+
* (c) Kévin Dunglas <kevin@dunglas.fr>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Dunglas\PhpSolidClient\Tests;
13+
14+
use Dunglas\PhpSolidClient\ContainerEntry;
15+
use Dunglas\PhpSolidClient\SolidClient;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\Component\HttpClient\MockHttpClient;
18+
use Symfony\Component\HttpClient\Response\MockResponse;
19+
20+
class ContainerOperationsTest extends TestCase
21+
{
22+
public function testGetResourceMetadata(): void
23+
{
24+
$response = new MockResponse('', [
25+
'http_code' => 200,
26+
'response_headers' => [
27+
'Content-Type' => 'text/turtle',
28+
'Content-Length' => '512',
29+
'Link' => '<http://www.w3.org/ns/ldp#Resource>; rel="type"',
30+
'WAC-Allow' => 'user="read write",public="read"',
31+
],
32+
]);
33+
$httpClient = new MockHttpClient($response);
34+
$client = new SolidClient($httpClient);
35+
36+
$metadata = $client->getResourceMetadata('http://pod.example/resource');
37+
38+
$this->assertSame('text/turtle', $metadata->contentType);
39+
$this->assertSame(512, $metadata->contentLength);
40+
$this->assertSame('http://www.w3.org/ns/ldp#Resource', $metadata->ldpType);
41+
$this->assertFalse($metadata->isContainer());
42+
$this->assertSame(['read', 'write'], $metadata->wacAllow['user']);
43+
$this->assertSame(['read'], $metadata->wacAllow['public']);
44+
}
45+
46+
public function testGetContainerContents(): void
47+
{
48+
$jsonLd = json_encode([
49+
'@id' => 'http://pod.example/container/',
50+
'@type' => ['http://www.w3.org/ns/ldp#BasicContainer'],
51+
'http://www.w3.org/ns/ldp#contains' => [
52+
['@id' => 'http://pod.example/container/file.txt'],
53+
[
54+
'@id' => 'http://pod.example/container/sub/',
55+
'@type' => ['http://www.w3.org/ns/ldp#BasicContainer'],
56+
],
57+
],
58+
]);
59+
60+
$response = new MockResponse($jsonLd, [
61+
'http_code' => 200,
62+
'response_headers' => ['Content-Type' => 'application/ld+json'],
63+
]);
64+
$httpClient = new MockHttpClient($response);
65+
$client = new SolidClient($httpClient);
66+
67+
$entries = $client->getContainerContents('http://pod.example/container/');
68+
69+
$this->assertCount(2, $entries);
70+
$this->assertInstanceOf(ContainerEntry::class, $entries[0]);
71+
$this->assertSame('http://pod.example/container/file.txt', $entries[0]->url);
72+
$this->assertFalse($entries[0]->isContainer);
73+
$this->assertSame('http://pod.example/container/sub/', $entries[1]->url);
74+
$this->assertTrue($entries[1]->isContainer);
75+
}
76+
77+
public function testGetContainerContentsWithRelativeIds(): void
78+
{
79+
$jsonLd = json_encode([
80+
'@id' => './',
81+
'@type' => ['http://www.w3.org/ns/ldp#BasicContainer'],
82+
'http://www.w3.org/ns/ldp#contains' => [
83+
['@id' => 'file.txt'],
84+
['@id' => 'sub/', '@type' => ['http://www.w3.org/ns/ldp#BasicContainer']],
85+
],
86+
]);
87+
88+
$response = new MockResponse($jsonLd, [
89+
'http_code' => 200,
90+
'response_headers' => ['Content-Type' => 'application/ld+json'],
91+
]);
92+
$httpClient = new MockHttpClient($response);
93+
$client = new SolidClient($httpClient);
94+
95+
$entries = $client->getContainerContents('http://pod.example/data/');
96+
97+
$this->assertCount(2, $entries);
98+
$this->assertSame('http://pod.example/data/file.txt', $entries[0]->url);
99+
$this->assertSame('http://pod.example/data/sub/', $entries[1]->url);
100+
}
101+
}

0 commit comments

Comments
 (0)