Skip to content

Commit d63fe47

Browse files
soyukaclaude
andcommitted
feat: add PUT, HEAD, DELETE and PATCH methods
Adds the missing HTTP method wrappers to SolidClient: - put(): creates/overwrites resources, with optional container Link header - head(): retrieves resource metadata headers - delete(): removes resources - patch(): updates resources via SPARQL Update or N3 Patch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0b0584b commit d63fe47

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed

src/SolidClient.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,44 @@ public function post(string $url, ?string $data = null, ?string $slug = null, bo
5858
return $this->request('POST', $url, $options);
5959
}
6060

61+
public function put(string $url, ?string $data = null, bool $isContainer = false, array $options = []): ResponseInterface
62+
{
63+
if (!isset($options['headers']['Content-Type'])) {
64+
$options['headers']['Content-Type'] = self::DEFAULT_MIME_TYPE;
65+
}
66+
if (null !== $data) {
67+
$options['body'] = $data;
68+
}
69+
if ($isContainer) {
70+
$options['headers']['Link'] = \sprintf('<%s>; rel="type"', self::LDP_BASIC_CONTAINER);
71+
}
72+
73+
return $this->request('PUT', $url, $options);
74+
}
75+
6176
public function get(string $url, array $options = []): ResponseInterface
6277
{
6378
return $this->request('GET', $url, $options);
6479
}
6580

81+
public function head(string $url, array $options = []): ResponseInterface
82+
{
83+
return $this->request('HEAD', $url, $options);
84+
}
85+
86+
public function delete(string $url, array $options = []): ResponseInterface
87+
{
88+
return $this->request('DELETE', $url, $options);
89+
}
90+
91+
public function patch(string $url, string $data, string $contentType = 'application/sparql-update', array $options = []): ResponseInterface
92+
{
93+
$options['headers']['Content-Type'] = $contentType;
94+
$options['body'] = $data;
95+
96+
return $this->request('PATCH', $url, $options);
97+
}
98+
6699
public function request(string $method, string $url, array $options = []): ResponseInterface
67100
{
68101
if ($accessToken = $this->oidcClient?->getAccessToken()) {

tests/SolidClientTest.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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\SolidClient;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\HttpClient\MockHttpClient;
17+
use Symfony\Component\HttpClient\Response\MockResponse;
18+
19+
class SolidClientTest extends TestCase
20+
{
21+
private static function findHeader(array $headers, string $name): ?string
22+
{
23+
$prefix = $name.': ';
24+
foreach ($headers as $header) {
25+
if (str_starts_with($header, $prefix)) {
26+
return substr($header, \strlen($prefix));
27+
}
28+
}
29+
30+
return null;
31+
}
32+
33+
public function testPut(): void
34+
{
35+
$response = new MockResponse('', ['http_code' => 201]);
36+
$httpClient = new MockHttpClient($response);
37+
$client = new SolidClient($httpClient);
38+
39+
$client->put('http://pod.example/resource', '<> a <http://schema.org/Thing> .');
40+
41+
$this->assertSame('PUT', $response->getRequestMethod());
42+
$this->assertSame('http://pod.example/resource', $response->getRequestUrl());
43+
$this->assertSame('<> a <http://schema.org/Thing> .', $response->getRequestOptions()['body']);
44+
$this->assertSame('text/turtle', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type'));
45+
}
46+
47+
public function testPutContainer(): void
48+
{
49+
$response = new MockResponse('', ['http_code' => 201]);
50+
$httpClient = new MockHttpClient($response);
51+
$client = new SolidClient($httpClient);
52+
53+
$client->put('http://pod.example/container/', null, true);
54+
55+
$this->assertSame('PUT', $response->getRequestMethod());
56+
$this->assertStringContainsString('ldp#BasicContainer', self::findHeader($response->getRequestOptions()['headers'], 'Link') ?? '');
57+
}
58+
59+
public function testPutCustomContentType(): void
60+
{
61+
$response = new MockResponse('', ['http_code' => 201]);
62+
$httpClient = new MockHttpClient($response);
63+
$client = new SolidClient($httpClient);
64+
65+
$client->put('http://pod.example/resource', '{}', false, [
66+
'headers' => ['Content-Type' => 'application/ld+json'],
67+
]);
68+
69+
$this->assertSame('application/ld+json', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type'));
70+
}
71+
72+
public function testHead(): void
73+
{
74+
$response = new MockResponse('', [
75+
'http_code' => 200,
76+
'response_headers' => [
77+
'Content-Type' => 'text/turtle',
78+
'Content-Length' => '1234',
79+
],
80+
]);
81+
$httpClient = new MockHttpClient($response);
82+
$client = new SolidClient($httpClient);
83+
84+
$client->head('http://pod.example/resource');
85+
86+
$this->assertSame('HEAD', $response->getRequestMethod());
87+
$this->assertSame('http://pod.example/resource', $response->getRequestUrl());
88+
}
89+
90+
public function testDelete(): void
91+
{
92+
$response = new MockResponse('', ['http_code' => 200]);
93+
$httpClient = new MockHttpClient($response);
94+
$client = new SolidClient($httpClient);
95+
96+
$client->delete('http://pod.example/resource');
97+
98+
$this->assertSame('DELETE', $response->getRequestMethod());
99+
$this->assertSame('http://pod.example/resource', $response->getRequestUrl());
100+
}
101+
102+
public function testPatchSparqlUpdate(): void
103+
{
104+
$sparql = 'INSERT DATA { <> <http://schema.org/name> "Test" . }';
105+
$response = new MockResponse('', ['http_code' => 200]);
106+
$httpClient = new MockHttpClient($response);
107+
$client = new SolidClient($httpClient);
108+
109+
$client->patch('http://pod.example/resource', $sparql);
110+
111+
$this->assertSame('PATCH', $response->getRequestMethod());
112+
$this->assertSame($sparql, $response->getRequestOptions()['body']);
113+
$this->assertSame('application/sparql-update', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type'));
114+
}
115+
116+
public function testPatchN3(): void
117+
{
118+
$n3Patch = '@prefix solid: <http://www.w3.org/ns/solid/terms#>. _:patch a solid:InsertDeletePatch .';
119+
$response = new MockResponse('', ['http_code' => 200]);
120+
$httpClient = new MockHttpClient($response);
121+
$client = new SolidClient($httpClient);
122+
123+
$client->patch('http://pod.example/resource', $n3Patch, 'text/n3');
124+
125+
$this->assertSame('text/n3', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type'));
126+
}
127+
128+
public function testPost(): void
129+
{
130+
$response = new MockResponse('', [
131+
'http_code' => 201,
132+
'response_headers' => ['Location' => 'http://pod.example/container/new-resource'],
133+
]);
134+
$httpClient = new MockHttpClient($response);
135+
$client = new SolidClient($httpClient);
136+
137+
$client->post('http://pod.example/container/', '<> a <http://schema.org/Thing> .', 'new-resource');
138+
139+
$this->assertSame('POST', $response->getRequestMethod());
140+
$this->assertSame('new-resource', self::findHeader($response->getRequestOptions()['headers'], 'Slug'));
141+
$this->assertStringContainsString('ldp#Resource', self::findHeader($response->getRequestOptions()['headers'], 'Link') ?? '');
142+
}
143+
144+
public function testGet(): void
145+
{
146+
$body = '<> a <http://schema.org/Thing> .';
147+
$response = new MockResponse($body, ['http_code' => 200]);
148+
$httpClient = new MockHttpClient($response);
149+
$client = new SolidClient($httpClient);
150+
151+
$result = $client->get('http://pod.example/resource');
152+
153+
$this->assertSame('GET', $response->getRequestMethod());
154+
$this->assertSame($body, $result->getContent());
155+
}
156+
}

0 commit comments

Comments
 (0)