Skip to content

Commit 50b0f60

Browse files
soyukaclaude
andcommitted
feat: add recursive container traversal via walkContainer generator
Adds walkContainer() which recursively descends an LDP container tree using getContainerContents(), yielding ContainerEntry objects. Supports configurable depth limits: -1 for unlimited, 0 for current level only. Uses PHP generators for memory-efficient traversal of deep hierarchies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 90b224f commit 50b0f60

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

src/SolidClient.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ public function ensureContainerExists(string $url, array $options = []): void
167167
$this->put($url, null, true, $options);
168168
}
169169

170+
/**
171+
* Recursively walks an LDP container tree, yielding ContainerEntry objects.
172+
*
173+
* @param int $maxDepth -1 for unlimited, 0 for current level only
174+
*
175+
* @return \Generator<int, ContainerEntry>
176+
*/
177+
public function walkContainer(string $url, int $maxDepth = -1, array $options = []): \Generator
178+
{
179+
$entries = $this->getContainerContents($url, $options);
180+
181+
foreach ($entries as $entry) {
182+
yield $entry;
183+
184+
if ($entry->isContainer && 0 !== $maxDepth) {
185+
yield from $this->walkContainer(
186+
$entry->url,
187+
-1 === $maxDepth ? -1 : $maxDepth - 1,
188+
$options,
189+
);
190+
}
191+
}
192+
}
193+
170194
public function getResourceMetadata(string $url, array $options = []): ResourceMetadata
171195
{
172196
$response = $this->head($url, $options);

tests/ContainerOperationsTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,74 @@ public function testEnsureContainerExistsCreates(): void
138138
$methods = array_column($requests, 0);
139139
$this->assertContains('PUT', $methods);
140140
}
141+
142+
public function testWalkContainer(): void
143+
{
144+
$rootJson = json_encode([
145+
'@id' => 'http://pod.example/root/',
146+
'@type' => ['http://www.w3.org/ns/ldp#BasicContainer'],
147+
'http://www.w3.org/ns/ldp#contains' => [
148+
['@id' => 'http://pod.example/root/file.txt'],
149+
['@id' => 'http://pod.example/root/sub/', '@type' => ['http://www.w3.org/ns/ldp#BasicContainer']],
150+
],
151+
]);
152+
153+
$subJson = json_encode([
154+
'@id' => 'http://pod.example/root/sub/',
155+
'@type' => ['http://www.w3.org/ns/ldp#BasicContainer'],
156+
'http://www.w3.org/ns/ldp#contains' => [
157+
['@id' => 'http://pod.example/root/sub/nested.ttl'],
158+
],
159+
]);
160+
161+
$httpClient = new MockHttpClient(static function (string $method, string $url) use ($rootJson, $subJson): MockResponse {
162+
if ('http://pod.example/root/' === $url) {
163+
return new MockResponse($rootJson, [
164+
'http_code' => 200,
165+
'response_headers' => ['Content-Type' => 'application/ld+json'],
166+
]);
167+
}
168+
if ('http://pod.example/root/sub/' === $url) {
169+
return new MockResponse($subJson, [
170+
'http_code' => 200,
171+
'response_headers' => ['Content-Type' => 'application/ld+json'],
172+
]);
173+
}
174+
175+
return new MockResponse('', ['http_code' => 404]);
176+
});
177+
$client = new SolidClient($httpClient);
178+
179+
$entries = iterator_to_array($client->walkContainer('http://pod.example/root/'), false);
180+
181+
$this->assertCount(3, $entries);
182+
$this->assertSame('http://pod.example/root/file.txt', $entries[0]->url);
183+
$this->assertSame('http://pod.example/root/sub/', $entries[1]->url);
184+
$this->assertSame('http://pod.example/root/sub/nested.ttl', $entries[2]->url);
185+
}
186+
187+
public function testWalkContainerWithMaxDepth(): void
188+
{
189+
$rootJson = json_encode([
190+
'@id' => 'http://pod.example/root/',
191+
'@type' => ['http://www.w3.org/ns/ldp#BasicContainer'],
192+
'http://www.w3.org/ns/ldp#contains' => [
193+
['@id' => 'http://pod.example/root/sub/', '@type' => ['http://www.w3.org/ns/ldp#BasicContainer']],
194+
],
195+
]);
196+
197+
$httpClient = new MockHttpClient(static function (string $method, string $url) use ($rootJson): MockResponse {
198+
return new MockResponse($rootJson, [
199+
'http_code' => 200,
200+
'response_headers' => ['Content-Type' => 'application/ld+json'],
201+
]);
202+
});
203+
$client = new SolidClient($httpClient);
204+
205+
$entries = iterator_to_array($client->walkContainer('http://pod.example/root/', 0), false);
206+
207+
// maxDepth=0 should only return root level entries, no recursion
208+
$this->assertCount(1, $entries);
209+
$this->assertSame('http://pod.example/root/sub/', $entries[0]->url);
210+
}
141211
}

0 commit comments

Comments
 (0)