Skip to content

Commit 625edd5

Browse files
soyukaclaude
andcommitted
feat: add ResourceMetadata value object and getResourceMetadata()
Parses HEAD response headers into a structured ResourceMetadata object with content type, length, last modified, LDP type, WAC-Allow permissions, and ACL URL from Link headers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d63fe47 commit 625edd5

File tree

3 files changed

+155
-0
lines changed

3 files changed

+155
-0
lines changed

src/ResourceMetadata.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 ResourceMetadata
15+
{
16+
/**
17+
* @param array<string, list<string>> $wacAllow parsed WAC-Allow header (e.g. ['user' => ['read', 'write'], 'public' => ['read']])
18+
*/
19+
public function __construct(
20+
public readonly ?string $contentType = null,
21+
public readonly ?int $contentLength = null,
22+
public readonly ?\DateTimeImmutable $lastModified = null,
23+
public readonly ?string $ldpType = null,
24+
public readonly array $wacAllow = [],
25+
public readonly ?string $aclUrl = null,
26+
) {
27+
}
28+
29+
public function isContainer(): bool
30+
{
31+
return 'http://www.w3.org/ns/ldp#BasicContainer' === $this->ldpType
32+
|| 'http://www.w3.org/ns/ldp#Container' === $this->ldpType;
33+
}
34+
35+
/**
36+
* @param array<string, list<string>> $responseHeaders normalized response headers
37+
*/
38+
public static function fromResponseHeaders(array $responseHeaders): self
39+
{
40+
$contentType = isset($responseHeaders['content-type'][0])
41+
? explode(';', $responseHeaders['content-type'][0], 2)[0]
42+
: null;
43+
44+
$contentLength = isset($responseHeaders['content-length'][0])
45+
? (int) $responseHeaders['content-length'][0]
46+
: null;
47+
48+
$lastModified = isset($responseHeaders['last-modified'][0])
49+
? \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC7231, $responseHeaders['last-modified'][0]) ?: null
50+
: null;
51+
52+
$ldpType = null;
53+
$aclUrl = null;
54+
foreach ($responseHeaders['link'] ?? [] as $linkHeader) {
55+
foreach (explode(',', $linkHeader) as $link) {
56+
$link = trim($link);
57+
if (preg_match('/<([^>]+)>;\s*rel="type"/', $link, $matches)) {
58+
if (str_contains($matches[1], 'ldp#')) {
59+
$ldpType = $matches[1];
60+
}
61+
}
62+
if (preg_match('/<([^>]+)>;\s*rel="acl"/', $link, $matches)) {
63+
$aclUrl = $matches[1];
64+
}
65+
}
66+
}
67+
68+
$wacAllow = [];
69+
if (isset($responseHeaders['wac-allow'][0])) {
70+
// Format: user="read write append control",public="read"
71+
if (preg_match_all('/(\w+)="([^"]*)"/', $responseHeaders['wac-allow'][0], $matches, \PREG_SET_ORDER)) {
72+
foreach ($matches as $match) {
73+
$wacAllow[$match[1]] = array_filter(explode(' ', $match[2]));
74+
}
75+
}
76+
}
77+
78+
return new self($contentType, $contentLength, $lastModified, $ldpType, $wacAllow, $aclUrl);
79+
}
80+
}

src/SolidClient.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ public function patch(string $url, string $data, string $contentType = 'applicat
9696
return $this->request('PATCH', $url, $options);
9797
}
9898

99+
public function getResourceMetadata(string $url, array $options = []): ResourceMetadata
100+
{
101+
$response = $this->head($url, $options);
102+
103+
return ResourceMetadata::fromResponseHeaders($response->getHeaders(false));
104+
}
105+
99106
public function request(string $method, string $url, array $options = []): ResponseInterface
100107
{
101108
if ($accessToken = $this->oidcClient?->getAccessToken()) {

tests/ResourceMetadataTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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\ResourceMetadata;
15+
use PHPUnit\Framework\TestCase;
16+
17+
class ResourceMetadataTest extends TestCase
18+
{
19+
public function testFromResponseHeaders(): void
20+
{
21+
$headers = [
22+
'content-type' => ['text/turtle; charset=utf-8'],
23+
'content-length' => ['4567'],
24+
'last-modified' => ['Mon, 23 Feb 2026 12:00:00 GMT'],
25+
'link' => [
26+
'<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
27+
'<http://pod.example/resource.acl>; rel="acl"',
28+
],
29+
'wac-allow' => ['user="read write append control",public="read"'],
30+
];
31+
32+
$metadata = ResourceMetadata::fromResponseHeaders($headers);
33+
34+
$this->assertSame('text/turtle', $metadata->contentType);
35+
$this->assertSame(4567, $metadata->contentLength);
36+
$this->assertInstanceOf(\DateTimeImmutable::class, $metadata->lastModified);
37+
$this->assertSame('http://www.w3.org/ns/ldp#BasicContainer', $metadata->ldpType);
38+
$this->assertTrue($metadata->isContainer());
39+
$this->assertSame('http://pod.example/resource.acl', $metadata->aclUrl);
40+
$this->assertSame(['read', 'write', 'append', 'control'], $metadata->wacAllow['user']);
41+
$this->assertSame(['read'], $metadata->wacAllow['public']);
42+
}
43+
44+
public function testResourceNotContainer(): void
45+
{
46+
$headers = [
47+
'content-type' => ['application/ld+json'],
48+
'link' => ['<http://www.w3.org/ns/ldp#Resource>; rel="type"'],
49+
];
50+
51+
$metadata = ResourceMetadata::fromResponseHeaders($headers);
52+
53+
$this->assertFalse($metadata->isContainer());
54+
$this->assertSame('http://www.w3.org/ns/ldp#Resource', $metadata->ldpType);
55+
}
56+
57+
public function testEmptyHeaders(): void
58+
{
59+
$metadata = ResourceMetadata::fromResponseHeaders([]);
60+
61+
$this->assertNull($metadata->contentType);
62+
$this->assertNull($metadata->contentLength);
63+
$this->assertNull($metadata->lastModified);
64+
$this->assertNull($metadata->ldpType);
65+
$this->assertNull($metadata->aclUrl);
66+
$this->assertSame([], $metadata->wacAllow);
67+
}
68+
}

0 commit comments

Comments
 (0)