Skip to content

Commit 923b2f1

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 7175534 commit 923b2f1

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

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

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