From 0b0584b6946ac20109d1878273c57aa32fc76cb4 Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 23 Feb 2026 21:43:00 +0100 Subject: [PATCH 1/2] style: apply PHP CS Fixer auto-fixes Co-Authored-By: Claude Opus 4.6 --- src/Bundle/Form/SolidLoginType.php | 2 +- src/SolidClient.php | 8 ++++---- src/SolidClientFactory.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Bundle/Form/SolidLoginType.php b/src/Bundle/Form/SolidLoginType.php index 7272c94..fb4e0cd 100644 --- a/src/Bundle/Form/SolidLoginType.php +++ b/src/Bundle/Form/SolidLoginType.php @@ -55,7 +55,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'constraints' => [new Callback(function (array $data, ExecutionContextInterface $context): void { + 'constraints' => [new Callback(static function (array $data, ExecutionContextInterface $context): void { $webId = $data['webid'] ?? ''; $op = $data['op'] ?? ''; diff --git a/src/SolidClient.php b/src/SolidClient.php index 7e61e0e..c70ee41 100644 --- a/src/SolidClient.php +++ b/src/SolidClient.php @@ -31,7 +31,7 @@ public function __construct( ) { } - public function createContainer(string $parentUrl, string $name, string $data = null): ResponseInterface + public function createContainer(string $parentUrl, string $name, ?string $data = null): ResponseInterface { return $this->post($parentUrl, $data, $name, true); } @@ -41,7 +41,7 @@ public function createContainer(string $parentUrl, string $name, string $data = * * @see https://github.com/solid/solid-web-client/blob/main/src/client.js#L231= */ - public function post(string $url, string $data = null, string $slug = null, bool $isContainer = false, array $options = []): ResponseInterface + public function post(string $url, ?string $data = null, ?string $slug = null, bool $isContainer = false, array $options = []): ResponseInterface { if ($isContainer || !isset($options['headers']['Content-Type'])) { $options['headers']['Content-Type'] = self::DEFAULT_MIME_TYPE; @@ -53,7 +53,7 @@ public function post(string $url, string $data = null, string $slug = null, bool $options['headers']['Slug'] = $slug; } - $options['headers']['Link'] = sprintf('<%s>; rel="type"', $isContainer ? self::LDP_BASIC_CONTAINER : self::LDP_RESOURCE); + $options['headers']['Link'] = \sprintf('<%s>; rel="type"', $isContainer ? self::LDP_BASIC_CONTAINER : self::LDP_RESOURCE); return $this->request('POST', $url, $options); } @@ -88,7 +88,7 @@ public function getOidcIssuer(string $webId, array $options = []): string { $graph = $this->getProfile($webId, $options); - $issuer = $graph->get($webId, sprintf('<%s>', self::OIDC_ISSUER))?->getUri(); + $issuer = $graph->get($webId, \sprintf('<%s>', self::OIDC_ISSUER))?->getUri(); if (!\is_string($issuer)) { throw new Exception('Unable to find the OIDC issuer associated with this WebID', 1); } diff --git a/src/SolidClientFactory.php b/src/SolidClientFactory.php index fcfe629..7d21591 100644 --- a/src/SolidClientFactory.php +++ b/src/SolidClientFactory.php @@ -22,7 +22,7 @@ public function __construct(private readonly HttpClientInterface $httpClient) { } - public function create(OidcClient $oidcClient = null): SolidClient + public function create(?OidcClient $oidcClient = null): SolidClient { return new SolidClient($this->httpClient, $oidcClient); } From d63fe47e24c07192bddbf26e825332bedd681c92 Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 23 Feb 2026 21:44:42 +0100 Subject: [PATCH 2/2] 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 --- src/SolidClient.php | 33 ++++++++ tests/SolidClientTest.php | 156 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 tests/SolidClientTest.php diff --git a/src/SolidClient.php b/src/SolidClient.php index c70ee41..ac1faf1 100644 --- a/src/SolidClient.php +++ b/src/SolidClient.php @@ -58,11 +58,44 @@ public function post(string $url, ?string $data = null, ?string $slug = null, bo return $this->request('POST', $url, $options); } + public function put(string $url, ?string $data = null, bool $isContainer = false, array $options = []): ResponseInterface + { + if (!isset($options['headers']['Content-Type'])) { + $options['headers']['Content-Type'] = self::DEFAULT_MIME_TYPE; + } + if (null !== $data) { + $options['body'] = $data; + } + if ($isContainer) { + $options['headers']['Link'] = \sprintf('<%s>; rel="type"', self::LDP_BASIC_CONTAINER); + } + + return $this->request('PUT', $url, $options); + } + public function get(string $url, array $options = []): ResponseInterface { return $this->request('GET', $url, $options); } + public function head(string $url, array $options = []): ResponseInterface + { + return $this->request('HEAD', $url, $options); + } + + public function delete(string $url, array $options = []): ResponseInterface + { + return $this->request('DELETE', $url, $options); + } + + public function patch(string $url, string $data, string $contentType = 'application/sparql-update', array $options = []): ResponseInterface + { + $options['headers']['Content-Type'] = $contentType; + $options['body'] = $data; + + return $this->request('PATCH', $url, $options); + } + public function request(string $method, string $url, array $options = []): ResponseInterface { if ($accessToken = $this->oidcClient?->getAccessToken()) { diff --git a/tests/SolidClientTest.php b/tests/SolidClientTest.php new file mode 100644 index 0000000..62e4b9f --- /dev/null +++ b/tests/SolidClientTest.php @@ -0,0 +1,156 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Dunglas\PhpSolidClient\Tests; + +use Dunglas\PhpSolidClient\SolidClient; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; + +class SolidClientTest extends TestCase +{ + private static function findHeader(array $headers, string $name): ?string + { + $prefix = $name.': '; + foreach ($headers as $header) { + if (str_starts_with($header, $prefix)) { + return substr($header, \strlen($prefix)); + } + } + + return null; + } + + public function testPut(): void + { + $response = new MockResponse('', ['http_code' => 201]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->put('http://pod.example/resource', '<> a .'); + + $this->assertSame('PUT', $response->getRequestMethod()); + $this->assertSame('http://pod.example/resource', $response->getRequestUrl()); + $this->assertSame('<> a .', $response->getRequestOptions()['body']); + $this->assertSame('text/turtle', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type')); + } + + public function testPutContainer(): void + { + $response = new MockResponse('', ['http_code' => 201]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->put('http://pod.example/container/', null, true); + + $this->assertSame('PUT', $response->getRequestMethod()); + $this->assertStringContainsString('ldp#BasicContainer', self::findHeader($response->getRequestOptions()['headers'], 'Link') ?? ''); + } + + public function testPutCustomContentType(): void + { + $response = new MockResponse('', ['http_code' => 201]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->put('http://pod.example/resource', '{}', false, [ + 'headers' => ['Content-Type' => 'application/ld+json'], + ]); + + $this->assertSame('application/ld+json', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type')); + } + + public function testHead(): void + { + $response = new MockResponse('', [ + 'http_code' => 200, + 'response_headers' => [ + 'Content-Type' => 'text/turtle', + 'Content-Length' => '1234', + ], + ]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->head('http://pod.example/resource'); + + $this->assertSame('HEAD', $response->getRequestMethod()); + $this->assertSame('http://pod.example/resource', $response->getRequestUrl()); + } + + public function testDelete(): void + { + $response = new MockResponse('', ['http_code' => 200]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->delete('http://pod.example/resource'); + + $this->assertSame('DELETE', $response->getRequestMethod()); + $this->assertSame('http://pod.example/resource', $response->getRequestUrl()); + } + + public function testPatchSparqlUpdate(): void + { + $sparql = 'INSERT DATA { <> "Test" . }'; + $response = new MockResponse('', ['http_code' => 200]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->patch('http://pod.example/resource', $sparql); + + $this->assertSame('PATCH', $response->getRequestMethod()); + $this->assertSame($sparql, $response->getRequestOptions()['body']); + $this->assertSame('application/sparql-update', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type')); + } + + public function testPatchN3(): void + { + $n3Patch = '@prefix solid: . _:patch a solid:InsertDeletePatch .'; + $response = new MockResponse('', ['http_code' => 200]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->patch('http://pod.example/resource', $n3Patch, 'text/n3'); + + $this->assertSame('text/n3', self::findHeader($response->getRequestOptions()['headers'], 'Content-Type')); + } + + public function testPost(): void + { + $response = new MockResponse('', [ + 'http_code' => 201, + 'response_headers' => ['Location' => 'http://pod.example/container/new-resource'], + ]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $client->post('http://pod.example/container/', '<> a .', 'new-resource'); + + $this->assertSame('POST', $response->getRequestMethod()); + $this->assertSame('new-resource', self::findHeader($response->getRequestOptions()['headers'], 'Slug')); + $this->assertStringContainsString('ldp#Resource', self::findHeader($response->getRequestOptions()['headers'], 'Link') ?? ''); + } + + public function testGet(): void + { + $body = '<> a .'; + $response = new MockResponse($body, ['http_code' => 200]); + $httpClient = new MockHttpClient($response); + $client = new SolidClient($httpClient); + + $result = $client->get('http://pod.example/resource'); + + $this->assertSame('GET', $response->getRequestMethod()); + $this->assertSame($body, $result->getContent()); + } +}