From 0cc24ae4415504e0568989f8673096d6985b4e70 Mon Sep 17 00:00:00 2001 From: Jan Henk Hazelaar Date: Thu, 7 May 2026 11:11:52 +0200 Subject: [PATCH 1/3] Add Project DTO and wire it through GetProjectRequest Introduce Project data object with a fromResponse(array) factory that maps the API's Project_UUId/Project_Titel/Project_Timestamp keys onto uuid/title/created_at/updated_at. GetProjectRequest::createDtoFromResponse delegates the field mapping to the DTO, and ProjectResource::get now returns the typed Project directly. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Data/Project.php | 23 +++++++++++++++ src/Requests/Projects/GetProjectRequest.php | 7 +++++ src/Resources/ProjectResource.php | 5 ++-- tests/Data/ProjectTest.php | 17 +++++++++++ .../Projects/GetProjectRequestTest.php | 29 +++++++++++++++++++ tests/Resources/ProjectResourceTest.php | 22 ++++++++------ 6 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 src/Data/Project.php create mode 100644 tests/Data/ProjectTest.php diff --git a/src/Data/Project.php b/src/Data/Project.php new file mode 100644 index 0000000..96ca4f7 --- /dev/null +++ b/src/Data/Project.php @@ -0,0 +1,23 @@ +uuid}/"; } + + public function createDtoFromResponse(Response $response): Project + { + return Project::fromResponse($response->json('data.object')); + } } diff --git a/src/Resources/ProjectResource.php b/src/Resources/ProjectResource.php index 2de15a7..ea1533a 100644 --- a/src/Resources/ProjectResource.php +++ b/src/Resources/ProjectResource.php @@ -2,6 +2,7 @@ namespace NieuwbouwOffice\PhpSdk\Resources; +use NieuwbouwOffice\PhpSdk\Data\Project; use NieuwbouwOffice\PhpSdk\Requests\Projects\GetProjectRequest; use NieuwbouwOffice\PhpSdk\Requests\Projects\GetProjectsRequest; use Saloon\Http\BaseResource; @@ -14,8 +15,8 @@ public function list(): Response return $this->connector->send(new GetProjectsRequest); } - public function get(string $uuid): Response + public function get(string $uuid): Project { - return $this->connector->send(new GetProjectRequest($uuid)); + return $this->connector->send(new GetProjectRequest($uuid))->dto(); } } diff --git a/tests/Data/ProjectTest.php b/tests/Data/ProjectTest.php new file mode 100644 index 0000000..f6b359e --- /dev/null +++ b/tests/Data/ProjectTest.php @@ -0,0 +1,17 @@ +uuid)->toBe('abc-123') + ->and($project->title)->toBe('Project ABC') + ->and($project->created_at)->toBe('2026-01-01T00:00:00Z') + ->and($project->updated_at)->toBe('2026-02-01T00:00:00Z'); +}); diff --git a/tests/Requests/Projects/GetProjectRequestTest.php b/tests/Requests/Projects/GetProjectRequestTest.php index e4c0e55..240af64 100644 --- a/tests/Requests/Projects/GetProjectRequestTest.php +++ b/tests/Requests/Projects/GetProjectRequestTest.php @@ -1,7 +1,11 @@ getMethod())->toBe(Method::GET); @@ -15,3 +19,28 @@ expect((new GetProjectRequest('abc-123'))->resolveEndpoint()) ->toBe('/projects/abc-123/'); }); + +it('creates a Project DTO from the response', function () { + $mockClient = new MockClient([ + GetProjectRequest::class => MockResponse::make([ + 'data' => [ + 'object' => [ + 'Project_UUId' => 'e00afb7a1791a22eb8bca3707687c549', + 'Project_Titel' => 'De Suikerzijde', + 'Project_Timestamp' => '2025-12-02 14:43:53', + ], + ], + ]), + ]); + + $connector = new NieuwbouwOffice('test-token'); + $connector->withMockClient($mockClient); + + $project = $connector->send(new GetProjectRequest('e00afb7a1791a22eb8bca3707687c549'))->dto(); + + expect($project)->toBeInstanceOf(Project::class) + ->and($project->uuid)->toBe('e00afb7a1791a22eb8bca3707687c549') + ->and($project->title)->toBe('De Suikerzijde') + ->and($project->created_at)->toBe('2025-12-02 14:43:53') + ->and($project->updated_at)->toBe('2025-12-02 14:43:53'); +}); diff --git a/tests/Resources/ProjectResourceTest.php b/tests/Resources/ProjectResourceTest.php index af6713f..a6bf615 100644 --- a/tests/Resources/ProjectResourceTest.php +++ b/tests/Resources/ProjectResourceTest.php @@ -1,5 +1,6 @@ assertSent(GetProjectsRequest::class); }); -it('get() sends a GetProjectRequest with the given uuid and returns the response', function () { +it('get() sends a GetProjectRequest with the given uuid and returns a Project DTO', function () { $mockClient = new MockClient([ GetProjectRequest::class => MockResponse::make([ - 'uuid' => 'abc-123', - 'name' => 'Project ABC', + 'data' => [ + 'object' => [ + 'Project_UUId' => 'abc-123', + 'Project_Titel' => 'Project ABC', + 'Project_Timestamp' => '2025-12-02 14:43:53', + ], + ], ]), ]); $connector = new NieuwbouwOffice('test-token'); $connector->withMockClient($mockClient); - $response = $connector->projects()->get('abc-123'); + $project = $connector->projects()->get('abc-123'); - expect($response)->toBeInstanceOf(Response::class) - ->and($response->json())->toBe([ - 'uuid' => 'abc-123', - 'name' => 'Project ABC', - ]); + expect($project)->toBeInstanceOf(Project::class) + ->and($project->uuid)->toBe('abc-123') + ->and($project->title)->toBe('Project ABC'); $mockClient->assertSent(function ($request) { return $request instanceof GetProjectRequest From 8f1f8825cf731b3d469f128c646250e47af6593b Mon Sep 17 00:00:00 2001 From: Jan Henk Hazelaar Date: Thu, 7 May 2026 11:24:42 +0200 Subject: [PATCH 2/3] Ignore macOS .DS_Store files Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 81d7cc9..6cadeca 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ psalm.xml testbench.yaml /docs /coverage + +# OS +.DS_Store From 73dac59a6ba6c235c93ec94e141b855e675e307f Mon Sep 17 00:00:00 2001 From: Jan Henk Hazelaar Date: Thu, 7 May 2026 11:24:50 +0200 Subject: [PATCH 3/3] Return Project DTOs from the list endpoint GetProjectsRequest::createDtoFromResponse maps data.objects through Project::fromResponse, and ProjectResource::list now returns Project[] via ->dto() (matching get()'s shape). Project::fromResponse prefers Laatst_Bijgewerkt for updated_at when present, falling back to Project_Timestamp so the single-project endpoint is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Data/Project.php | 2 +- src/Requests/Projects/GetProjectsRequest.php | 13 ++++++ src/Resources/ProjectResource.php | 8 ++-- .../Projects/GetProjectsRequestTest.php | 45 +++++++++++++++++++ tests/Resources/ProjectResourceTest.php | 33 +++++++++----- 5 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/Data/Project.php b/src/Data/Project.php index 96ca4f7..c2e5ba6 100644 --- a/src/Data/Project.php +++ b/src/Data/Project.php @@ -17,7 +17,7 @@ public static function fromResponse(array $data): self uuid: $data['Project_UUId'], title: $data['Project_Titel'], created_at: $data['Project_Timestamp'], - updated_at: $data['Project_Timestamp'], + updated_at: $data['Laatst_Bijgewerkt'] ?? $data['Project_Timestamp'], ); } } diff --git a/src/Requests/Projects/GetProjectsRequest.php b/src/Requests/Projects/GetProjectsRequest.php index 34ffecb..4352d4e 100644 --- a/src/Requests/Projects/GetProjectsRequest.php +++ b/src/Requests/Projects/GetProjectsRequest.php @@ -2,8 +2,10 @@ namespace NieuwbouwOffice\PhpSdk\Requests\Projects; +use NieuwbouwOffice\PhpSdk\Data\Project; use Saloon\Enums\Method; use Saloon\Http\Request; +use Saloon\Http\Response; class GetProjectsRequest extends Request { @@ -13,4 +15,15 @@ public function resolveEndpoint(): string { return '/projects/'; } + + /** + * @return Project[] + */ + public function createDtoFromResponse(Response $response): array + { + return array_map( + fn (array $object) => Project::fromResponse($object), + $response->json('data.objects'), + ); + } } diff --git a/src/Resources/ProjectResource.php b/src/Resources/ProjectResource.php index ea1533a..f98ded3 100644 --- a/src/Resources/ProjectResource.php +++ b/src/Resources/ProjectResource.php @@ -6,13 +6,15 @@ use NieuwbouwOffice\PhpSdk\Requests\Projects\GetProjectRequest; use NieuwbouwOffice\PhpSdk\Requests\Projects\GetProjectsRequest; use Saloon\Http\BaseResource; -use Saloon\Http\Response; class ProjectResource extends BaseResource { - public function list(): Response + /** + * @return Project[] + */ + public function list(): array { - return $this->connector->send(new GetProjectsRequest); + return $this->connector->send(new GetProjectsRequest)->dto(); } public function get(string $uuid): Project diff --git a/tests/Requests/Projects/GetProjectsRequestTest.php b/tests/Requests/Projects/GetProjectsRequestTest.php index 5189c59..045bb3f 100644 --- a/tests/Requests/Projects/GetProjectsRequestTest.php +++ b/tests/Requests/Projects/GetProjectsRequestTest.php @@ -1,7 +1,11 @@ getMethod())->toBe(Method::GET); @@ -10,3 +14,44 @@ it('resolves to the /projects/ endpoint', function () { expect((new GetProjectsRequest)->resolveEndpoint())->toBe('/projects/'); }); + +it('creates an array of Project DTOs from the response', function () { + $mockClient = new MockClient([ + GetProjectsRequest::class => MockResponse::make([ + 'data' => [ + 'objects' => [ + [ + 'Project_UUId' => 'e00afb7a1791a22eb8bca3707687c549', + 'Project_Titel' => 'De Suikerzijde', + 'Project_Timestamp' => '2025-12-02 14:43:53', + 'Laatst_Bijgewerkt' => '2025-12-02 14:43:53', + ], + [ + 'Project_UUId' => 'e51d280c47f8ed8b780adfc1bb25436a', + 'Project_Titel' => 'Suikerzijde - Hanny van den Horsthof', + 'Project_Timestamp' => '2025-06-12 16:53:48', + 'Laatst_Bijgewerkt' => '2026-05-07 08:47:23', + ], + ], + ], + 'meta' => ['count' => 2], + ]), + ]); + + $connector = new NieuwbouwOffice('test-token'); + $connector->withMockClient($mockClient); + + $projects = $connector->send(new GetProjectsRequest)->dto(); + + expect($projects)->toBeArray()->toHaveCount(2) + ->and($projects[0])->toBeInstanceOf(Project::class) + ->and($projects[0]->uuid)->toBe('e00afb7a1791a22eb8bca3707687c549') + ->and($projects[0]->title)->toBe('De Suikerzijde') + ->and($projects[0]->created_at)->toBe('2025-12-02 14:43:53') + ->and($projects[0]->updated_at)->toBe('2025-12-02 14:43:53') + ->and($projects[1])->toBeInstanceOf(Project::class) + ->and($projects[1]->uuid)->toBe('e51d280c47f8ed8b780adfc1bb25436a') + ->and($projects[1]->title)->toBe('Suikerzijde - Hanny van den Horsthof') + ->and($projects[1]->created_at)->toBe('2025-06-12 16:53:48') + ->and($projects[1]->updated_at)->toBe('2026-05-07 08:47:23'); +}); diff --git a/tests/Resources/ProjectResourceTest.php b/tests/Resources/ProjectResourceTest.php index a6bf615..315c896 100644 --- a/tests/Resources/ProjectResourceTest.php +++ b/tests/Resources/ProjectResourceTest.php @@ -7,30 +7,39 @@ use NieuwbouwOffice\PhpSdk\Resources\ProjectResource; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; -use Saloon\Http\Response; -it('list() sends a GetProjectsRequest and returns the response', function () { +it('list() sends a GetProjectsRequest and returns an array of Project DTOs', function () { $mockClient = new MockClient([ GetProjectsRequest::class => MockResponse::make([ 'data' => [ - ['uuid' => 'a', 'name' => 'Project A'], - ['uuid' => 'b', 'name' => 'Project B'], + 'objects' => [ + [ + 'Project_UUId' => 'a', + 'Project_Titel' => 'Project A', + 'Project_Timestamp' => '2025-12-02 14:43:53', + 'Laatst_Bijgewerkt' => '2025-12-02 14:43:53', + ], + [ + 'Project_UUId' => 'b', + 'Project_Titel' => 'Project B', + 'Project_Timestamp' => '2025-06-12 16:53:48', + 'Laatst_Bijgewerkt' => '2026-05-07 08:47:23', + ], + ], ], + 'meta' => ['count' => 2], ]), ]); $connector = new NieuwbouwOffice('test-token'); $connector->withMockClient($mockClient); - $response = $connector->projects()->list(); + $projects = $connector->projects()->list(); - expect($response)->toBeInstanceOf(Response::class) - ->and($response->json())->toBe([ - 'data' => [ - ['uuid' => 'a', 'name' => 'Project A'], - ['uuid' => 'b', 'name' => 'Project B'], - ], - ]); + expect($projects)->toBeArray()->toHaveCount(2) + ->and($projects[0])->toBeInstanceOf(Project::class) + ->and($projects[0]->uuid)->toBe('a') + ->and($projects[1]->uuid)->toBe('b'); $mockClient->assertSent(GetProjectsRequest::class); });