Skip to content

Commit 6e80b78

Browse files
soyukaclaude
andcommitted
feat: add ensureContainerExists()
Checks via HEAD whether a container exists, catches ClientExceptionInterface for 404 specifically and lets other errors propagate. Recursively creates missing parent containers via PUT. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 98ba8cb commit 6e80b78

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

src/SolidClient.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use EasyRdf\Graph;
1515
use ML\JsonLD\JsonLD;
16+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
1617
use Symfony\Contracts\HttpClient\HttpClientInterface;
1718
use Symfony\Contracts\HttpClient\ResponseInterface;
1819

@@ -139,6 +140,50 @@ public function getContainerContents(string $url, array $options = []): array
139140
return $entries;
140141
}
141142

143+
/**
144+
* Ensures that an LDP container exists at the given URL, creating it (and any missing parents) if necessary.
145+
*/
146+
public function ensureContainerExists(string $url, array $options = []): void
147+
{
148+
if (!str_ends_with($url, '/')) {
149+
$url .= '/';
150+
}
151+
152+
try {
153+
$this->head($url, $options)->getHeaders();
154+
155+
return;
156+
} catch (ClientExceptionInterface $e) {
157+
if (404 !== $e->getResponse()->getStatusCode()) {
158+
throw $e;
159+
}
160+
}
161+
162+
// Ensure parent exists first
163+
$parentUrl = self::getParentContainerUrl($url);
164+
if (null !== $parentUrl && $parentUrl !== $url) {
165+
$this->ensureContainerExists($parentUrl, $options);
166+
}
167+
168+
$this->put($url, null, true, $options);
169+
}
170+
171+
private static function getParentContainerUrl(string $url): ?string
172+
{
173+
$trimmed = rtrim($url, '/');
174+
$lastSlash = strrpos($trimmed, '/');
175+
if (false === $lastSlash) {
176+
return null;
177+
}
178+
179+
$parent = substr($trimmed, 0, $lastSlash + 1);
180+
if (preg_match('#^https?://[^/]+/$#', $parent)) {
181+
return null;
182+
}
183+
184+
return $parent;
185+
}
186+
142187
public function request(string $method, string $url, array $options = []): ResponseInterface
143188
{
144189
if ($accessToken = $this->oidcClient?->getAccessToken()) {

tests/ContainerOperationsTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,45 @@ public function testGetContainerContentsWithRelativeIds(): void
9898
$this->assertSame('http://pod.example/data/file.txt', $entries[0]->url);
9999
$this->assertSame('http://pod.example/data/sub/', $entries[1]->url);
100100
}
101+
102+
public function testEnsureContainerExistsAlreadyExists(): void
103+
{
104+
$responses = [
105+
new MockResponse('', ['http_code' => 200]),
106+
];
107+
$httpClient = new MockHttpClient($responses);
108+
$client = new SolidClient($httpClient);
109+
110+
$client->ensureContainerExists('http://pod.example/existing/');
111+
112+
// Only one HEAD request should have been made
113+
$this->assertSame(1, $httpClient->getRequestsCount());
114+
}
115+
116+
public function testEnsureContainerExistsCreates(): void
117+
{
118+
$requests = [];
119+
$httpClient = new MockHttpClient(static function (string $method, string $url) use (&$requests): MockResponse {
120+
$requests[] = [$method, $url];
121+
if ('HEAD' === $method && 'http://pod.example/parent/' === $url) {
122+
return new MockResponse('', ['http_code' => 200]);
123+
}
124+
if ('HEAD' === $method) {
125+
return new MockResponse('', ['http_code' => 404]);
126+
}
127+
if ('PUT' === $method) {
128+
return new MockResponse('', ['http_code' => 201]);
129+
}
130+
131+
return new MockResponse('', ['http_code' => 500]);
132+
});
133+
$client = new SolidClient($httpClient);
134+
135+
$client->ensureContainerExists('http://pod.example/parent/child/');
136+
137+
// Should have HEAD child/ (404), HEAD parent/ (200), then PUT child/
138+
$methods = array_column($requests, 0);
139+
$this->assertContains('PUT', $methods);
140+
}
141+
101142
}

0 commit comments

Comments
 (0)