From dbbcb5ab37305cb7db52417b9857147d5d948dcf Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Thu, 22 Jan 2026 13:35:58 +0100 Subject: [PATCH 1/2] feat: add Media resource with get, getUploadUrl, and patch methods - Add Media resource class with get(), getUploadUrl(), and patch() methods - Add MediaResponse and MediaUploadUrlResponse DTOs - Add patchJson() method to TransporterInterface for PATCH requests - Add test fixtures for mocking media API responses - Include feature tests for all media operations --- src/Contracts/TransporterInterface.php | 16 ++-- src/Langfuse.php | 8 +- src/Media.php | 89 +++++++++++++++++++ src/Responses/MediaResponse.php | 39 ++++++++ src/Responses/MediaUploadUrlResponse.php | 27 ++++++ src/Testing/Responses/GetMediaResponse.php | 35 ++++++++ src/Testing/Responses/PatchMediaResponse.php | 18 ++++ .../Responses/PostMediaUploadUrlResponse.php | 31 +++++++ src/Transporters/HttpTransporter.php | 13 ++- tests/Feature/MediaTest.php | 67 ++++++++++++++ 10 files changed, 327 insertions(+), 16 deletions(-) create mode 100644 src/Media.php create mode 100644 src/Responses/MediaResponse.php create mode 100644 src/Responses/MediaUploadUrlResponse.php create mode 100644 src/Testing/Responses/GetMediaResponse.php create mode 100644 src/Testing/Responses/PatchMediaResponse.php create mode 100644 src/Testing/Responses/PostMediaUploadUrlResponse.php create mode 100644 tests/Feature/MediaTest.php diff --git a/src/Contracts/TransporterInterface.php b/src/Contracts/TransporterInterface.php index 8234ade..558497a 100644 --- a/src/Contracts/TransporterInterface.php +++ b/src/Contracts/TransporterInterface.php @@ -9,34 +9,34 @@ interface TransporterInterface { /** - * @param array $options + * @param array $options */ public function request(string $method, string $uri, array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function get(string $uri, array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function post(string $uri, array $options = []): ResponseInterface; /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options */ public function postJson(string $uri, array $data = [], array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function delete(string $uri, array $options = []): ResponseInterface; /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options */ public function patchJson(string $uri, array $data = [], array $options = []): ResponseInterface; } diff --git a/src/Langfuse.php b/src/Langfuse.php index ebe96f9..82120d9 100644 --- a/src/Langfuse.php +++ b/src/Langfuse.php @@ -12,7 +12,13 @@ public function __construct( private readonly TransporterInterface $transporter, private readonly string $environment = 'default', private readonly string $label = 'latest', - ) { + ) {} + + public function media(): Media + { + return new Media( + transporter: $this->transporter, + ); } public function prompt(): Prompt diff --git a/src/Media.php b/src/Media.php new file mode 100644 index 0000000..6d60c17 --- /dev/null +++ b/src/Media.php @@ -0,0 +1,89 @@ +transporter->get( + uri: sprintf('/api/public/media/%s', urlencode($mediaId)), + ); + + /** @var array{ + * mediaId: string, + * contentType: string, + * contentLength: int, + * uploadedAt: string, + * url: string, + * urlExpiry: string + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return MediaResponse::fromArray($data); + } + + /** + * @throws JsonException + */ + public function getUploadUrl( + string $traceId, + string $contentType, + int $contentLength, + string $sha256Hash, + string $field, + ?string $observationId = null, + ): MediaUploadUrlResponse { + $response = $this->transporter->postJson( + uri: '/api/public/media', + data: array_filter([ + 'traceId' => $traceId, + 'observationId' => $observationId, + 'contentType' => $contentType, + 'contentLength' => $contentLength, + 'sha256Hash' => $sha256Hash, + 'field' => $field, + ], fn ($v) => $v !== null), + ); + + /** @var array{ + * uploadUrl: string|null, + * mediaId: string + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return MediaUploadUrlResponse::fromArray($data); + } + + public function patch( + string $mediaId, + string $uploadedAt, + int $uploadHttpStatus, + ?string $uploadHttpError = null, + ?int $uploadTimeMs = null, + ): void { + $this->transporter->patchJson( + uri: sprintf('/api/public/media/%s', urlencode($mediaId)), + data: array_filter([ + 'uploadedAt' => $uploadedAt, + 'uploadHttpStatus' => $uploadHttpStatus, + 'uploadHttpError' => $uploadHttpError, + 'uploadTimeMs' => $uploadTimeMs, + ], fn ($v) => $v !== null), + ); + } +} diff --git a/src/Responses/MediaResponse.php b/src/Responses/MediaResponse.php new file mode 100644 index 0000000..b214770 --- /dev/null +++ b/src/Responses/MediaResponse.php @@ -0,0 +1,39 @@ +|string> $headers + * @param array $data + */ + public function __construct(int $status = 200, array $headers = [], string $version = '1.1', ?string $reason = null, array $data = []) + { + parent::__construct($status, $headers, (string) json_encode($this->payload($data)), $version, $reason); + } + + /** + * @param array $data + * @return array + */ + public function payload(array $data = []): array + { + return array_merge([ + 'mediaId' => 'media-abc123', + 'contentType' => 'image/png', + 'contentLength' => 12345, + 'uploadedAt' => '2025-01-22T10:00:00.000Z', + 'url' => 'https://storage.langfuse.com/media/abc123.png', + 'urlExpiry' => '2025-01-22T11:00:00.000Z', + ], $data); + } +} diff --git a/src/Testing/Responses/PatchMediaResponse.php b/src/Testing/Responses/PatchMediaResponse.php new file mode 100644 index 0000000..58319da --- /dev/null +++ b/src/Testing/Responses/PatchMediaResponse.php @@ -0,0 +1,18 @@ +|string> $headers + */ + public function __construct(int $status = 204, array $headers = [], string $version = '1.1', ?string $reason = null) + { + parent::__construct($status, $headers, '', $version, $reason); + } +} diff --git a/src/Testing/Responses/PostMediaUploadUrlResponse.php b/src/Testing/Responses/PostMediaUploadUrlResponse.php new file mode 100644 index 0000000..8f84da0 --- /dev/null +++ b/src/Testing/Responses/PostMediaUploadUrlResponse.php @@ -0,0 +1,31 @@ +|string> $headers + * @param array $data + */ + public function __construct(int $status = 200, array $headers = [], string $version = '1.1', ?string $reason = null, array $data = []) + { + parent::__construct($status, $headers, (string) json_encode($this->payload($data)), $version, $reason); + } + + /** + * @param array $data + * @return array + */ + public function payload(array $data = []): array + { + return array_merge([ + 'mediaId' => 'media-new123', + 'uploadUrl' => 'https://storage.langfuse.com/upload/presigned-url', + ], $data); + } +} diff --git a/src/Transporters/HttpTransporter.php b/src/Transporters/HttpTransporter.php index 319cf49..a2b734b 100644 --- a/src/Transporters/HttpTransporter.php +++ b/src/Transporters/HttpTransporter.php @@ -21,8 +21,7 @@ class HttpTransporter implements TransporterInterface { public function __construct( public readonly ClientInterface $client - ) { - } + ) {} /** * @throws BadRequestException @@ -45,7 +44,7 @@ public function request(string $method, string $uri, array $options = []): Respo } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -61,7 +60,7 @@ public function get(string $uri, array $options = []): ResponseInterface } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -82,7 +81,7 @@ public function postJson(string $uri, array $data = [], array $options = []): Re } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -98,8 +97,8 @@ public function delete(string $uri, array $options = []): ResponseInterface } /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options * * @throws BadRequestException * @throws ForbiddenException diff --git a/tests/Feature/MediaTest.php b/tests/Feature/MediaTest.php new file mode 100644 index 0000000..482cfaf --- /dev/null +++ b/tests/Feature/MediaTest.php @@ -0,0 +1,67 @@ + $handlerStack]); + + $media = (new Langfuse(new HttpTransporter($client))) + ->media() + ->get('media-abc123'); + + expect($media)->toBeInstanceOf(MediaResponse::class) + ->and($media->mediaId)->toBe('media-abc123') + ->and($media->contentType)->toBe('image/png') + ->and($media->contentLength)->toBe(12345) + ->and($media->uploadedAt)->toBe('2025-01-22T10:00:00.000Z') + ->and($media->url)->toBe('https://storage.langfuse.com/media/abc123.png') + ->and($media->urlExpiry)->toBe('2025-01-22T11:00:00.000Z'); +}); + +it('can get upload url', function (): void { + $mock = new MockHandler([new PostMediaUploadUrlResponse]); + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $response = (new Langfuse(new HttpTransporter($client))) + ->media() + ->getUploadUrl( + traceId: 'trace-123', + contentType: 'image/png', + contentLength: 12345, + sha256Hash: 'abc123hash', + field: 'input', + ); + + expect($response)->toBeInstanceOf(MediaUploadUrlResponse::class) + ->and($response->mediaId)->toBe('media-new123') + ->and($response->uploadUrl)->toBe('https://storage.langfuse.com/upload/presigned-url'); +}); + +it('can patch a media record', function (): void { + $mock = new MockHandler([new PatchMediaResponse]); + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $langfuse = new Langfuse(new HttpTransporter($client)); + $langfuse->media()->patch( + mediaId: 'media-abc123', + uploadedAt: '2025-01-22T10:00:00.000Z', + uploadHttpStatus: 200, + ); + + expect($mock->count())->toBe(0); +}); From 10710649b4df8a7f4a02d068de4ab68cc9d2207c Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 20 Feb 2026 13:37:19 +0100 Subject: [PATCH 2/2] chore: remove test:refactor dry-run --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9651b02..98892f9 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "test:lint": "@php vendor/bin/pint --config https://raw.githubusercontent.com/DIJ-digital/pint-config/main/pint.json", "test:unit": "pest", "test:types": "phpstan", - "test:refactor": "rector --dry-run", + "test:refactor": "rector", "test": [ "@test:lint", "@test:type-coverage",