diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml
index b8bdcef..deba1e5 100644
--- a/.github/workflows/ci-test.yml
+++ b/.github/workflows/ci-test.yml
@@ -22,18 +22,4 @@ jobs:
- name: Run tests with coverage
run: |
mkdir -p build
- vendor/bin/phpunit \
- --coverage-text \
- --coverage-clover build/coverage.xml
-
- - name: Upload coverage artifact
- uses: actions/upload-artifact@v4
- with:
- name: coverage-clover
- path: build/coverage.xml
-
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v4
- with:
- files: build/coverage.xml
- fail_ci_if_error: true
\ No newline at end of file
+ vendor/bin/phpunit
\ No newline at end of file
diff --git a/README.md b/README.md
index 632a2c7..5c0c00b 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
HTTP client for **Yandex Cloud Vision OCR** (sync + async). Designed as a small, predictable, PSR-friendly Composer library with clear errors, DTOs, and optional concurrency via a runner interface.
-
+
diff --git a/src/Ocr/Command/GetOperationCommand.php b/src/Ocr/Command/GetOperationCommand.php
deleted file mode 100644
index 4924287..0000000
--- a/src/Ocr/Command/GetOperationCommand.php
+++ /dev/null
@@ -1,37 +0,0 @@
-httpClient->sendJsonRequest(
- 'GET',
- OcrEndpoints::OPERATION_BASE_URI . '/' . rawurlencode($operationId),
- $this->requestBuilder->buildHeaders([], false),
- null
- );
-
- return new OperationStatus($operationId, (bool) ($data['done'] ?? false), $data, $meta);
- }
-}
diff --git a/src/Ocr/Command/GetRecognitionCommand.php b/src/Ocr/Command/GetRecognitionCommand.php
deleted file mode 100644
index 3da71e8..0000000
--- a/src/Ocr/Command/GetRecognitionCommand.php
+++ /dev/null
@@ -1,39 +0,0 @@
- $operationId]);
-
- [$data, $meta] = $this->httpClient->sendJsonRequest(
- 'GET',
- OcrEndpoints::OCR_BASE_URI . '/getRecognition?' . $query,
- $this->requestBuilder->buildHeaders([], false),
- null
- );
-
- return new OcrResponse($data, $meta);
- }
-}
diff --git a/src/Ocr/Command/RecognizeTextCommand.php b/src/Ocr/Command/RecognizeTextCommand.php
deleted file mode 100644
index d3db030..0000000
--- a/src/Ocr/Command/RecognizeTextCommand.php
+++ /dev/null
@@ -1,36 +0,0 @@
- $options
- */
- public function execute(string $bytes, string $mime, array $options = []): OcrResponse
- {
- $payload = $this->requestBuilder->buildRecognizePayload($bytes, $mime, $options);
-
- [$data, $meta] = $this->httpClient->sendJsonRequest(
- 'POST',
- OcrEndpoints::OCR_BASE_URI . '/recognizeText',
- $this->requestBuilder->buildHeaders($options, true),
- $payload
- );
-
- return new OcrResponse($data, $meta);
- }
-}
diff --git a/src/Ocr/Command/StartTextRecognitionCommand.php b/src/Ocr/Command/StartTextRecognitionCommand.php
deleted file mode 100644
index 4bef1bc..0000000
--- a/src/Ocr/Command/StartTextRecognitionCommand.php
+++ /dev/null
@@ -1,42 +0,0 @@
- $options
- */
- public function execute(string $bytes, string $mime, array $options = []): OperationHandle
- {
- $payload = $this->requestBuilder->buildRecognizePayload($bytes, $mime, $options);
-
- [$data, $meta] = $this->httpClient->sendJsonRequest(
- 'POST',
- OcrEndpoints::OCR_BASE_URI . '/recognizeTextAsync',
- $this->requestBuilder->buildHeaders($options, true),
- $payload
- );
-
- $operationId = $data['id'] ?? null;
- if (!is_string($operationId) || $operationId === '') {
- throw new ValidationException('Missing operation id in async recognition response.');
- }
-
- return new OperationHandle($operationId, $meta['request_id'] ?? null);
- }
-}
diff --git a/src/Ocr/Command/WaitCommand.php b/src/Ocr/Command/WaitCommand.php
deleted file mode 100644
index 1ad1d14..0000000
--- a/src/Ocr/Command/WaitCommand.php
+++ /dev/null
@@ -1,81 +0,0 @@
-getOperationCommand->execute($operationId);
- $payload = $status->getPayload();
-
- if ($status->isDone()) {
- $errorMessage = $this->extractOperationError($payload);
- if ($errorMessage !== null) {
- throw new ApiException($errorMessage);
- }
-
- $response = $payload['response'] ?? null;
- if (is_array($response)) {
- return new OcrResponse($response, $status->getMeta());
- }
-
- return $this->getRecognitionCommand->execute($operationId);
- }
-
- $errorMessage = $this->extractOperationError($payload);
- if ($errorMessage !== null) {
- throw new ApiException($errorMessage);
- }
-
- $now = time();
- if ($now >= $deadline) {
- throw new TimeoutException('OCR operation timed out.');
- }
-
- $delay = $backoff->getDelayForAttempt($attempt);
- $remaining = $deadline - $now;
- $sleepSeconds = min($delay, $remaining);
- if ($sleepSeconds > 0) {
- sleep($sleepSeconds);
- }
- $attempt++;
- }
- }
-
- /**
- * @param array $payload
- */
- private function extractOperationError(array $payload): ?string
- {
- $error = $payload['error'] ?? null;
- if (!is_array($error)) {
- return null;
- }
-
- $message = $error['message'] ?? null;
- if (is_string($message) && $message !== '') {
- return $message;
- }
-
- return 'OCR operation failed.';
- }
-}
diff --git a/src/Dto/OcrResponse.php b/src/Ocr/Dto/OcrResponse.php
similarity index 92%
rename from src/Dto/OcrResponse.php
rename to src/Ocr/Dto/OcrResponse.php
index 9dde123..0d519ac 100644
--- a/src/Dto/OcrResponse.php
+++ b/src/Ocr/Dto/OcrResponse.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace PhpVision\YandexVision\Dto;
+namespace PhpVision\YandexVision\Ocr\Dto;
final readonly class OcrResponse
{
diff --git a/src/Dto/OperationStatus.php b/src/Ocr/Dto/OperationStatus.php
similarity index 94%
rename from src/Dto/OperationStatus.php
rename to src/Ocr/Dto/OperationStatus.php
index 9a7e195..703b249 100644
--- a/src/Dto/OperationStatus.php
+++ b/src/Ocr/Dto/OperationStatus.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace PhpVision\YandexVision\Dto;
+namespace PhpVision\YandexVision\Ocr\Dto;
final readonly class OperationStatus
{
diff --git a/src/Ocr/OcrHttpClient.php b/src/Ocr/OcrHttpClient.php
index 247c387..77332f2 100644
--- a/src/Ocr/OcrHttpClient.php
+++ b/src/Ocr/OcrHttpClient.php
@@ -4,6 +4,7 @@
namespace PhpVision\YandexVision\Ocr;
+use PhpVision\YandexVision\Ocr\Request\OcrRequestInterface;
use PhpVision\YandexVision\Exception\ApiException;
use PhpVision\YandexVision\Exception\HttpException;
use PhpVision\YandexVision\Exception\ValidationException;
@@ -22,12 +23,25 @@ public function __construct(
) {
}
+ /**
+ * @return array{0: array, 1: array}
+ */
+ public function send(OcrRequestInterface $request): array
+ {
+ return $this->sendJsonRequest(
+ $request->getMethod(),
+ $request->getUrl(),
+ $request->getHeaders(),
+ $request->getBody()
+ );
+ }
+
/**
* @param array $headers
* @param array|null $body
* @return array{0: array, 1: array}
*/
- public function sendJsonRequest(string $method, string $url, array $headers, ?array $body): array
+ private function sendJsonRequest(string $method, string $url, array $headers, ?array $body): array
{
$request = $this->requestFactory->createRequest($method, $url);
foreach ($headers as $name => $value) {
diff --git a/src/Ocr/OcrService.php b/src/Ocr/OcrService.php
index 3125100..47370c9 100644
--- a/src/Ocr/OcrService.php
+++ b/src/Ocr/OcrService.php
@@ -7,24 +7,22 @@
use PhpVision\YandexVision\Auth\CredentialProviderInterface;
use PhpVision\YandexVision\Concurrency\RunnerInterface;
use PhpVision\YandexVision\Concurrency\SequentialRunner;
-use PhpVision\YandexVision\Dto\OcrResponse;
-use PhpVision\YandexVision\Dto\OperationStatus;
-use PhpVision\YandexVision\Ocr\Command\GetOperationCommand;
-use PhpVision\YandexVision\Ocr\Command\GetRecognitionCommand;
-use PhpVision\YandexVision\Ocr\Command\RecognizeTextCommand;
-use PhpVision\YandexVision\Ocr\Command\StartTextRecognitionCommand;
-use PhpVision\YandexVision\Ocr\Command\WaitCommand;
+use PhpVision\YandexVision\Ocr\Dto\OcrResponse;
+use PhpVision\YandexVision\Ocr\Dto\OperationStatus;
+use PhpVision\YandexVision\Exception\ApiException;
+use PhpVision\YandexVision\Exception\TimeoutException;
+use PhpVision\YandexVision\Exception\ValidationException;
+use PhpVision\YandexVision\Ocr\Request\OperationRequest;
+use PhpVision\YandexVision\Ocr\Request\RecognitionRequest;
+use PhpVision\YandexVision\Ocr\Request\TextRecognitionAsyncRequest;
+use PhpVision\YandexVision\Ocr\Request\TextRecognitionRequest;
use PhpVision\YandexVision\Transports\TransportInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
final readonly class OcrService
{
- private RecognizeTextCommand $recognizeTextCommand;
- private StartTextRecognitionCommand $startTextRecognitionCommand;
- private GetOperationCommand $getOperationCommand;
- private GetRecognitionCommand $getRecognitionCommand;
- private WaitCommand $waitCommand;
+ private OcrHttpClient $httpClient;
private OcrRequestBuilder $requestBuilder;
public function __construct(
@@ -34,13 +32,7 @@ public function __construct(
StreamFactoryInterface $streamFactory
) {
$this->requestBuilder = new OcrRequestBuilder($credentials);
- $httpClient = new OcrHttpClient($transport, $requestFactory, $streamFactory);
-
- $this->recognizeTextCommand = new RecognizeTextCommand($httpClient, $this->requestBuilder);
- $this->startTextRecognitionCommand = new StartTextRecognitionCommand($httpClient, $this->requestBuilder);
- $this->getOperationCommand = new GetOperationCommand($httpClient, $this->requestBuilder);
- $this->getRecognitionCommand = new GetRecognitionCommand($httpClient, $this->requestBuilder);
- $this->waitCommand = new WaitCommand($this->getOperationCommand, $this->getRecognitionCommand);
+ $this->httpClient = new OcrHttpClient($transport, $requestFactory, $streamFactory);
}
/**
@@ -48,7 +40,12 @@ public function __construct(
*/
public function recognizeText(string $bytes, string $mime, array $options = []): OcrResponse
{
- return $this->recognizeTextCommand->execute($bytes, $mime, $options);
+ $payload = $this->requestBuilder->buildRecognizePayload($bytes, $mime, $options);
+ $headers = $this->requestBuilder->buildHeaders($options, true);
+
+ [$data, $meta] = $this->httpClient->send(new TextRecognitionRequest($payload, $headers));
+
+ return new OcrResponse($data, $meta);
}
/**
@@ -58,7 +55,7 @@ public function recognizeTextFromFile(string $path, array $options = []): OcrRes
{
[$bytes, $mime] = $this->requestBuilder->readFilePayload($path, $options);
- return $this->recognizeTextCommand->execute($bytes, $mime, $options);
+ return $this->recognizeText($bytes, $mime, $options);
}
/**
@@ -66,7 +63,17 @@ public function recognizeTextFromFile(string $path, array $options = []): OcrRes
*/
public function startTextRecognition(string $bytes, string $mime, array $options = []): OperationHandle
{
- return $this->startTextRecognitionCommand->execute($bytes, $mime, $options);
+ $payload = $this->requestBuilder->buildRecognizePayload($bytes, $mime, $options);
+ $headers = $this->requestBuilder->buildHeaders($options, true);
+
+ [$data, $meta] = $this->httpClient->send(new TextRecognitionAsyncRequest($payload, $headers));
+
+ $operationId = $data['id'] ?? null;
+ if (!is_string($operationId) || $operationId === '') {
+ throw new ValidationException('Missing operation id in async recognition response.');
+ }
+
+ return new OperationHandle($operationId, $meta['request_id'] ?? null);
}
/**
@@ -76,22 +83,71 @@ public function startTextRecognitionFromFile(string $path, array $options = []):
{
[$bytes, $mime] = $this->requestBuilder->readFilePayload($path, $options);
- return $this->startTextRecognitionCommand->execute($bytes, $mime, $options);
+ return $this->startTextRecognition($bytes, $mime, $options);
}
public function getOperation(string $operationId): OperationStatus
{
- return $this->getOperationCommand->execute($operationId);
+ $headers = $this->requestBuilder->buildHeaders([], false);
+ $request = new OperationRequest($operationId, $headers);
+
+ [$data, $meta] = $this->httpClient->send($request);
+
+ return new OperationStatus($request->getOperationId(), (bool) ($data['done'] ?? false), $data, $meta);
}
public function getRecognition(string $operationId): OcrResponse
{
- return $this->getRecognitionCommand->execute($operationId);
+ $headers = $this->requestBuilder->buildHeaders([], false);
+ $request = new RecognitionRequest($operationId, $headers);
+
+ [$data, $meta] = $this->httpClient->send($request);
+
+ return new OcrResponse($data, $meta);
}
public function wait(string $operationId, int $timeoutSeconds = 60, ?BackoffPolicy $backoff = null): OcrResponse
{
- return $this->waitCommand->execute($operationId, $timeoutSeconds, $backoff);
+ $backoff = $backoff ?? new BackoffPolicy();
+ $deadline = time() + max(0, $timeoutSeconds);
+ $attempt = 0;
+
+ while (true) {
+ $status = $this->getOperation($operationId);
+ $payload = $status->getPayload();
+
+ if ($status->isDone()) {
+ $errorMessage = $this->extractOperationError($payload);
+ if ($errorMessage !== null) {
+ throw new ApiException($errorMessage);
+ }
+
+ $response = $payload['response'] ?? null;
+ if (is_array($response)) {
+ return new OcrResponse($response, $status->getMeta());
+ }
+
+ return $this->getRecognition($operationId);
+ }
+
+ $errorMessage = $this->extractOperationError($payload);
+ if ($errorMessage !== null) {
+ throw new ApiException($errorMessage);
+ }
+
+ $now = time();
+ if ($now >= $deadline) {
+ throw new TimeoutException('OCR operation timed out.');
+ }
+
+ $delay = $backoff->getDelayForAttempt($attempt);
+ $remaining = $deadline - $now;
+ $sleepSeconds = min($delay, $remaining);
+ if ($sleepSeconds > 0) {
+ sleep($sleepSeconds);
+ }
+ $attempt++;
+ }
}
/**
@@ -114,4 +170,22 @@ public function waitMany(
return $runner->run($tasks);
}
+ /**
+ * @param array $payload
+ */
+ private function extractOperationError(array $payload): ?string
+ {
+ $error = $payload['error'] ?? null;
+ if (!is_array($error)) {
+ return null;
+ }
+
+ $message = $error['message'] ?? null;
+ if (is_string($message) && $message !== '') {
+ return $message;
+ }
+
+ return 'OCR operation failed.';
+ }
+
}
diff --git a/src/Ocr/Request/OcrRequestInterface.php b/src/Ocr/Request/OcrRequestInterface.php
new file mode 100644
index 0000000..ae5ad55
--- /dev/null
+++ b/src/Ocr/Request/OcrRequestInterface.php
@@ -0,0 +1,22 @@
+
+ */
+ public function getHeaders(): array;
+
+ /**
+ * @return array|null
+ */
+ public function getBody(): ?array;
+}
diff --git a/src/Ocr/Request/OperationRequest.php b/src/Ocr/Request/OperationRequest.php
new file mode 100644
index 0000000..f4b0406
--- /dev/null
+++ b/src/Ocr/Request/OperationRequest.php
@@ -0,0 +1,54 @@
+ $headers
+ */
+ public function __construct(string $operationId, private array $headers)
+ {
+ $operationId = trim($operationId);
+ if ($operationId === '') {
+ throw new ValidationException('Operation id must be a non-empty string.');
+ }
+
+ $this->operationId = $operationId;
+ }
+
+ public function getMethod(): string
+ {
+ return 'GET';
+ }
+
+ public function getUrl(): string
+ {
+ return OcrEndpoints::OPERATION_BASE_URI . '/' . rawurlencode($this->operationId);
+ }
+
+ /**
+ * @return array
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ public function getBody(): ?array
+ {
+ return null;
+ }
+
+ public function getOperationId(): string
+ {
+ return $this->operationId;
+ }
+}
diff --git a/src/Ocr/Request/RecognitionRequest.php b/src/Ocr/Request/RecognitionRequest.php
new file mode 100644
index 0000000..29c5da2
--- /dev/null
+++ b/src/Ocr/Request/RecognitionRequest.php
@@ -0,0 +1,51 @@
+ $headers
+ */
+ public function __construct(string $operationId, private array $headers)
+ {
+ $operationId = trim($operationId);
+ if ($operationId === '') {
+ throw new ValidationException('Operation id must be a non-empty string.');
+ }
+
+ $this->operationId = $operationId;
+ }
+
+ public function getMethod(): string
+ {
+ return 'GET';
+ }
+
+ public function getUrl(): string
+ {
+ $query = http_build_query(['operationId' => $this->operationId]);
+
+ return OcrEndpoints::OCR_BASE_URI . '/getRecognition?' . $query;
+ }
+
+ /**
+ * @return array
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ public function getBody(): ?array
+ {
+ return null;
+ }
+}
diff --git a/src/Ocr/Request/TextRecognitionAsyncRequest.php b/src/Ocr/Request/TextRecognitionAsyncRequest.php
new file mode 100644
index 0000000..c3169a0
--- /dev/null
+++ b/src/Ocr/Request/TextRecognitionAsyncRequest.php
@@ -0,0 +1,44 @@
+ $payload
+ * @param array $headers
+ */
+ public function __construct(private array $payload, private array $headers)
+ {
+ }
+
+ public function getMethod(): string
+ {
+ return 'POST';
+ }
+
+ public function getUrl(): string
+ {
+ return OcrEndpoints::OCR_BASE_URI . '/recognizeTextAsync';
+ }
+
+ /**
+ * @return array
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ /**
+ * @return array
+ */
+ public function getBody(): array
+ {
+ return $this->payload;
+ }
+}
diff --git a/src/Ocr/Request/TextRecognitionRequest.php b/src/Ocr/Request/TextRecognitionRequest.php
new file mode 100644
index 0000000..5a4f114
--- /dev/null
+++ b/src/Ocr/Request/TextRecognitionRequest.php
@@ -0,0 +1,44 @@
+ $payload
+ * @param array $headers
+ */
+ public function __construct(private array $payload, private array $headers)
+ {
+ }
+
+ public function getMethod(): string
+ {
+ return 'POST';
+ }
+
+ public function getUrl(): string
+ {
+ return OcrEndpoints::OCR_BASE_URI . '/recognizeText';
+ }
+
+ /**
+ * @return array
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ /**
+ * @return array
+ */
+ public function getBody(): array
+ {
+ return $this->payload;
+ }
+}
diff --git a/tests/OcrServiceTest.php b/tests/OcrServiceTest.php
index 9e0505f..faa7f9b 100644
--- a/tests/OcrServiceTest.php
+++ b/tests/OcrServiceTest.php
@@ -40,6 +40,8 @@ public function testRecognizeTextBuildsPayloadAndHeaders(): void
$request = $transport->getLastRequest();
self::assertNotNull($request);
+ self::assertSame('POST', $request->getMethod());
+ self::assertSame('/ocr/v1/recognizeText', $request->getUri()->getPath());
self::assertSame('Api-Key test', $request->getHeaderLine('Authorization'));
self::assertSame('folder-1', $request->getHeaderLine('x-folder-id'));
self::assertSame('req-override', $request->getHeaderLine('x-request-id'));
@@ -87,6 +89,11 @@ public function testStartTextRecognitionReturnsOperationHandle(): void
self::assertSame('op-1', $handle->getOperationId());
self::assertSame('req-op', $handle->getRequestId());
+
+ $request = $transport->getLastRequest();
+ self::assertNotNull($request);
+ self::assertSame('POST', $request->getMethod());
+ self::assertSame('/ocr/v1/recognizeTextAsync', $request->getUri()->getPath());
}
public function testGetOperationRequiresId(): void
@@ -97,6 +104,14 @@ public function testGetOperationRequiresId(): void
$service->getOperation(' ');
}
+ public function testGetRecognitionRequiresId(): void
+ {
+ $service = $this->createService(new FakeTransport());
+
+ $this->expectException(ValidationException::class);
+ $service->getRecognition(' ');
+ }
+
public function testWaitReturnsResponseFromOperation(): void
{
$transport = new FakeTransport([